Repository navigation
test(review): add hard-link witness test to save-artifact overwrite guard (#12578) - #12581
Conversation
yiliang114
left a comment
There was a problem hiding this comment.
Thanks for picking this up — the witness mechanism itself is right. I verified it locally against both mutants that matter: with isSameFile(outputPath, inputPath) reduced to outputPath === inputPath the new cases go red, and with an inode-blind isSameFile that always falls through to realpathSync they are the only red rows in the suite. So the discriminating power is there.
A few things need to align with #12578 and the sibling witnesses before this lands:
1. The gate is the pre-#12568 form (the one must-fix). #12568 merged today and converted isSameFile to stat(..., { bigint: true }), so a 64-bit NTFS file id above 2^53 now arrives exact and the guard is no longer inert there. Both sibling witnesses (findings.test.ts, repo-context.test.ts) moved to the narrow gate:
const inode = statSync(path).ino;
if (inode <= 0) {
ctx.skip();
return;
}The !Number.isSafeInteger(inode) || inode <= 0 form here over-skips on exactly the high-id NTFS volumes this series (#11848) is about — the guard works there now, so the witness should run, not skip. It also contradicts this PR's own comment, which already describes the post-#12568 bigint behavior.
2. One case, not three. #12578 scopes this to a single case beside refuses to overwrite the %s input, and the triage thread on the issue explains why: the three labels share one isSameFile call in a single loop, so a hard-link witness on findings pins dev/ino sight for all three. Could you collapse the it.each to one it on findings and tighten the assertion to /must not overwrite the findings input/?
3. Placement and cleanup. The case belongs beside the existing overwrite it.each, not the traversal-guard block — that's where someone auditing overwrite coverage will look. The rmSync(alias) calls can go: afterEach already removes the temp root recursively, and neither sibling witness cleans up its link.
On CI: the six failures all died in Install dependencies / Build cli — infrastructure, not this change. A rebase onto current main should pick up a healthy pipeline, and is needed anyway for the gate in point 1.
…ard (QwenLM#12578) Issue QwenLM#12578: of the three consumers of isSameFile named in QwenLM#11848's triage, two (findings, repo-context) keep a hard-link witness that catches the fail-open case when the shared comparator's `stat(..., { bigint: true })` post-QwenLM#12568 regresses to a string-identity check. save-artifact's overwrite guard at save-artifact.ts:649-657 had no such witness — every overwrite shape it covered was already caught by the pre-fix realpathSync.native fallback. Add a 3-row it.each mirroring the findings.test.ts / repo-context.test.ts hard-link gate (`inode <= 0 / !Number.isSafeInteger(inode) → skip`), one per consumer (findings, composed, report). The existing same-path test at line 874 covers the no-link case; this row covers the hard-link case where the output is a different path string but the same inode as one of the three inputs. Fixes QwenLM#12578. No production code changes.
…te, plain it ctx Reviewer findings on QwenLM#12581 (two rounds of CHANGES_REQUESTED): - it.each does not receive a TestContext in vitest 3.2.7, so ctx was undefined and ctx.skip() a runtime TypeError on the ino<=0 volumes the gate exists for (and a tsc error). Rewrite as a plain it(name, (ctx) => ...) like findings.test.ts:1448. - Narrow the gate to inode <= 0: QwenLM#12568 merged and isSameFile now stats with { bigint: true }, so 64-bit ids above 2^53 are exact and the guard works there — the old !Number.isSafeInteger form over-skips exactly the volumes this series (QwenLM#11848) targets. - Collapse the three it.each rows to one case on the findings input: the three labels share one isSameFile call in a single loop, so one witness pins all three (per QwenLM#12578's thread). - Move the case beside the existing overwrite it.each instead of the traversal-guard block; drop manual rmSync (afterEach removes root). - Rebase onto current main (picks up QwenLM#12568 and a healthy pipeline).
cf14710 to
d92c3fd
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — the revision addresses all three points from the previous round: a single hard-link witness on the findings input placed beside the overwrite it.each, the narrow inode <= 0 gate matching the post-#12568 sibling witnesses, and the cleanup boilerplate is gone. Full matrix green on d92c3fd (ubuntu Test, Lint & Static, no-AK Integration, TUI parity, OpenTUI no-flicker, web-shell E2E all pass). Thanks for the quick turnaround.
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Approving at d92c3fdf. Independent second pass on a test-only diff.
The part worth confirming was whether the discriminating power survives collapsing three cases into one, so I checked that by reading rather than rerunning the mutants. A string-identity isSameFile lets alias !== paths.findings through, the write lands, and both the toThrow(/must not overwrite the findings input/) and the unchanged-content assertion go red. An inode-blind one that falls through to realpathSync also goes red, because realpathSync resolves symlinks but not hard links, so the two names stay distinct path strings. Both mutants from the earlier round are still killed by the single case.
Placement and gate match the sibling witnesses: the case sits directly after the it.each(['findings', 'composed', 'report']) overwrite block, which is where someone auditing overwrite coverage looks, and the inode <= 0 skip is the post-#12568 narrow form rather than the !Number.isSafeInteger(inode) one that would over-skip on exactly the high-id NTFS volumes this series is about.
18 checks pass, 8 skipped, none failing at this head; no unresolved threads.
What this PR does
Adds one hard-link witness test to
packages/cli/src/commands/review/save-artifact.test.ts, placed beside the existingrefuses to overwrite the %s inputit.each: an output path hard-linked to thefindingsinput must makesaveReviewArtifactthrowmust not overwrite the findings input.Why it's needed
Issue #12578 notes that of the three consumers of the shared
isSameFilecomparator named in #11848's triage,findingsandrepo-contexteach keep a hard-link witness that goes red if the comparator ever loses inode sight, whilesave-artifacthad none — every overwrite shape it covered (same path, symlink traversal, case-insensitive alias) is also caught by a string-compare fallback, so a regression to that shape would ship green.Since #12568 landed,
isSameFilestats with{ bigint: true }, so a 64-bit NTFS file id above 2^53 arrives exact and the guard works there; the new test uses the same narrowinode <= 0gate as the two sibling witnesses (only an unusable inode skips).Reviewer Test Plan
How to verify
npx vitest run packages/cli/src/commands/review/save-artifact.test.ts— 69/69 pass, includingrefuses to overwrite the findings input when the output is hardlinked to it (#12578).isSameFile(outputPath, inputPath)to a plain===and the new case goes red; make the comparator inode-blind (alwaysrealpathSync) and it is the only red row in this suite.findings.test.tsandrepo-context.test.ts:statSync(path).ino; if (inode <= 0) ctx.skip()inside a plainit(name, (ctx) => …).Evidence (Before & After)
Before: no hard-link case in this suite; the same-path
it.eachat line ~832 pins the identity guard but not the dev/ino comparison under a second name.After: a hard-linked output is refused with
must not overwrite the findings input, and the input file's bytes are unchanged.Tested on
save-artifact.test.tspass;tsc --noEmitclean for this file (baseline errors in unrelatedacp-integration/configfiles unchanged);prettier --checkandeslintclean.Risk & Scope
itcase; no production code touched.isSameFilecall shape in a single loop, so the findings witness pins the comparison for all three — one case is sufficient (as scoped in save-artifact's isSameFile overwrite guard has no hard-link witness (follow-up from #11848) #12578's thread).it.eachso a reader auditing overwrite coverage sees both the same-path and hard-link shapes together.Linked Issues
isSameFileand the deletion-journal swap check fail open on Windows #11848 (the root fail-open) and fix(cli): keep 64-bit file ids exact in the two identity comparators #12568 (the comparator bigint fix, merged today).