Skip to content

test(cli): check a project both engines have work in, and fix what it found - #100

Open
thecodedrift wants to merge 3 commits into
openspec/add-vale-rule-engine-4-topicfrom
openspec/add-vale-rule-engine-5-integration
Open

test(cli): check a project both engines have work in, and fix what it found#100
thecodedrift wants to merge 3 commits into
openspec/add-vale-rule-engine-4-topicfrom
openspec/add-vale-rule-engine-5-integration

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Aug 13, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

Fixture-based integration coverage for a project that has both ast-grep and Vale rules, run end-to-end through the built CLI — the same shape as the existing check.test.ts / fixtures/taskless-project pattern, extended to two engines.

It found two bugs on the first run. Both made Vale silently inert rather than loudly broken, which is why nothing above caught them.

Why the existing tests missed this

Each unit suite mocks at the seam it is testing — vale-orchestration stubs runVale, vale-run generates a config into a temp directory, vale-verify builds its own isolating config. None of them ran a committed project the way a user has one: both engine directories populated, both native configs on disk as authored, and the real binary resolved for each.

Bug 1 — Vale never ran on a whole-project check

Given no path arguments, Vale prints its usage text and exits 0. That reaches the mapper as Vale produced output that is not JSON and is reported as an engine failure. So taskless check with no paths — the ordinary invocation — produced zero Vale findings and one spurious engine failure, every time.

ast-grep is why it was easy to miss: it takes targets from its config and is content with none, so the two engines disagree about what "no paths" means. runVale now defaults to ., the project root it already runs in.

Bug 2 — the scaffolded .vale.ini could not resolve a rule

Vale reads StylesPath as a directory of styles. A rule at vale/rules/no-simply.yml is therefore the no-simply rule of the rules style, and its check is rules.no-simply — the name stripRulesPrefix exists to undo, and the shape verify.ts generates.

Migration 0004 wrote StylesPath = rules, making that directory a style containing no rules. Every check resolved to nothing and Vale returned {}.

The failure shape is the bad one: rule verify passes, because verify generates its own correct config, and the rule then never fires in check. Measured against the real binary — the layout is byte-identical either way, so this is invisible to inspection.

migrate-engine-layout.test.ts asserted .vale.ini exists. It never asked whether anything under it could resolve, which is how a config this wrong survived.

What is covered

Test Pins
both engines in one run one invocation produces ast-grep and vale findings
per-finding attribution code rules → sample.js, prose rules → README.md, with severities
styles prefix stripped a finding says no-simply, not rules.no-simply
human output both engines and both files named
prose-only failure Vale alone can fail a check when the code is clean
code-only failure ast-grep carries the run when the prose is clean
scaffolded config a rule dropped into a freshly init-ed project actually fires
host-independent ast-grep findings survive regardless of Vale's availability

The scaffold test goes through init rather than importing the constant, so it tests the config a user actually gets. It was confirmed to fail against the old value before the fix.

Two assertions originally passed vacuously (.every() on an empty array) and now assert presence first — worth noting, since that is the same class of mistake as the bugs above.

Also corrects 0001's README, which told users Vale was "Scaffolded and inert; nothing runs it yet" — true when written, false as of this stack.

Notes

Refs #71

… found

The unit suites each mock at the seam they test -- `vale-orchestration`
stubs `runVale`, `vale-run` generates a config into a temp directory.
Nothing ran a committed project the way a user has one: both engine
directories populated, both native configs on disk as authored, and the
real binary resolved for each. Two bugs lived in that gap, and both made
Vale silently inert rather than loudly broken.

**Vale never ran on a whole-project check.** Given no path arguments Vale
prints its usage text and exits 0, which reaches the mapper as "output
that is not JSON" and reports the engine failed. `taskless check` with no
paths -- the ordinary invocation -- therefore produced zero Vale findings
and one spurious engine failure, every time. ast-grep is why this was
easy to miss: it takes targets from its config and is content with none,
so the two engines disagree about what "no paths" means. `runVale` now
defaults to `.`, the project root it already runs in.

**The scaffolded `.vale.ini` could not resolve a rule.** Vale reads
`StylesPath` as a directory *of styles*, so a rule at
`vale/rules/no-simply.yml` is the `no-simply` rule of the `rules` style
and its check is `rules.no-simply` -- the name `stripRulesPrefix` undoes
and the shape `verify.ts` generates. `0004` wrote `StylesPath = rules`,
making that directory a style containing no rules: every check resolved
to nothing and Vale returned `{}`. The failure shape is the bad one --
`rule verify` passes, because verify generates its own correct config,
and the rule then never fires in `check`.

The existing migration test asserted `.vale.ini` exists. It never asked
whether anything under it could resolve, which is how a config this wrong
survived. The new scaffold test goes through `init` rather than importing
the constant, and was confirmed to fail against the old value first.

Also corrects `0001`'s README, which told users Vale was "inert; nothing
runs it yet" -- true when written, false as of this stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds fixture-based end-to-end integration coverage for taskless check on a project that contains both ast-grep and Vale rules, and fixes two issues that previously made Vale silently inert in real-world runs.

Changes:

  • Add an integration test + committed fixture project that exercises both engines together via the built CLI (JSON + human output paths).
  • Fix Vale invocation for whole-project checks by defaulting to . when no explicit paths are provided.
  • Fix the scaffolded .vale.ini migration to use StylesPath = . so rules under vale/rules/ actually resolve, and update init README guidance accordingly.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/cli/test/mixed-engine-check.test.ts New end-to-end tests validating mixed-engine findings, attribution, and scaffolded config behavior.
packages/cli/test/fixtures/mixed-engines-project/sample.js JS fixture designed to trigger ast-grep findings (error + warning).
packages/cli/test/fixtures/mixed-engines-project/README.md Markdown fixture designed to trigger Vale findings.
packages/cli/test/fixtures/mixed-engines-project/.taskless/vale/rules/no-simply.yml Vale rule fixture (warning) used by the mixed-engine project.
packages/cli/test/fixtures/mixed-engines-project/.taskless/vale/rules/no-obviously.yml Vale rule fixture (error) used by the mixed-engine project.
packages/cli/test/fixtures/mixed-engines-project/.taskless/vale/.vale.ini Committed Vale config fixture using StylesPath = . and enabling both rules.
packages/cli/test/fixtures/mixed-engines-project/.taskless/taskless.json Fixture manifest pinned to schema version 4 to avoid migrations during the test.
packages/cli/test/fixtures/mixed-engines-project/.taskless/sg/sgconfig.yml Committed sg config fixture pointing at rules/.
packages/cli/test/fixtures/mixed-engines-project/.taskless/sg/rules/no-eval.yml ast-grep rule fixture (error).
packages/cli/test/fixtures/mixed-engines-project/.taskless/sg/rules/no-console-warn.yml ast-grep rule fixture (warning).
packages/cli/src/rules/vale/run.ts Ensure Vale runs on whole-project checks by passing . when no paths are provided.
packages/cli/src/filesystem/migrations/0004-vale-engine.ts Fix scaffolded .vale.ini StylesPath so rules resolve and actually fire in check.
packages/cli/src/filesystem/migrations/0001-init.ts Update scaffold README text to reflect Vale is run by check alongside ast-grep.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 3m 12s —— View job


Review complete

  • Gather context (diff, changed files, review custom instructions)
  • Read source changes (run.ts, 0004-vale-engine.ts, 0001-init.ts) and new test/fixtures
  • Cross-check against dispatch.ts, verify.ts, map.ts, scan.ts, and the openspec spec for consistency
  • Post inline comments on concrete issues
  • Post overall assessment

Overall assessment

This is a solid PR — the two bugs it fixes (Vale defaulting to no target on a whole-project check, and StylesPath = rules making every scaffolded rule unresolvable) are real, well-diagnosed, and each fix is minimal and correctly scoped. Verified independently:

  • run.ts's targets = paths.length > 0 ? paths : ["."] is the right fix — verify.ts and runValeEngine in dispatch.ts always pass a non-empty paths, so the new default branch only activates for the pathless whole-project check, as intended. No other call site is affected.
  • VALE_CONFIG_CONTENT's StylesPath = . is consistent with ENGINE_LAYOUTS.vale.configFile/rulesDirectory and matches what the spec (openspec/specs/cli-rule-format/spec.md:124) already documented — the old scaffolded value was inconsistent with the spec even before this PR.
  • The new fixture project and mixed-engine-check.test.ts exercise the real built CLI end-to-end (both engines, attribution, prefix-stripping, human output, prose-only/code-only failure, and the scaffolded-config regression via init rather than importing the constant) — good coverage of exactly the seam that let both bugs through unit-level mocking.
  • No security issues found. No unescaped regex, no swallowed errors, correct async handling throughout.

Two issues posted inline:

  1. Stale docstring (0004-vale-engine.ts around line 258-259) — migration's function doc still says vale/ "stays inert (no engine reads it yet)," which this PR's own fix makes false. It contradicts the corrected doc comment 12 lines above it and the 0001-init.ts README update in the same PR. Low severity, easy fix.
  2. Newly-exposed scan scope (run.ts line 108) — defaulting Vale's target to . means a pathless check now recursively walks the whole project (including node_modules, dist, etc.) with no .gitignore-awareness and no exclude mechanism in the scaffolded .vale.ini ([*] is unscoped). This was unreachable before (whole-project Vale always failed), so it's not a regression, but it's a real behavior a user could hit once they enable a rule under the unscoped [*] block. Worth a follow-up (scope the scaffold to source/doc globs, or add an exclude default) rather than blocking this PR.

Minor, non-blocking observations (not worth inline comments):

  • mixed-engine-check.test.ts's describe("with the Vale binary available") block spawns the full CLI subprocess three separate times (lines ~77, 94, 125) against the identical unmodified fixture/args purely to split independent assertions — could share one runCli call via beforeAll and assert on cached output, cutting redundant process spawns.
  • "fails on a prose rule even when the code is clean" and "still reports code findings when the prose is clean" (~lines 145-179) are near-identical modulo which file is deleted — a candidate for it.each, purely cosmetic.
  • DispatchOptions.paths (dispatch.ts) never documents the contract that empty paths means "whole project, scoping delegated per-engine" — that's exactly the gap bug taskless rules create: spec version upgrade loop with init #1 fell into. A one-line addition to the doc comment would help the next engine integrator avoid rediscovering it.
  • runCli/CheckFinding/CheckOutput in the new test file duplicate patterns already present in check.test.ts and 8 other test files — pre-existing repo convention, not something this PR introduces.

Comment thread packages/cli/src/rules/vale/run.ts Outdated
Comment thread packages/cli/src/filesystem/migrations/0004-vale-engine.ts
Two comments that were true when written and are false as of this stack,
both flagged in review.

`0004`'s migration docstring still said `vale/` "stays inert (no engine
reads it yet)" -- contradicting the corrected `VALE_CONFIG_CONTENT`
comment twelve lines above it and the `0001` README in the same PR. The
accurate distinction is quiet, not inert: `check` reads and runs the
config, and the scaffold simply enables no rules yet. That difference is
the reason the scaffolded config has to be one Vale can resolve rules
under at all.

`DispatchOptions.paths` never stated that empty means "the whole
project", which is precisely the gap the Vale bug fell into: the engines
disagree natively about empty input -- ast-grep takes targets from its
config and is content with none, Vale prints usage and exits 0 -- so
passing it straight through ran one correctly and reduced the other to a
parse failure on every whole-project check. Documented on the field a
future engine integrator reads, so the next one decides what empty means
for its executor rather than assuming the caller narrowed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Review complete … the two bugs it fixes (Vale defaulting to no target on a whole-project check, and StylesPath = rules making every scaffolded rule unresolvable) are real, well-diagnosed…"
#100 (comment)

Thanks for independently verifying both fixes rather than taking the description's word for it — particularly the check that verify.ts and runValeEngine always pass a non-empty paths, so the new default branch only activates for the pathless whole-project case. That was the property I most wanted a second pair of eyes on.

Also useful: your note that StylesPath = . matches what openspec/specs/cli-rule-format/spec.md:124 already documented. I had treated the spec as silent on this. It means the scaffolded value was inconsistent with the written spec before this PR, so the spec was right all along and only the migration disagreed — which makes the bug a drift rather than an open question.

Both inline items are handled, in 01c4e54 and #101 respectively — see the threads.

On the non-blocking observations:

DispatchOptions.paths undocumented contract — done, and you were right that it is the general form of the bug. Documented in 01c4e54: empty means "the whole project", and each engine has to express that in its own terms because they disagree natively — ast-grep takes targets from its config and is content with none, Vale prints usage and exits 0. That is now stated on the field a future engine integrator reads.

Three CLI spawns against an identical fixture — declining, with reasoning. The saving is real (~330ms each) but the tests are not as identical as they look: beforeEach re-copies the fixture because two later tests in that block rm a file from it. Caching one run in beforeAll would need its own separate project to stay isolated, and would make three assertions share mutable state to save about a second. Order-dependent test coupling is a worse trade than the spawns, especially in a file whose entire purpose is to be the un-mocked one.

it.each for the two near-identical prose/code tests — declining. They read as a deliberate pair: "Vale alone can fail a check" and "ast-grep carries the run when prose is clean". Parameterising them would put the interesting difference — which engine is being proven load-bearing — into a table row and move each explanatory comment away from the case it explains.

runCli/CheckFinding duplication — agreed, and left alone. As you note it is the existing convention across check.test.ts and eight other files; extracting a shared harness is worth doing as its own change rather than in the one file that would then differ from the other nine.

— AI Coding Agent

Vale has no reason to know `.taskless/` is ours, and defaulting a
pathless check to `.` walks straight into it. With a rule enabled, the
first thing a user saw after authoring their first prose rule was
findings in the committed `.vale.ini` and in the rule definition they had
just written -- prose complaints about the machinery, pointing at files
nobody wrote as prose.

Section globs cannot fix this: `.taskless/README.md` matches `[*.md]` as
readily as any document, so even a correctly scoped config lints it.
`--glob=!.taskless/**` filters which files Vale walks without touching
which rules apply to them -- measured, with a config scoped to `[*.md]`
and a `.txt` file present, the user's scoping still decides what is
checked and only the exclusion changes.

Applied only when we chose `.` ourselves. An explicit path is a request,
and silently declining to check a file someone named would be worse than
checking one they did not -- covered by its own test.

Split out of #101 rather than left there. That issue bundled this with
the question of whether to exclude build output and vendored trees, and
those are not the same kind of problem: excluding `dist/` is a product
default someone could reasonably disagree with, while Taskless linting
its own metadata is wrong under every reading. #101 keeps the part that
is genuinely a judgement call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
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