Skip to content

test(runners): single-account invariant guard (EVO-1228)#51

Merged
dpaes merged 7 commits into
developfrom
feat/EVO-1228
Jun 11, 2026
Merged

test(runners): single-account invariant guard (EVO-1228)#51
dpaes merged 7 commits into
developfrom
feat/EVO-1228

Conversation

@nickoliveira23

@nickoliveira23 nickoliveira23 commented Jun 10, 2026

Copy link
Copy Markdown

Summary

  • New 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 with file:line + keyword + snippet plus remediation guidance (FR44, PRD §7).
  • Exceptions are literals in the spec (audit trail): only the ADR14 TenantDbContext seam. 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).
  • New single-account-guard.yml workflow (mirror of tenant-db-context-guard) blocks merges to develop/main on violation. Path note: the spec lives under src/runners/ (not test/) because jest's rootDir is src — it runs with the regular npm test suite; the card's dated callout records both adjustments.

Security

  • No runtime surface — test + CI only.

Test plan

  • npm test -- single-account — 8 specs green on the current baseline (zero violations in the 4 runners)
  • Live AC2 proof: injecting const x = account.tier; into campaign-sender fails the suite with batch-dispatcher.service.ts:<line> — keyword "Account."; reverting restores green
  • This PR itself executes the new guard workflow (pull_request workflows run from the head branch)

Changed Files

  • src/runners/single-account.spec.ts
  • .github/workflows/single-account-guard.yml

Linked Issue

  • EVO-1228

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:

  • Add a single-account invariant spec that scans runner TypeScript sources for account-routing keywords and fails when violations are found.

CI:

  • Add a GitHub Actions workflow that runs the single-account guard test on pushes and pull requests to main and develop, blocking merges on violations.

nickoliveira23 and others added 3 commits June 10, 2026 16:10
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>
@sourcery-ai

sourcery-ai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds 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

Change Details Files
Introduce Jest spec that enforces single-account invariant across all runner sources by scanning for forbidden account-routing patterns with a minimal, documented allowlist.
  • Add configuration for runner directories and modes to be scanned recursively for .ts (non-spec) files under src/runners
  • Define a set of forbidden account-related patterns (accountId, account_id, Account., tenant, accountById, byAccount) and a small allowlist of sanctioned tokens stripped from lines before scanning
  • Implement file collection, content scanning, and violation reporting utilities that produce file:line:keyword:text diagnostics and throw on violations
  • Add Jest tests that: enforce per-runner directories are present and free of routing, cover the entire src/runners tree (including future modes), self-test that violations are detected, and verify the allowlist behavior (including anti-laundering on the same line)
src/runners/single-account.spec.ts
Add CI workflow to run the single-account guard on pushes and pull requests to main/develop, blocking merges on violations.
  • Create GitHub Actions workflow that runs on push and pull_request to main and develop branches
  • Configure Node 20 environment with npm ci installation and invoke Jest in-band for the single-account spec only
  • Document the workflow purpose and relation to FR44/PRD §7 in comments
.github/workflows/single-account-guard.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread .github/workflows/single-account-guard.yml Outdated

@dpaes dpaes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. AC3 not delivered (medium). A neutral accountId log field (logger.log({ accountId })) currently FAILS the guard, and the token-strip exception mechanism cannot whitelist it per-line — the only sanctioned token is TenantDbContext, and adding accountId to ALLOWED_TOKENS would 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.

  2. 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 is ALLOWED_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.

nickoliveira23 and others added 4 commits June 11, 2026 11:58
…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>
@nickoliveira23

Copy link
Copy Markdown
Author

Re-review ready — all findings addressed in cda694e..e9aad92 (AC3 line-anchored allowlist + dead-symbol fix + the non-blocking minors + self-review hardening). Details in the Linear comment: https://linear.app/evoai/issue/EVO-1228

@dpaes dpaes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. AC3 (was the medium blocker)ALLOWED_LINE_PATTERNS (cda694e): whole-line anchored regexes (^...$, no g flag), tested against text.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 on accountId. Covered by three behavioural tests + an anchoring meta-test. Ships empty (no runner has a neutral mention today) — correct.
  2. Dead symbolALLOWED_LINE_PATTERNS now 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); getAccountByIdaccountById (regression test present).
  • prefix-glue strip: token strip gained (?<![A-Za-z0-9_])...(?![A-Za-z0-9_])/g lookarounds; tenantDbContextRouter() now trips on tenant. This surfaced TenantDbContextModule, 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-account check 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 like tenant-db-context.

Net-positive, no loose ends. Approving + squash-merge + delete branch.

@dpaes dpaes merged commit c4afb4e into develop Jun 11, 2026
3 checks passed
@dpaes dpaes deleted the feat/EVO-1228 branch June 11, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants