fix(cli): anchor sg test count parsing, and pin ast-grep's contract - #117
fix(cli): anchor sg test count parsing, and pin ast-grep's contract#117thecodedrift wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes incorrect pass/fail count reporting for ast-grep test runs by anchoring parsing to the actual summary line (after ANSI stripping) and improving UTF-8 decoding robustness, while adding a vendor-contract test suite to pin ast-grep’s observed CLI behaviors so future version bumps fail loudly at the boundary.
Changes:
- Anchor
sg testpass/fail parsing to the final summary line (stdout/stderr), stripping ANSI, and decode streams withStringDecoderto avoid UTF-8 chunk-boundary corruption. - Add
ast-grepvendor-contract tests covering--json=streamshape, severities, exit codes,sg testoutput/streams, filter semantics, and layout discovery behaviors. - Remove the now-redundant engine layout test file and update the reference to the new contract test location; add a changeset for the patch.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/rules/verify.ts | Fixes sg test count extraction by parsing an anchored summary line and decoding output robustly. |
| packages/cli/test/verify.test.ts | Adds a regression test ensuring echoed fixture text can’t poison parsed pass/fail counts. |
| packages/cli/test/ast-grep-vendor-contract.test.ts | Introduces a vendor-contract suite to pin ast-grep CLI/output/exit-code semantics and layout assumptions. |
| packages/cli/test/engine-layout.test.ts | Deletes the old layout-only test now covered by the vendor-contract suite. |
| packages/cli/src/rules/engines.ts | Updates documentation reference to the new contract test file. |
| .changeset/anchor-sg-test-summary.md | Adds release notes for the verify count parsing fix and decoding improvement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@claude /review |
|
Claude finished @thecodedrift's task in 4m 6s —— View job Review complete
Overall assessmentThis is a solid, well-reasoned fix. I traced the actual bug — the old code ran two unanchored, case-insensitive regexes ( The The vendor-contract suite is thorough and does real work: I checked several of its 18 assertions against the actual code paths they claim to guard (severity vocabulary vs. Findings posted inline (both minor, not blocking):
Also worth noting (already caught by Copilot's review, and I agree with it): No correctness issues found in the actual bug fix — the regex anchoring, ANSI stripping, and last-match-wins logic all check out against the described failure mode and the contract test's pinned output shapes. The |
thecodedrift
left a comment
There was a problem hiding this comment.
Just building on the statement from Claude, we should use builtins when possible within the CLI, as they're a lot more consistent and we maintain less code
Addresses PR #117 review: - Replace the hand-rolled `stripAnsi` (plus its `no-control-regex` suppression) with `node:util`'s `stripVTControlCharacters`. - `project()` in the vendor-contract suite now mkdirs the parent of the path actually given rather than a hardcoded `src/`. - Pin that `sg test` indents every echoed fixture line by two spaces, which is what makes the `^` anchor close the poisoning class rather than only the one fixture from #112, and say so in the doc comment. Refs #108 Refs #112
Both inline findings are addressed in 33b0806, along with the — AI Coding Agent |
Done in 33b0806 — the hand-rolled — AI Coding Agent |
|
@claude /review |
|
Claude finished @thecodedrift's task in 2m 40s —— View job Review complete
Overall assessmentThis is a re-review after 33b0806, which addresses everything raised in my previous pass. I re-verified all three fixes against the actual diff rather than taking the commit message at its word:
I also re-traced the core fix end-to-end against the diff (not just the description):
No new correctness, security, or test-adequacy issues found in this round. The regex, anchoring, ANSI stripping, and stream-join logic all hold up against the described failure mode and the now-pinned vendor properties. The Nothing further to flag; no inline comments needed this round. |
`runTests` scraped `ast-grep test`'s pass/fail counts with two unanchored regexes over stdout+stderr combined, taking the first match anywhere. A failing run echoes the offending fixture's source, so a fixture containing `'7 passed; 0 failed'` was read as the summary and verify reported `7 passed, 0 failed` for a run whose summary said `0 passed; 1 failed`. This corrupts reported counts only — it never turns a failure into a pass. Validity comes from the exit code, and a passing run echoes no fixture source, so the sole match on a clean run is the genuine summary. The counts still matter: `improve-rule` feeds them back to an agent iterating on a rule. The counts now come from the summary line itself, in one of the two exact forms ast-grep emits, with ANSI stripped first (the escape sits inside the phrase) and the last such line winning. Both streams are read through a `StringDecoder`, as `vale/run.ts` already does, so a multi-byte character split across a chunk boundary survives. `ast-grep test` has no structured output at 0.41.0, so the parse cannot be deleted. What makes it defensible is the new `packages/cli/test/ast-grep-vendor-contract.test.ts`, mirroring the Vale contract file: it invokes the pinned binary directly and pins the summary wording and exit codes (0 on pass, 4 on failure), the colorization, the fixture echo, `--skip-snapshot-tests`, `--filter`'s regex semantics, the `--json=stream` line protocol and match shape, 0-based ranges, the severity vocabulary (five values, not four), and the scan exit-code boundary. The two `.tests/`-discovery cases move here from `engine-layout.test.ts`, which existed only for them and is removed. Fixes #108 Fixes #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
Addresses PR #117 review: - Replace the hand-rolled `stripAnsi` (plus its `no-control-regex` suppression) with `node:util`'s `stripVTControlCharacters`. - `project()` in the vendor-contract suite now mkdirs the parent of the path actually given rather than a hardcoded `src/`. - Pin that `sg test` indents every echoed fixture line by two spaces, which is what makes the `^` anchor close the poisoning class rather than only the one fixture from #112, and say so in the doc comment. Refs #108 Refs #112
33b0806 to
7d8238f
Compare
Two coupled issues in one PR.
runTestsrecovers pass/fail counts by regex-scanningast-grep test's human-readable output, and nothing pins that output's shape — so the fix and the guard belong together. Pin without fix leaves the bug; fix without pin leaves the parser unguarded against a version bump.The bug (#112)
Two unanchored regexes ran against stdout and stderr concatenated, taking the first match anywhere. A failing run echoes the offending fixture's source, so fixture text could be read as the summary:
reported
passed = 7, failed = 0where the truth was0 passed; 1 failed.It corrupted counts; it did not cause a false pass. Validity comes from the exit code, and a passing run never echoes fixture source, so the only match on a clean run is the genuine summary. A rule could not be made to verify green this way. What it produced was a failure reported with nonsense numbers — output the
improve-ruleloop feeds back to an agent iterating on a rule.Fixed by anchoring to the summary line after stripping ANSI, last match wins, plus a
StringDecoderper stream (the same UTF-8 chunk-boundary defect already fixed for Vale invale/run.ts). The regression test fails against unpatched source withexpected 7 to be +0.The contract (#108)
New
packages/cli/test/ast-grep-vendor-contract.test.ts— 18 cases, binary invoked directly viafindSgBinary()rather than throughrunAstGrepScan, mirroringvale-vendor-contract.test.ts.@ast-grep/cliis exact-pinned at 0.41.0, so a deliberate bump is the only moment these assumptions can change; this is what makes that moment say something.Two behaviors turned out different from what the issue claimed:
hint,info,warning,error, andoff.offis accepted in a rule but disables it, so it can never reach the stream, which is asserted.AstGrepMatch's four-value union is therefore still correct and was left unchanged, with the reasoning recorded.sg testsplits across streams — the passing summary is on stdout, the failing one on stderr. The issue did not say which, and it matters: the parser joins the streams with a newline so the anchor holds across the concatenation.Also newly pinned rather than assumed: exact engine-failure exit codes (6 missing config, 8 unparseable rule), that
--skip-snapshot-testsis load-bearing (without it a valid invalid-case fails "No baseline found"), and that--filteris an unanchored regex —no-evalruns two rules where^no-eval$runs one, which is why the caller anchors it.engine-layout.test.tsis deleted: it held only the.tests/discovery cases, which move into the contract file where they belong alongside the rest. The stale reference insrc/rules/engines.tsnow points at the new location.Verification
pnpm lintclean ·pnpm typecheckclean ·pnpm test619 passing across 50 files ·openspec validate --all --strict23/0The contract suite ran rather than skipping, and it is cheap: 18 tests in ~120 ms (0.7 s wall), despite spawning the real binary.
Found, not fixed
--include-offis atest-only flag;scanrejects it, so off-severity rules are unreachable fromrunAstGrepScan. Nothing depends on it.runAstGrepScanstill useschunk.toString()for stderr — the same UTF-8 boundary defect, but there it only feeds an error message, and it sits outside both issues.Fixes #108
Fixes #112