test(runners): single-account invariant guard (EVO-1228)#51
Conversation
Greps every src/runners source for account-routing keywords (accountId, account_id, Account., tenant, accountById, byAccount, case-insensitive) and fails with file:line + keyword + snippet. Exceptions are literals in the spec (audit trail): only the ADR14 TenantDbContext seam. Includes a guard self-test and covers future runner directories automatically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors tenant-db-context-guard: blocks merges to develop/main when the invariant spec fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EVO-1228) Review finding M1: a whole-line exemption let any violating line pass by mentioning the sanctioned seam token in a comment. Allowed tokens are now stripped from the line before the forbidden-pattern scan, with an anti-laundering self-test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewer's GuideAdds a Jest-based "single-account invariant" guard spec that recursively scans all runner TypeScript sources for account-routing keywords, with a minimal allowlist, and wires it into CI via a new GitHub Actions workflow that blocks merges on violations. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The failure message in
assertNoViolationsreferencesALLOWED_LINE_PATTERNS, but the actual allowlist isALLOWED_TOKENS; consider renaming one or the other so the guidance is accurate and consistent. - In the workflow, you may want to invoke Jest with the explicit path (
npx jest --runInBand src/runners/single-account.spec.ts) rather than the patternsingle-account.specto make the guard more robust to future Jest config or test naming changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The failure message in `assertNoViolations` references `ALLOWED_LINE_PATTERNS`, but the actual allowlist is `ALLOWED_TOKENS`; consider renaming one or the other so the guidance is accurate and consistent.
- In the workflow, you may want to invoke Jest with the explicit path (`npx jest --runInBand src/runners/single-account.spec.ts`) rather than the pattern `single-account.spec` to make the guard more robust to future Jest config or test naming changes.
## Individual Comments
### Comment 1
<location path=".github/workflows/single-account-guard.yml" line_range="24-25" />
<code_context>
+ node-version: 20
+ - name: Install dependencies
+ run: npm ci
+ - name: Enforce single-account invariant
+ run: npx jest --runInBand single-account.spec
</code_context>
<issue_to_address>
**suggestion:** Consider pinning the Jest target to the exact spec path for robustness.
Using the bare pattern `single-account.spec` depends on Jest’s matching rules and file naming. If a similarly named test is added or the file is moved, this job could start running the wrong tests or miss this guard entirely. To avoid that, point Jest at the exact file path, e.g.
```yaml
run: npx jest --runInBand src/runners/single-account.spec.ts
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
dpaes
left a comment
There was a problem hiding this comment.
Code review — EVO-1228 · changes requested (held In Review, not merged)
Solid, net-positive guard. AC1 (runners clean) and AC2 (premium-tier routing fails with file/line/keyword) are genuinely met and verified by tracing the regex against the real runner files and synthetic violations. The CI workflow (single-account-guard.yml) correctly mirrors tenant-db-context-guard.
Holding the merge on two points:
-
AC3 not delivered (medium). A neutral
accountIdlog field (logger.log({ accountId })) currently FAILS the guard, and the token-strip exception mechanism cannot whitelist it per-line — the only sanctioned token isTenantDbContext, and addingaccountIdtoALLOWED_TOKENSwould strip it from every line and gut the guard. The per-occurrence neutral-log exemption the AC describes isn't expressible. It's vacuously fine today (no runner has such a mention), but the documented escape hatch doesn't actually work. Suggest a line-anchored allowlist (regex matching the whole specific line) for neutral occurrences. -
Dead symbol (concrete defect). The docstring (L14) and the thrown error (L108) tell devs to edit
ALLOWED_LINE_PATTERNS, which does not exist — the real construct isALLOWED_TOKENS. A dev hitting the failure will search for the wrong identifier. Please rename the references.
Lower-priority (non-blocking, not gating): getAccountById() slips through /\baccountById\b/i (camelCase boundary), and the allowed-token strip lacks word boundaries so prefix-glue (tenantDbContextRouter()) launders through.
Once AC3 + the symbol are addressed I'll re-review and merge.
…1, AC3 (EVO-1228) ALLOWED_LINE_PATTERNS: whole-line anchored regexes exempt genuinely neutral lines (log-only fields) per occurrence; anything glued onto the line breaks the anchor and the guard fires. Anchoring + statelessness enforced by a meta self-test. ALLOWED_TOKENS stays for the ADR14 seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… M2 (EVO-1228) Docstring and thrown error referenced ALLOWED_LINE_PATTERNS while only ALLOWED_TOKENS existed; now that both mechanisms are real, each is named with its actual purpose so a dev hitting the failure finds the right one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ned CI path — review minors (EVO-1228) Forbidden keywords now match as substrings (\b is camelCase-blind: getAccountById escaped it); the sanctioned-token strip gains lookaround anchors so glued identifiers (tenantDbContextRouter) stay scannable, with TenantDbContextModule sanctioned explicitly as its own entry; the CI workflow pins the exact spec path instead of a jest pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sts (EVO-1228) Adversarial self-review pass: interfaces hoisted so the ALLOWED_TOKENS JSDoc documents the const again; docstring now contrasts the two escape hatches instead of contradicting the line mechanism; line-pattern meta-test additionally bans unbounded wildcards (a .* entry would gut the guard while passing the anchor check); new meta-test forces every sanctioned token to keep the g flag and glue-proof lookarounds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re-review ready — all findings addressed in |
dpaes
left a comment
There was a problem hiding this comment.
Re-review — EVO-1228 ✅ Approved & merging
Both blockers from the prior round are genuinely resolved — verified by tracing the final single-account.spec.ts, not just the changelog — and the new single-account guard runs green on this very PR: real proof the 4 runners are clean, not just a dev claim.
Prior blockers — closed
- AC3 (was the medium blocker) —
ALLOWED_LINE_PATTERNS(cda694e): whole-line anchored regexes (^...$, nogflag), tested againsttext.trim()before the token strip. The escape hatch the AC describes is now expressible. Anti-laundering holds:logger.log({ accountId... }); routeByAccount(accountId);breaks the anchor → falls through to the scan → fails onaccountId. Covered by three behavioural tests + an anchoring meta-test. Ships empty (no runner has a neutral mention today) — correct. - Dead symbol —
ALLOWED_LINE_PATTERNSnow exists for real and the thrown error names both mechanisms with their actual purpose (cda694e/236ae63). A dev hitting the failure lands on the right identifier.
Minors — closed (f38b1b1)
- camelCase boundary: substring keywords (no
\b);getAccountById→accountById(regression test present). - prefix-glue strip: token strip gained
(?<![A-Za-z0-9_])...(?![A-Za-z0-9_])/glookarounds;tenantDbContextRouter()now trips ontenant. This surfacedTenantDbContextModule, sanctioned as its own entry — strip ordering is sound (the standalone pattern's lookahead won't eat the Module prefix). - Sourcery: CI workflow pins the exact spec path.
Nice hardening (e9aad92)
Meta-tests that guard the guard — ban unbounded wildcards in line patterns, force g + glue-proof lookarounds on every sanctioned token. The .spec.ts exclusion in collectSourceFiles is present, so the guard doesn't self-immolate on its own fixtures.
AC coverage
AC1 ✓ (CI green on the real runners) · AC2 ✓ (self-test + camelCase test) · AC3 ✓ (now delivered) · AC4 ✓ (mirror of tenant-db-context-guard).
Non-blocking (not holding merge)
account['tier']bracket notation slips past/account\./i— fine given the card scoped the keyword list as initial ("ampliar se necessário"); this is deliberate-evasion territory, not accidental reintroduction.- AC4's "blocks merge" depends on the
single-accountcheck being marked required on develop/main branch protection (the check exists and passes; making it required is repo-admin config) — worth confirming it's required liketenant-db-context.
Net-positive, no loose ends. Approving + squash-merge + delete branch.
Summary
src/runners/single-account.spec.ts: scans every runner source (campaign-packer, campaign-sender, event-receiver, event-process — and any future mode automatically) for account-routing keywords (accountId,account_id,Account.,tenant,accountById,byAccount, case-insensitive) and fails withfile:line + keyword + snippetplus remediation guidance (FR44, PRD §7).TenantDbContextseam. Sanctioned tokens are stripped from the line before scanning — never a whole-line exemption — so a real violation can't hide behind a comment mentioning the seam (anti-laundering self-test included).single-account-guard.ymlworkflow (mirror oftenant-db-context-guard) blocks merges to develop/main on violation. Path note: the spec lives undersrc/runners/(nottest/) because jest'srootDirissrc— it runs with the regularnpm testsuite; the card's dated callout records both adjustments.Security
Test plan
npm test -- single-account— 8 specs green on the current baseline (zero violations in the 4 runners)const x = account.tier;into campaign-sender fails the suite withbatch-dispatcher.service.ts:<line> — keyword "Account."; reverting restores greenChanged Files
src/runners/single-account.spec.ts.github/workflows/single-account-guard.ymlLinked Issue
Summary by Sourcery
Introduce a Jest-based guard to enforce the single-account invariant across all runner sources and wire it into CI to block merges when account-routing patterns are detected.
New Features:
CI: