Skip to content

[Bug]: Windows local verification: 30 test failures + 1 gate failure in 4 root causes — fixes on 4 prepared branches (+3 findings) #322

Description

@Ralle1976

Environment: Windows 11 x64 (build 26200), Node 24.18.0, pnpm 9.12.0, no Developer Mode (no unprivileged symlink), Windows PowerShell 5.1 default. Baseline origin/main @ 44b13d3 (v0.5.2), verified 2026-09-22.

Summary

Running the Windows-relevant gates locally on current main reproduces 30 test failures + 1 gate failure — arithmetic: 11 capability (symlink EPERM) + 18 policy + 1 sandbox = 30, plus test:windows itself failing on non-elevated shells. Following up on #298's baseline, each failure was root-caused: there are four distinct causes, and each has its own single-purpose prepared branch on my fork (per the import model used in #304):

Branch Fix Failures retired
test/srt-macos-host-path-join srt-macos env assertion mirrors the impl's join() 1 → fixed outright
test/policy-facade-windows-fixtures policy facade fixtures deterministic across hosts 18 → 8 run again + 10 named skips
test/symlink-privilege-gated-skip symlink-privilege-gated skips + Developer Mode in the windows CI job 11 → run under the privilege, named skips without it
fix/windows-source-check-fallback NTFS preflight (#303) false-negative on non-elevated hosts 1 gate → fixed outright

After all four: 0 failures, with 21 named, reason-labeled skips on a non-privileged Windows host (census below) — 15 of which execute once the Developer Mode CI step (branch C) or a local Developer Mode is in place.

The symlink-privilege branches overlap by zero cases (the 4 policy symlink cases live in local-permission-facade.test.ts; the 11 capability cases in 4 other files). Count check: branch B retires 8+6+4 = 18 ✓.

Branch A — srt-macos path-separator assertion (1 failure)

packages/local-runtime-v2/src/service/sandbox/backend/srt-macos.test.ts:197 asserted the hardcoded literal "/tmp/session-1/git-templates-empty" while the implementation computes the value with node:path.join() (srt-macos.ts:129), which renders \tmp\... on Windows. Fix: build the expected value with join() too — byte-identical to the literal on POSIX. Tradeoff stated plainly: the assertion becomes "the env value is the join of the sandbox temp dir and git-templates-empty" rather than a fixed POSIX string. A skip was rejected because it would hide one separator mismatch, and gating the whole file would drop 19 currently-passing Windows cases.

Branch B — policy facade fixtures are host-dependent (18 failures)

packages/local-runtime/test/unit/local-permission-facade.test.ts failed 18 of 142 on Windows with three fixture/host causes:

  1. Silent host-dependent platform. freshFacade only threaded platform to the facade when a test set it explicitly, so un-pinned tests exercised the Windows decision path on Windows hosts and the POSIX path everywhere else. The suite's intent is POSIX semantics (test:policy is gated platforms: ["darwin","linux"] in scripts/verify.mjs — CI never saw these failures). The simulation now defaults to 'darwin' on Windows hosts (explicitly, with a comment) and is always passed through. Restores 8 decision-logic cases (they run again instead of failing on host artifacts).
  2. NTFS cannot express Unix execute bits. inspectTrashRuntime's POSIX branch requires mode & 0o111, which can never be true on NTFS (chmod is a no-op for Unix bits there), so a Windows host simulating POSIX always saw "mavis-trash is not executable". The check is now host-aware. No production path changes: the POSIX branch only runs with a simulated POSIX platform.
  3. 10 remaining cases gated with named skips and precise reasons (see census): 6 hit a path-rendering boundary — the simulated platform does not reach the path resolution, so POSIX-style inputs resolve to C:\-style paths and containment asks where the test expects allow (evidence: a decision dump showed resolved: \tmp\private-output.log and Path: C:\tmp\private-output.log against rule C:\tmp/**, i.e. the glob never matched). Threading the simulated platform through the permission engine's path rendering would fix these, but that engine is security-adjacent and its suite is deliberately POSIX-only in the verify matrix — leaving that refactor to the team (happy to do it on request). 4 need the symlink privilege (branch C's probe).

Branch C — symlink-privilege-gated skips (11 failures) + CI step

fs.symlink on Windows requires Developer Mode or admin. The 11 cases across canonical-agent-config.test.ts (6), agent.repository.test.ts (3), mcp/project-config.test.ts (1), headless-invocation.test.ts (1) all die with EPERM: operation not permitted, symlink. These exercise link-following safety logic and are worth running wherever the host can create links, so instead of gating them to POSIX each file now probes the privilege lazily (memoized fs.symlinkSync probe) and gates with skipIf(process.platform === "win32" && !canSymlink()) — with the privilege (Developer Mode or elevated) they run exactly as before.

Companion CI step (same branch, separate commit): enable Developer Mode in the windows-latest job so these 11 run in CI instead of skipping:

- if: ${{ runner.os == 'Windows' }}
  run: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /v AllowDevelopmentWithoutDevLicense /t REG_DWORD /d 1 /f

GitHub-hosted Windows runners run elevated, so this is one line before pnpm verify. Framing note: "green by reclassification" was deliberately rejected as the only fix — #238 paused Windows CI "while checks are made reliable", and the skips are labeled deferred coverage pending this CI change, not a cleanup.

Skip census

File Cases gated Condition Runs with Developer Mode?
local-permission-facade.test.ts 6 Windows path-rendering boundary (branch B.3) no — needs the engine follow-up
local-permission-facade.test.ts 4 symlink privilege yes
canonical-agent-config.test.ts 6 symlink privilege yes
agent.repository.test.ts 3 symlink privilege yes
mcp/project-config.test.ts 1 symlink privilege yes
headless-invocation.test.ts 1 symlink privilege yes

Totals: 21 named skips on an unprivileged Windows host; 15 of them execute under the privilege (CI step above or local Developer Mode).

Branch D — the #303 NTFS preflight false-negatives on non-elevated hosts

Found while verifying this branch series: pnpm verify --profile windows fails at test:windows on my machine — and would fail for any contributor without an elevated shell. scripts/check-windows-source-location.mjs shells out to fsutil fsinfo volumeinfo, which on stock Windows 11 answers non-elevated processes with Fehler 5: Zugriff verweigert (exit 1); the preflight then reports "Windows could not verify the checkout volume" and rejects a checkout that is in fact on local NTFS (vol C: and Get-CimInstance Win32_LogicalDisk both report NTFS from the same shell). CI never caught it because GitHub-hosted Windows runners run elevated.

Fix: keep fsutil as the fast path and fall back to one elevation-free Get-CimInstance Win32_LogicalDisk query (locale-invariant FileSystem/DriveType properties) when fsutil is denied; the existing verdict checks stay the single source of truth. Regression tests cover all outcomes: fallback-NTFS passes (this bug), fallback-FAT32 and fallback-non-fixed reject, both-unavailable fails closed with both error messages, allowNonFixed, and the untouched fsutil fast path.

Finding 1 — the 14 PowerShell 5.1 regression tests from #304 run in no CI leg

packages/local-runtime/test/unit/child-bash-lifecycle.test.ts (the Windows PowerShell 5.1 exit codes suite added in #304) is declared in the capability group (test/vitest-suites.json), but test:capabilities carries no windows: true flag in scripts/verify.mjs, so the windows profile skips it — while the full profile runs on ubuntu (where the suite's skipIf(process.platform !== 'win32') skips the 14 cases). Net: the regression coverage that justified #304 executes only on developer Windows machines, never in CI. Suggested (small): add windows: true to test:capabilities, or move/copy the PS-5.1 suite into the windows group. With branch C's CI step, the capability group is viable on windows-latest.

Finding 2 — Windows tmpdir-under-home widens a containment auto-allow (product behavior, not test artifact)

One policy case (turn-scoped trusted exact writes > allows only the supplied exact Plan target…) expects an ask but observes allow on Windows. Root cause: os.tmpdir() resolves under %USERPROFILE% on Windows, so the working-directory-containment path (facade.ts resolveWorkingDirectory home fallback + the fs-permission containment step) treats temp writes as contained and auto-allows what the test expects to ask. If the same rule applies to real sessions, %TEMP% writes are auto-approved on Windows where a POSIX host would ask. Branch B gates the case with that reason — the behavior difference itself is left to you deliberately: containment semantics are a product call. Happy to root-cause the intended semantics in a follow-up.

Finding 3 (context) — the windows profile's test surface

Related to Finding 1: pnpm verify --profile windows currently runs no vitest groups except the 2-case windows contract (scripts/verify.mjs:40-81; policy is darwin/linux, sandbox is darwin, and capability/smoke/byok/status-contract have no windows: true). With branches B+C and the CI step, the policy and capability groups become viable on windows-latest; extending the profile is then a one-flag change each. Not proposing it here — that is a CI-design decision.

Validation & boundaries

Per branch, on the environment above (2026-09-22):

Branch Check Result
A pnpm test:sandbox 26 passed, 22 skipped, 0 failed (was 1 failed)
B pnpm test:policy 132 passed, 10 skipped, 0 failed (was 124 passed, 18 failed); pnpm typecheck clean
C vitest run of the 4 affected files 188 passed, 11 skipped, 0 failed (was 11 EPERM failures); pnpm typecheck clean
D pnpm test:windows + real CLI preflight 8/8 (was 7/8 with the real-host case failing); CLI exit 0 (was exit 1)

Context number: full pnpm test:capabilities on the rebased #298 branch (baseline + those fixes) = 11 failed / 4369 passed / 60 skipped — exactly the 11 symlink-EPERM cases this issue's branches address.

Not run: macOS/Linux hosts (each fix is a no-op on POSIX by construction — gates short-circuit before the probe and the render fix mirrors the impl's join — but no POSIX run was executed here), a privileged Windows host for branch C's cases (they run unchanged with the privilege; the probe only affects the skip predicate), full pnpm verify per branch, live-model runs, Desktop app.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliStandalone mcode: TUI, headless, ACP and source builds/tooling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions