Repository navigation
Add an ast-grep vendor-contract test #108
Description
What a vendor-contract test is here
PR #93 added packages/cli/test/vale-vendor-contract.test.ts. It invokes the vendored Vale binary directly — deliberately not through runVale — and pins the givens: the exit-code semantics, the JSON shape and field names, the Span base, the config-error channel, matcher precedence. Every case names the code that rests on it, so a Vale upgrade that changes a default fails there with an instruction ("go change normalizeSeverity") instead of surfacing downstream as a mysterious mapping bug.
That file is currently the only vendor-contract test in the stack. There is no ast-grep equivalent, even though ast-grep is the executor for every sg rule and the CLI's logic is built on just as many observed-but-undocumented behaviors.
Why it matters
@ast-grep/cli and all seven platform packages are exact-pinned at 0.41.0 in packages/cli/package.json. An exact pin is the right call, but it means the only moment these assumptions can change is a deliberate bump — and at that moment nothing tells us which ones moved. The pin makes the bump the single checkpoint; a contract test is what makes the checkpoint say something.
The failure mode that hides is worse than for Vale: an empty or mis-scoped ast-grep scan reports success. runAstGrepScan in packages/cli/src/rules/scan.ts treats exit 0 and exit 1 as normal, parses whatever JSON lines arrive, and silently discards any stdout line that does not parse. A scan that matched nothing because discovery changed is indistinguishable from a clean codebase. Vale at least fails loudly on a bad config; here, a broken assumption reads as green.
Behaviors worth pinning
Confirmed against the source in this repo:
-
--json=streamline protocol and match shape.scan.tsparses one JSON object per line and maps it viaAstGrepMatchinpackages/cli/src/types/check.ts—ruleId,severity,message,note?,text,file,range.start/end.{line,column},replacement?. A rename arrives asundefinedin aCheckResult, not as an error. Worth pinning that the stream is newline-delimited JSON with no interleaved status lines, sincescan.tsswallows unparseable lines by design. -
rangeline/column are 0-based.packages/cli/src/util/format.tsrendersrange.start.line + 1/column + 1, andpackages/cli/src/rules/runtime/narrow.tsdoes the same. If ast-grep ever emitted 1-based positions, every reported location would silently be off by one — no error, just wrong. -
severityvocabulary.AstGrepMatch.severityis typed as exactlyerror | warning | info | hint. Nothing verifies ast-grep cannot emit a fifth value. -
Scan exit codes.
scan.tsencodes "exit 1 means error-severity matches were found; exit > 1 means the binary or config failed." That is the entire boundary between "findings" and "engine failure" and it is asserted nowhere against the binary. -
sg testoutput format and exit code.runTestsinpackages/cli/src/rules/verify.tsscrapes counts out of the human-readable linetest result: ok. 3 passed; 0 failed;with/(\d+)\s+passed/iand/(\d+)\s+failed/i, and keys success off exit 0. This is prose parsing against an unpinned format — if the wording changes, verify reports0 passedon a passing run. Also worth pinning:--skip-snapshot-testsand that--filtertakes an anchored regex (^id$). -
--versionidentifies itself as ast-grep.AST_GREP_BINARY.identityis/ast-grep/i(scan.ts). If the string changes,findSgBinaryrejects the real binary as a placeholder and throws "ast-grep binary not found" — the resolver exists precisely because@ast-grep/cli's postinstall can leave a placeholder text file at the binary path. -
ruleDirsrecursion vs..tests/— already pinned, worth relocating.ruleDirsrecurses and parses every.ymlbeneath it as a rule, which is why fixtures live in a dot-prefixed.tests/: measured against 0.41.0,tests/and__tests__/both hard-fail the whole scan withFail to parse yaml as RuleConfig: missing field 'language', while a dot-directory is skipped by discovery andsg teststill reads it viatestConfigs[].testDir. Documented atpackages/cli/src/rules/engines.ts(RULE_TESTS_DIRECTORY) andpackages/cli/src/rules/assemble.ts(assembleSgConfig), and pinned today bypackages/cli/test/engine-layout.test.ts. That test is the existing proof this class of check pays for itself; the contract file is where it belongs alongside the rest.
Noted but not currently depended on: files / ignores scoping inside a rule. packages/cli/src/rules/engines.ts cites it as the reason sg needs no per-rule config file, but no code path exercises it yet. Pin it if and when rule scope ships.
Suggested shape
packages/cli/test/ast-grep-vendor-contract.test.ts, mirroring the Vale file: resolve the binary via findSgBinary(), skip the suite if absent, build throwaway projects in mkdtemp, invoke the binary directly rather than through runAstGrepScan, and comment each case with the code it protects.
Refs #93
Activity
fix(cli): use the builtin ANSI stripper, and pin the echo indent
fix(cli): anchor sg test count parsing, and pin ast-grep's contract
fix(cli): use the builtin ANSI stripper, and pin the echo indent
fix(cli): anchor sg test count parsing, and pin ast-grep's contract
fix(cli): use the builtin ANSI stripper, and pin the echo indent
fix(cli): use the builtin ANSI stripper, and pin the echo indent