From f338742b55524f90d33e1f49157c95b440a66612 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:20:23 +0000 Subject: [PATCH] fix: harden qa monitoring smoke docs --- agents/qa-engineer/AGENTS.md | 4 +- docs/paperclip-ops-runbook.md | 6 ++- tests/scripts/test_e2e_smoke.py | 70 ++++++++++++++++----------------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/agents/qa-engineer/AGENTS.md b/agents/qa-engineer/AGENTS.md index ffc10a63..323921f9 100644 --- a/agents/qa-engineer/AGENTS.md +++ b/agents/qa-engineer/AGENTS.md @@ -25,7 +25,7 @@ You are running without a human operator. NEVER call `AskUserQuestion`. When ski ## Log Sources -1. **Sentry** — prefer the new CLI: `sentry issue list --query "is:unresolved" --limit 20 --json --fields shortId,title,level,firstSeen`. If only `sentry-cli` exists, use `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20`. Use `sentry issue view ` or Sentry REST API for details. +1. **Sentry** — prefer the installed CLI: `sentry issues list --query "is:unresolved" --max-rows 20`. If only `sentry-cli` exists, use `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20`. Use Sentry REST API for details when the CLI list output is insufficient. 2. **Coolify app logs** — use `mcp__coolify__application_logs` for app `v0kkssccwoswgwwscws4kscc`. Use `mcp__coolify__get_application` for app lookup/status. @@ -120,7 +120,7 @@ even when the run is partial or errored. When reviewing after a deploy, whether from scheduled heartbeat, Sentry trigger, or handoff: 1. **Run `/canary`** — MANDATORY. Handles console errors, performance regressions, page failures, baseline comparison. -2. **Sentry scan** — run `sentry issue list --query "is:unresolved" --limit 20 --json --fields shortId,title,level,firstSeen`; if only legacy `sentry-cli` exists, run `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20`. Cross-reference against the deploy timestamp. +2. **Sentry scan** — run `sentry issues list --query "is:unresolved" --max-rows 20`; if only legacy `sentry-cli` exists, run `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20`. Cross-reference against the deploy timestamp. 3. Run E2E smoke tests if credentials are configured (see below). 4. Report results to **CTO** — GREEN (all clear) or RED (issues found). diff --git a/docs/paperclip-ops-runbook.md b/docs/paperclip-ops-runbook.md index 9b8636eb..9f2a1750 100644 --- a/docs/paperclip-ops-runbook.md +++ b/docs/paperclip-ops-runbook.md @@ -403,7 +403,11 @@ Agents need `PATH=/paperclip/bin:$PATH` to find them. | `codex` | `/paperclip/bin/codex` | `npm install --prefix /paperclip/.npm-global @openai/codex@latest && ln -sf /paperclip/.npm-global/node_modules/.bin/codex /paperclip/bin/codex` | | `sentry` / `sentry-cli` | `/paperclip/bin/sentry` or system path | `npm install --prefix /paperclip/.npm-global sentry @sentry/cli && ln -sf /paperclip/.npm-global/node_modules/.bin/sentry /paperclip/bin/sentry && ln -sf /paperclip/.npm-global/node_modules/.bin/sentry-cli /paperclip/bin/sentry-cli` | -`sentry` and legacy `sentry-cli` use different issue-list syntax. Prefer `sentry issue list --query "is:unresolved" --limit 20`; use `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20` only as a legacy fallback. QA and CTO receive `SENTRY_ORG=ffmemes` and `SENTRY_PROJECT=ff-backend` from the manifest. +`sentry` and legacy `sentry-cli` can expose different issue-list flags across +installed versions. Prefer `sentry issues list --query "is:unresolved" --max-rows 20`; +use `sentry-cli issues list --org "$SENTRY_ORG" --project "$SENTRY_PROJECT" --status unresolved --max-rows 20` +only as a legacy fallback. QA and CTO receive `SENTRY_ORG=ffmemes` and +`SENTRY_PROJECT=ff-backend` from the manifest. Post-deployment command (runs after each Coolify deploy) is configured to reinstall these, but runs as non-root `node` user — see Coolify Quirks below. diff --git a/tests/scripts/test_e2e_smoke.py b/tests/scripts/test_e2e_smoke.py index 1b4e45a7..e819aeee 100644 --- a/tests/scripts/test_e2e_smoke.py +++ b/tests/scripts/test_e2e_smoke.py @@ -1,51 +1,49 @@ -import importlib.util -import sys -from pathlib import Path -from types import SimpleNamespace +from scripts.e2e_smoke import button_data, find_like_button, has_reaction_buttons -SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "scripts" -if str(SCRIPTS_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPTS_DIR)) -spec = importlib.util.spec_from_file_location("e2e_smoke", SCRIPTS_DIR / "e2e_smoke.py") -assert spec and spec.loader -e2e_smoke = importlib.util.module_from_spec(spec) -spec.loader.exec_module(e2e_smoke) +class _UrlButton: + url = "https://t.me/share/url?url=https%3A%2F%2Ft.me%2Fffmemesbot" -class UrlButton: - url = "https://t.me/share/url?url=https%3A%2F%2Ft.me%2Fffmemesbot" +class _CallbackButton: + def __init__(self, data: bytes) -> None: + self.data = data + + +class _Row: + def __init__(self, buttons: list[object]) -> None: + self.buttons = buttons + + +class _ReplyMarkup: + def __init__(self, rows: list[_Row]) -> None: + self.rows = rows + + +class _Message: + def __init__(self, buttons: list[object]) -> None: + self.reply_markup = _ReplyMarkup([_Row(buttons)]) -def _message_with_buttons(*buttons): - return SimpleNamespace( - reply_markup=SimpleNamespace( - rows=[SimpleNamespace(buttons=list(buttons))], - ) - ) +def test_button_data_ignores_url_buttons_without_data() -> None: + assert button_data(_UrlButton()) == b"" -def test_has_reaction_buttons_ignores_url_buttons_without_data(): - msg = _message_with_buttons( - UrlButton(), - SimpleNamespace(data=b"r:123:1"), - ) +def test_reaction_button_detection_skips_url_buttons() -> None: + msg = _Message([_UrlButton(), _CallbackButton(b"r:123:1")]) - assert e2e_smoke.has_reaction_buttons(msg) is True + assert has_reaction_buttons(msg) is True -def test_find_like_button_skips_url_buttons_before_reaction_row(): - like_button = SimpleNamespace(data=b"r:123:1") - msg = _message_with_buttons( - UrlButton(), - SimpleNamespace(data=b"r:123:2"), - like_button, - ) +def test_find_like_button_skips_url_buttons_before_reaction_row() -> None: + like_button = _CallbackButton(b"r:123:1") + msg = _Message([_UrlButton(), _CallbackButton(b"r:123:2"), like_button]) - assert e2e_smoke.find_like_button(msg) is like_button + assert find_like_button(msg) is like_button -def test_has_reaction_buttons_returns_false_for_url_only_keyboard(): - msg = _message_with_buttons(UrlButton()) +def test_reaction_button_detection_handles_url_only_markup() -> None: + msg = _Message([_UrlButton()]) - assert e2e_smoke.has_reaction_buttons(msg) is False + assert has_reaction_buttons(msg) is False + assert find_like_button(msg) is None