Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agents/qa-engineer/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` 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.
Expand Down Expand Up @@ -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).

Expand Down
6 changes: 5 additions & 1 deletion docs/paperclip-ops-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
70 changes: 34 additions & 36 deletions tests/scripts/test_e2e_smoke.py
Original file line number Diff line number Diff line change
@@ -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
Loading