Skip to content

fix(scripts): let parseVitestArgv keep every value of a repeated flag - #8002

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-7329-parse-vitest-argv-repeated-flags
Sep 6, 2026
Merged

fix(scripts): let parseVitestArgv keep every value of a repeated flag#8002
baozhoutao merged 1 commit into
mainfrom
claude/issue-7329-parse-vitest-argv-repeated-flags

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #7329

parseVitestArgv stored flags in a plain object, flags[token] = next, so a flag written twice on one command line kept only its last value. The guard's own two readers never noticed — :303 asks --changed for existence, :357 asks --root for its one effective value, and neither is legitimately repeated — but the root test:integration script is real and repeated:

package.json:25   "test:integration": "vitest run --project dom --project dom-heavy"

Fed to the exported parser, that answers --project = dom-heavy and drops dom with no symptom. Triage's boundary 1 is taken: fix the contract rather than document that it is narrow, because someone has already routed around this helper — PR #7327's pin carries its own extractor instead of reusing it, and a second router-around would be a third copy of the same extraction logic.

The shape, and why the scalar readers are untouched

flags is left exactly as it was: last-wins, one scalar per flag. A sibling map flagValues carries every occurrence of every flag in argv order.

/** @returns {{ …, flags: Record<string, string | true>, flagValues: Record<string, Array<string | true>> }} */

Three properties motivated this over the alternatives:

  • Backward compatibility is structural, not careful. No existing reader's expression changes, and no reader can be broken by forgetting to update it, because the type of flags never moved. Turning flags into arrays would have broken typeof flags['--root'] === 'string' at :357 and flags['--root'] ?? flags['-r'] in runner-package-test-entry-3746.test.ts; an array-or-scalar union would have pushed a Array.isArray() fallback into every call site — the lenient-fallback shape AGENTS.md #0.1 argues against.
  • A single-occurrence flag is present in flagValues too, as a one-element array. That is the difference between a sibling map a reader can trust and one a reader has to guard: "which projects does this run" is flagValues['--project'] ?? [], never flagValues[x] ?? [flags[x]].
  • The two views cannot drift. All three recording sites — the --flag=value form, the space-separated value form, the bare boolean — route through one record(name, value) helper that writes both. Previously they were three independent assignments.

Naming: flagValues over repeated, because the map holds every flag rather than only the repeated ones; a reader who sees repeated['--project'] come back undefined for a single --project a would reasonably add exactly the fallback this shape exists to avoid.

The pin

In scripts/__tests__/vitest-invocation-guard.test.ts, two cases in the existing parseVitestArgv describe:

  • --project dom --project dom-heavy yields ['dom', 'dom-heavy']both values, in order — and the scalar still reads dom-heavy, and neither value leaked into positionals.
  • a single --project a yields ['a']; --shard=1/4 yields ['1/4'] and a bare --watch yields [true], so the other two spellings are pinned too.

Per triage boundary 3, a non-empty-only assertion is deliberately not what is written: it passes against the unfixed parser.

Reverse verification

Two legs, because the cheap one is weak on its own.

Leg 1 — the pin against the unmodified parser (run before the fix): 2 failed | 35 passed. But it failed with TypeError: Cannot read properties of undefined (reading '--project') — that proves the field is absent, not that the pin detects the defect.

Leg 2 — value-level ablation on the committed implementation: flagValues kept, only the accumulation broken ((flagValues[name] ??= []).push(value)flagValues[name] = [value], i.e. last-wins in both views). On-disk proof, injected spelling grep -c = 1 and deleted spelling grep -c = 0, blob 27e24f857d4435. Result:

 × keeps EVERY value of a repeated flag, in order, next to the last-wins scalar
AssertionError: expected [ 'dom-heavy' ] to deeply equal [ 'dom', 'dom-heavy' ]
 Tests  1 failed | 36 passed (37)

That is the card's defect verbatim. Restored with git checkout HEAD -- <path>; blob hash back to 27e24f8, git diff HEAD empty.

Gates (all at 92b4602)

Gate Verdict line
the three importer test files Test Files 3 passed (3) / Tests 52 passed (52)
whole scripts/__tests__/ suite Test Files 107 passed (107) / Tests 3247 passed (3247)
pnpm type-check:scripts exit 0, no diagnostics
pnpm lint:root ✖ 32 problems (0 errors, 32 warnings) — 0 errors; per-file JSON says the parser contributes 0 messages and the test file's single warning (VITE_CONFIG_NAME unused) is pre-existing, at origin/main:282 and shifted to :307 by this diff's 25 added lines
pnpm check:control-bytes ✅ check-control-bytes: OK (scanned 6439 tracked text file(s); skipped 85 binary).
node scripts/check-changeset-presence.mjs ✅ No source or published contract of a released package changed in this range, so no changeset is owed.2 file(s) changed, 0 of them published source
node scripts/check-governed-queue-guard.mjs --test <the 2 paths> ✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched.

check-node-esm-load.mjs was not run: it grades discoverPackages(root).filter(p => !p.manifest.private) against MIN_PACKAGES = 30 published workspace packages and their dist. scripts/ is not a package and this diff publishes nothing, so it is out of that gate's scope.

Live E2E (informational) is red on every branch today for an upstream reason (#7990 / objectstack#16186) and is not this diff's.

Readers of parseVitestArgv, re-grepped on the branch

Still exactly three, and this confirms triage's correction to the card ("the guard is its only consumer" was wrong):

  1. scripts/vitest-invocation-guard.mjs — itself. :332 flags['--changed'] (existence), :386 flags['--root'] / flags['-r'] (value). Scalar; unaffected.
  2. scripts/__tests__/vitest-invocation-guard.test.ts — its own test, where the pin lands.
  3. scripts/__tests__/runner-package-test-entry-3746.test.ts — reads flags['--root'] ?? flags['-r'] at :133 and positionals at :193. Neither is a repeatable flag, so the defect stays latent as the card graded it, not live. Left untouched and green.

scripts/__tests__/package-scripts-vitest-projects.test.ts keeps its own extractor untouched, per triage boundary 4: it is another PR's acceptance artefact, and its independence is the current protection. Whether to collapse it onto flagValues now that this exists is that card owner's call, not this PR's.

Out of scope

Filed #8001 (finding): the same test file declares VITE_CONFIG_NAME for the "directory has no vitest.config.*, Vitest falls back to vite.config.*" case and never sweeps for it — an assertion described in a comment but never written, over the exact mechanism of objectui#3746. Different defect class, so not fixed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ


Generated by Claude Code

`parseVitestArgv` stored flags in a plain object (`flags[token] = next`), so a
flag written twice on one command line kept only its last value. The guard's own
readers never noticed — `:303` asks `--changed` for existence and `:357` asks
`--root` for its one effective value, and neither is legitimately repeated — but
the root `test:integration` script is `vitest run --project dom --project
dom-heavy`, which the parser answers as `dom-heavy` with `dom` dropped and no
symptom. The first reader to reuse the exported parser to ask "which projects
does this command run" gets a confidently wrong answer.

`flags` is left exactly as it was, last-wins, so the two scalar readers are
byte-for-byte unaffected. A sibling `flagValues` map carries every occurrence of
every flag in argv order; a flag seen once is a one-element array there, so a
reader never needs a scalar-or-array fallback. All three recording sites (the
`--flag=value` form, the space-separated value form and the bare boolean) route
through one `record()` helper, so the two views cannot drift.

The pin asserts both values come back in order for `--project dom --project
dom-heavy`, that the scalar still reads the last one, and that a single
`--project a` reads back as `['a']` — a non-empty-only assertion would pass
against the unfixed parser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ

Copy link
Copy Markdown
Contributor Author

Standing down on Live E2E (informational) — red on the base branch too, not this PR's. domain:devx @ objectui execution seat, PM session session_01MM7kaS4dPpYHV5BsMyu4tQ, R45, 2026-09-06T08:08Z. Same signature as main's scheduled run 34017174769 (job 101442890465): the published backend boots without its auth core (objectstack#16186); consumer-side anchor #7990. This diff is one scripts/ parser and its test — no backend pin, no e2e/ path. No fix to port, no re-run spent. Flip and arming wait on the remaining shards (ACCEPT 5557905058 on #7329).


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 6, 2026 08:18
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 692543f Sep 6, 2026
30 of 31 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-7329-parse-vitest-argv-repeated-flags branch September 6, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants