fix(cli): handle Windows filesystem identity semantics - #10440
Conversation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Re-run requested by @yiliang114 — gate re-checked against the current head; nothing changed its outcome. Template ✓ — all required sections present, bilingual. Problem: observed, not theoretical. Scheduled run 33214395104 failed 30 tests exclusively on Direction: restoring the scheduled Windows unit-test gate is core CI hygiene — no product-surface question to debate. Size: 799 changed lines = 275 production + 524 test (no generated/schema). Production lines in core paths: 0 — the only Approach: focused — each failure class is fixed at its root rather than patched per-test: identity checks learn strict inode verifiability with fail-closed parity, the absence probe learns the Windows Risk: the diff touches Moving on to code review. 🔍 中文说明由 @yiliang114 触发的 re-run——已对当前 head 重新过门禁,结论不变。 模板 ✓——必填章节齐全,双语完整。 问题:已观测到,而非理论性问题。定时任务 33214395104 仅在 方向:恢复定时 Windows 单测门禁属于核心 CI 卫生,没有产品方向上的争议。 规模:共 799 行改动 = 275 行生产代码 + 524 行测试(无生成/schema 文件)。核心路径的生产行数为 0——唯一触及的 方案:聚焦——每类失败都在根上修复,而不是逐测试打补丁:身份校验引入严格 inode 可验证性并带 fail-closed 一致性,缺失探测学会处理 Windows 的 风险:diff 触及 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewRead the diff against my own baseline for the stated problem (strict inode predicate + uniform normalization, a fail-closed absence probe that distinguishes the regular-file-ancestor shape, platform-native fixtures). The PR matches that shape and improves on it in one good direction: verifiability itself becomes part of the identity comparison — a directory recorded as verifiable that stops reporting a verifiable inode (and vice versa) now fails closed instead of silently degrading to device-only comparison. That is strictly safer than what was there before. Verified against the base tree, not taken from the PR description:
No blockers. One non-blocking observation: core's Files changed (15)
Testing — this PR's own CI, read via API (unattended run; PR code never executed locally)CI on Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The one claim no pre-merge lane can settle: the real Windows lane itself. True NTFS file IDs past the safe-integer range and the 中文说明代码审查:将 diff 与我独立提出的方案(严格 inode 谓词 + 统一归一化、可区分"普通文件祖先"形态的 fail-closed 缺失探测、跨平台 fixture)对照,本 PR 与之一致,且在一点上做得更好:可验证性本身成为身份比较的一部分——记录为可验证的目录若不再报告可验证 inode(反之亦然),现在会 fail closed,而不是悄悄退化为仅比较 device。这比原实现严格更安全。 以下均对照基线代码树验证,而非照抄 PR 描述: 无阻塞问题。一条非阻塞观察:core 的 测试:无人值守运行,PR 代码未在本地执行。 合并前无法由任何沙箱通道验证的只剩真实 Windows 任务本身:真实 NTFS 超大文件 ID 与 ENOENT/ENOTDIR 差异只能在合入后的下一次定时/合并队列运行中检验—— — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean, focused fix verified against the base tree; the one reservation is structural, not a defect in the PR: the lane it repairs cannot run pre-merge by workflow design. Reflection:
Approving now, pinned to — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.3)
Config resolves targetDir in the constructor but stores cwd verbatim,
so getWorkingDir() returns it unresolved. The rewritten assertions
compared against path.resolve('/tmp'), which holds on POSIX but is
drive-qualified on Windows (C:\tmp), re-breaking both rebinds tests on
the windows-latest lane this PR restores. Restore toBe('/tmp') and
comment the getter-contract asymmetry.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
expect.any(String) also matches an empty or unrelated error, so the closed-generation isolation path could silently stop being exercised. Pin WorkspaceGenerationClosedError's exact message instead. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
isPathProvablyAbsent's ancestor walk used lstatSync(...).isDirectory(), which is false for a symlink resolving to a directory, so a genuinely absent path under a symlinked intermediate was reported "not provably absent" and forced a full re-review every local round. Probe the ancestor with statSync so resolved directories are traversable; a symlink to a file still refuses, and a broken link throws ENOENT and the walk continues. Document the fail-closed contract on the helper and add real-fs tests for the directory, file-component, and symlink shapes. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…d lstatSync On POSIX a regular-file intermediate raises ENOTDIR at the leaf, so the ancestor walk added for Windows never executes there: a future simplification back to "leaf ENOENT ⇒ absent" would pass every real-fs test while restoring the R19-3 misclassification. Add platform- independent arms that spy lstatSync into the Windows shape — ENOENT leaf with a regular-file ancestor stays unmeasurable (false), ENOENT leaf under a directory ancestor is genuine absence (true), and a non-ENOENT leaf error is never absence. Restore the fail-closed pointers the deleted R19-2/R19-3 call-site comments carried. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ve values same-file.ts used core's canonical hasVerifiableInode (Number(ino) !== 0), but Windows surfaces 64-bit NTFS file indices rounded at the JS boundary: two distinct files whose indices land in one double-rounding bucket compare equal, so isSameFile equated them and the anti-clobber guards findings/save-artifact/repo-context spuriously refused non-colliding paths. Restate Number.isSafeInteger(ino) && ino > 0 locally instead of tightening core, whose looser predicate also gates assertVerifiableTranscriptIdentity on bigint transcript inodes. Extend the volume-pose harness with a rounded-inode shape: distinct files stay distinct, one file under two spellings stays one. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…y checks The safe-positive inode semantics this PR establishes (verifiable predicate, normalize to 0, verifiability-parity compare) were pasted into standalone-deletion-journal.ts and acpAgent.ts while both already import conversation-directory-identity.js — a lockstep edit the next tightening would have to repeat in three places, with one missed copy enough to let one identity check accept what another rejects. Export hasVerifiableInode and normalizedInode from the cli-local module and import them at both call sites; no core barrel import, so the serve bundle closure is unchanged. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…dentity managedConversationExpectation builds expectations from real fs stats, always safe-positive on CI platforms, so the new degradation branch in assertManagedConversationDirectoryIdentity never executed — a mutant that unconditionally claims inode verifiability survives every test yet reintroduces the 'standalone working directory identity is compromised' rejection on Windows volumes with file IDs beyond MAX_SAFE_INTEGER. Mirror the conversation-directory-identity simulation: spy fs.lstat to report ino 0 and Number.MAX_SAFE_INTEGER + 1, and assert sessionCd resolves with the inode-0 expectation instead of throwing. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
node:fs/promises re-exports no Stats member, so the degradation test's cast must use the node:fs type directly to keep tsc --noEmit green. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
isPathProvablyAbsent walked the same missing ancestor chain to the repo root once per path. In a sparse-checkout repo every out-of-cone tracked path is absent with unmaterialized ancestors, so the walk multiplied the metadata calls by depth+1 on each invisibleTrackedPaths enumeration — the slowest call on Windows, the platform this series restores (R1-6). Thread an optional per-enumeration memo of actually-probed ancestor verdicts through the walk and seed one in the enumeration; the leaf lstat stays per-path, ENOENT-only fail-closed semantics unchanged. Two spied-statSync tests pin the one-probe-per-ancestor count and the memoized non-ENOENT refusal. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.3)
…ration `vanishedStillOnDisk` called `isPathProvablyAbsent` without the per-enumeration `ancestorProbes` memo this PR introduces, while the sibling enumeration `invisibleTrackedPaths` passes one. A subtree dropped between rounds (a bulk deletion committed, a branch switch) sends every vanished path down the same missing ancestor chain: K×(d+1) synchronous statSync calls where K+O(chain) would suffice — the multiplied walk this diff introduced at that call site. Pass a fresh per-invocation memo, mirroring `invisibleTrackedPaths`, and pin it with an end-to-end capture test counting the ancestor probes (red without the memo argument). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
… (R1-8)
The module comment claimed the verifiability predicate was the shared
semantics for EVERY conversation-identity check ("import it, do not
restate it"), while four restatements stood in the tree. Consolidate
what can be consolidated without moving a semantic boundary:
- export `isSameDirectoryIdentity` and replace acpAgent.ts's textually
identical `isSameManagedDirectoryIdentity` with it (three call
sites);
- same-file.ts now imports `hasVerifiableInode` from the identity
module instead of keeping a fourth verbatim copy;
- narrow the module comment to the consumers that actually import it
and document the two deliberate local restatements that remain:
`syncStandaloneRoot`'s inline composite around the open handle, and
`hasExpectedManagedDirectoryIdentity`, whose expectation side must
keep deriving verifiability from `inode !== 0` because the wire
payload carries no `inodeVerifiable` field.
Core's looser canonical predicate stays untouched.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Serve A/B failure on 1a10e72 classified NOT PR-caused (infra cancellation):
Leaving the rerun to settle; no code action. |
|
CI attribution —
(The Triggering a failed-jobs rerun; leaving the infra attribution to maintainers. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally (the lane this PR restores runs only on schedule/merge_group/workflow_dispatch — unrunnable pre-merge by workflow design; the review harness's test phase also did not complete on this machine: forced clean rebuilds exceed the per-call budget).
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/routes/session-pr-backfill.test.ts:2849 — [review] fs-error isolation path unpinned after the ENOTDIR fixture swappackages/cli/src/acp-integration/acpAgent.ts:3217 — [review] mixed-verifiability comparator arm untested
中文说明
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally (the lane this PR restores runs only on schedule/merge_group/workflow_dispatch — unrunnable pre-merge by workflow design; the review harness's test phase also did not complete on this machine: forced clean rebuilds exceed the per-call budget)。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 2940 passed · 0 failed · 2940 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2940 通过 · 0 失败 · 2940 总计 抖动门: Verification reportPR 10440 verification —
|
| cell | head | base | flip |
|---|---|---|---|
| C1 record under ino=2^60 | {inode: 0, inodeVerifiable: false}; recorded value admitted by the wire boundary (isManagedConversationIdentityNumber, acpAgent.ts:3245, identical both arms) |
{inode: 1152921504606847000, inodeVerifiable: true}; wire boundary rejects it |
yes |
| C2 revalidate on a consistently-unsafe volume | resolves | resolves | no |
| C3 transition unverifiable→verifiable (ino 0 → safe) | throws identity_changed |
resolves (dev-only compare: fail-open) | yes |
| C4 transition verifiable→unverifiable | throws identity_changed |
throws identity_changed |
no |
| C5 device mismatch | throws | throws | no |
| C6 verifiable inode change | throws | throws | no |
Witnesses: 01-ab-h1-identity-head.png, 02-ab-h1-identity-base.png. C1 is the incident path — base records an identity the protocol boundary cannot carry; C3 is the security-relevant direction: base accepts an identity transition indistinguishable from a same-path swap.
H2 — absence probe (isPathProvablyAbsent / invisibleTrackedPaths) in a real sparse-checkout repo
The container's git 2.39.5 lacks sparse-checkout check-rules, so the harness interposes a PATH-shim git whose check-rules is a faithful oracle for this fixture (echoes only in-cone paths; all flagged paths are out-of-cone). Without the shim, invisibleTrackedPaths takes its documented fail-closed catch (returns all tagged paths) — observed during harness construction on both arms.
| cell | head | base | flip |
|---|---|---|---|
C1 f/x.ts, ENOENT leaf below a regular-file ancestor (Windows shape via spy) |
stays flagged (unmeasurable) | exempted — certifies bytes git diff is blind to (R19-2) |
yes |
C2 gone/y.ts, genuine absence under an existing dir |
exempted | exempted | no |
C3 keep/z.ts, present tagged path |
stays flagged | stays flagged | no |
| C4 statSync ancestor probes over 6 flagged paths sharing a missing chain | 5 (memoized) | 0 (base never walks) | informational |
| C5 direct shapes (head-only export): existing / ENOENT-under-dir / ENOTDIR-under-file / symlink-to-dir / broken symlink / spied Windows shape | false / true / false / true / true / false | not exported at base | — |
Witnesses: 03-ab-h2-absence-head.png, 04-ab-h2-absence-base.png. C4 without the memo is 10 probes (3 paths × 3 + 1) — pinned by the PR's own probe-count test (see M4).
H3 — isSameFile under rounded-inode collision (statSync spied to ino = 2^60)
| cell | head | base | flip |
|---|---|---|---|
| C1 two distinct files, same rounded ino | false (canonical-spelling fallback) |
true — distinct files equated; anti-clobber guards mis-refuse |
yes |
| C2 one file via symlinked-directory spelling, unsafe ino | true (no over-refusal) |
true |
no |
| C3 hard links, safe inodes (volume off) | true |
true |
no |
| C4 distinct files, ino 0 | false |
false |
no |
| C5 predicate surface | strict: 0/2^60/MAX+1 → false; 1/MAX → true | loose (core Number(ino) !== 0): 2^60/MAX+1 → true — the root cause |
yes |
| C6 hard links under unsafe ino | false (documented hard-link loss on unverifiable volumes) |
true |
semantic |
Witnesses: 05-ab-h3-samefile-head.png, 06-ab-h3-samefile-base.png.
Gates (all green, both arms)
| suite | head | base |
|---|---|---|
| packages/cli — the 7 changed test files | 741 passed | 725 passed (+16 = the PR's new cases) |
| packages/acp-bridge — sessionAttachments.test.ts | 53 passed | 53 passed |
| packages/core — config.test.ts | 579 passed | 579 passed |
Base green on Linux confirms the 30 original failures are platform-specific and the head suites do not regress Linux behavior.
Corrections
None required — the description's mechanism claims matched measurement. Verified the commit-2 claim about the getter-contract asymmetry: config.ts:2552 resolves targetDir, config.ts:2683 stores cwd verbatim, getWorkingDir() (config.ts:7804) returns it unresolved — the PR's toBe('/tmp') assertions are correct as written.
Findings
No blocking findings. Non-blocking observations:
invisibleTrackedPaths' sparse exemption needs a git that shipscheck-rules(absent in this container's git 2.39.5; present in current GitHub-runner gits). On older git the function silently degrades to the pre-exemption wedge — every out-of-cone path stays flagged — which is the documented fail-closed intent, not a certification hazard. Pre-existing machinery; this PR only swapped the absence filter inside it and added the memo. No action needed.- Harness note, not a PR defect: my first H3 attempt used a factory-form
vi.mock('node:fs', …)from a harness file outside the package and the pose did not reachsame-file.ts; spy-mode interception did. The PR's own factory mock insame-file.test.tsdemonstrably reaches the module (M5 turns its unsafe-inode test red), so its pose-based tests are not vacuous.
Mutation / vacuity matrix (head)
| mutant | guard under test | suite | result | classification |
|---|---|---|---|---|
M1 predicate → Number(ino) !== 0 |
safe-positive predicate + normalization | identity (1 red), same-file (1 red), acpAgent (1 red) | caught — acpAgent red reproduces the incident error "The standalone working directory identity is compromised" (08-mutation-m1-acp-incident-red.png) |
load-bearing |
| M2 leaf ENOENT ⇒ absent (walk removed) | Windows-shape fail-closed | local-anchor: 3 red incl. "Windows shape: an ENOENT leaf below a regular-file ancestor stays unmeasurable" (expected true to be false) |
caught (07-mutation-m2-windows-shape-red.png) |
load-bearing |
M3 walk statSync → lstatSync |
symlink-to-directory traversal | local-anchor: 4 red incl. both symlink arms | caught | load-bearing |
M4 vanishedStillOnDisk memo removed |
shared ancestor probes | capture-local: 1 red, expected 20 to be less than or equal to 9 (exactly siblingCount × chain.length) |
caught | load-bearing |
| M5 same-file import → core barrel | strict predicate at the import site | same-file: 1 red ("refuses inode identity above the safe-integer range") | caught | load-bearing |
| CTRL leaf-exists ⇒ absent (same file as M2/M3) | positive control | local-anchor: 1 red ("keeps an existing path present") | caught — harness is live | control |
0 survivors. Every new guard is pinned by a test that fails with the intended behavioral mismatch; the layered set (identity predicate restated at three call sites) is held together by M1 catching all three consumers.
Reviewer Test Plan walk-through
- Windows lane completes under unsafe inode — not runnable here (no Windows); simulated equivalents all green: H1 C1/C2, H3, acpAgent degradation tests (gate), sessionAttachments (gate). The lane itself is in Not covered.
- Verifiable↔unverifiable transitions fail closed; canonical/device mismatches rejected — H1 C3–C6: both transitions throw at head (base failed open on C3); dev/ino mismatches throw on both arms. ✔
- Path below a regular-file ancestor unmeasurable and cached anchor refused; genuine absence classified absent — H2 C1/C2/C5 + M2/M3. ✔
- Config native spelling; one failing workspace doesn't block backfill — config.test.ts and session-pr-backfill.test.ts green at head; the backfill isolation test now pins the exact
WorkspaceGenerationClosedErrormessage. Theresolve('/tmp')distinction is a Windows-only assertion (no-op on Linux). ✔ / partial (see Not covered).
Not covered
- The actual
windows-latestlane and incident run 33214395104 — no Windows runner in this container. The harnesses reproduce the shapes the issue reports (unsafeStats.ino, ENOENT-for-ENOTDIR leaf) via fs spies, not the OS behavior that produces them. - Per-commit attribution: checkout is depth-2 (12 commits in the snapshot, 1 reachable); the aggregate
HEAD^1..HEADdiff was verified instead. - Trial merge into current
main(no network); base tip379bef96is the snapshot's main and the PR head already merges it (1a10e72). - Config path-spelling assertions on Windows (Linux no-op); git versions of the real lanes re
check-rules. - Repo-wide suite (targeted changed-file gates only).
Methodology
Environment: node:22-bookworm CI container, ext4, git 2.39.5, user node. Base arm = git worktree add tmp/base-tree HEAD^1 with head's unchanged dependency tree symlinked in (per-package node_modules, unchanged-package dist, head git-commit.ts); core resolves to base source via the package's own vitest alias, and core production code is provably identical between arms. Each harness drives the real production modules (compiled-by-vitest source, no stubs of the code under test); platform shapes come from spy-mode node:fs interception and a PATH-shim git implementing check-rules for the sparse fixture. Arm-selected expectation tables encode base's predicted (broken) behavior as the base arm's pass condition, so a base cell failing as predicted counts as a passing assertion. Mutation runs revert one hunk at a time in a scratch edit, run the owning suite, and git checkout -- the file (worktree verified tracked-clean afterwards). Raw logs in logs/, harness sources archived at the artifact root, captures in evidence/ (01–06 A/B arms as printed, 07–08 live mutant reds). Assertion tally: harness rows 59 + head gates 1373 + base gates 1357 + mutation runs 151 (incl. capture re-runs) = 2940.
Flakiness gate log
rounds=5 files=9 skipped=0
file packages/acp-bridge/src/sessionAttachments.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/sessionAttachments.test.ts
file packages/cli/src/acp-integration/acpAgent.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.test.ts
file packages/cli/src/commands/review/capture-local.incremental.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/capture-local.incremental.test.ts
file packages/cli/src/commands/review/lib/local-anchor.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/local-anchor.test.ts
file packages/cli/src/commands/review/lib/same-file.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/same-file.test.ts
file packages/cli/src/serve/conversations/conversation-workspace.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/conversation-workspace.test.ts
file packages/cli/src/serve/routes/session-pr-backfill.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/session-pr-backfill.test.ts
file packages/cli/src/utils/conversation-directory-identity.test.ts: (cd packages/cli) npx --no-install vitest run ./src/utils/conversation-directory-identity.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/acp-bridge/src/sessionAttachments.test.ts: PP
packages/cli/src/acp-integration/acpAgent.test.ts: PP
packages/cli/src/commands/review/capture-local.incremental.test.ts: PP
packages/cli/src/commands/review/lib/local-anchor.test.ts: PP
packages/cli/src/commands/review/lib/same-file.test.ts: PP
packages/cli/src/serve/conversations/conversation-workspace.test.ts: PP
packages/cli/src/serve/routes/session-pr-backfill.test.ts: PP
packages/cli/src/utils/conversation-directory-identity.test.ts: P
packages/core/src/config/config.test.ts: P
verdict: timeout
summary: the 15-minute budget elapsed before two full rounds completed (1 done) — no flakiness signal either way
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/capture-local.incremental.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/local-anchor.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/same-file.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/conversations/conversation-workspace.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/routes/session-pr-backfill.test.ts: P (exit 0)
round 1 · packages/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/capture-local.incremental.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/local-anchor.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/same-file.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/conversations/conversation-workspace.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/routes/session-pr-backfill.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification








What this PR does
This PR restores the scheduled Windows unit-test lane by treating only safe positive inode values as exact filesystem identity proof. Unverifiable Windows file IDs are normalized to
0across standalone conversation roots, child directories, deletion journals, and ACP relocation expectations, while the existing canonical-path, device, and directory-shape checks remain in place.It also keeps local review anchors fail-closed when Windows reports
ENOENTfor a path whose intermediate component is a regular file, and makes the affected path, Config, attachment, and workspace-isolation fixtures platform-independent.Why it's needed
Scheduled run 33214395104 failed only on
Test (windows-latest, Node 22.x)with 30 failures. The runner had normal disk and memory capacity; the failures were deterministic cross-platform assumptions introduced by earlier changes:Stats.inovalues outside JavaScript's safe integer range, but the standalone conversation path treated every non-zero value as verifiable while durability and protocol boundaries rejected or downgraded it.ENOTDIR; Windows reportsENOENTfor the same intermediate-file shape, which incorrectly looked like a genuinely absent leaf.ENOTDIRspelling.Fixes #10438.
Reviewer Test Plan
How to verify
Evidence (Before & After)
N/A — this is a non-UI CI and filesystem correctness fix. Before: the scheduled Windows lane reported 30 failures. After: changed-file ESLint passes locally; the Windows result is delegated to this PR's CI because the local host is macOS.
Tested on
Environment (optional)
Node.js 22.22.0. UI verification: N/A because this change has no UI or rendering surface.
Risk & Scope
Linked Issues
Fixes #10438.
中文说明
本 PR 做了什么
本 PR 通过只把安全正整数 inode 作为精确文件系统身份凭据,恢复定时 Windows 单测任务。无法精确表示的 Windows 文件 ID 会在 standalone conversation root、子目录、删除日志和 ACP relocation expectation 边界统一归一为
0,同时保留现有的 canonical path、device 和目录形态校验。本 PR 还修正了 Windows 将“路径中间节点是普通文件”报告为
ENOENT时 local review anchor 错误 fail-open 的问题,并把相关的路径、Config、attachment 和 workspace isolation 测试改成跨平台 fixture。为什么需要
定时任务 33214395104 只有
Test (windows-latest, Node 22.x)失败,共 30 个失败。runner 的磁盘和内存正常;这些失败是早期改动引入的确定性跨平台假设:Stats.ino,但 standalone conversation 路径把所有非零值当成可验证身份,而 durability 和 protocol 边界会拒绝或降级该值。ENOTDIR的 review-anchor case;Windows 对相同的中间普通文件形态返回ENOENT,从而被错误识别为叶子路径确实不存在。ENOTDIR拼写的 workspace isolation fixture。修复 #10438。
Reviewer Test Plan
如何验证
Evidence(Before & After)
N/A——这是非 UI 的 CI 和文件系统正确性修复。Before:定时 Windows 任务报告 30 个失败。After:本地 changed-file ESLint 通过;本地主机是 macOS,因此 Windows 结果交由本 PR CI 验证。
Tested on
Environment(可选)
Node.js 22.22.0。UI verification:N/A,因为本改动没有 UI 或渲染影响面。
Risk & Scope
Linked Issues
修复 #10438。