Skill Tracer
Skills › gstack › gstack › windows-curation.ts

windows-curation.ts

550 lines · scripts/lib/windows-curation.ts at 3f79d81

1/**
2 * Windows-safe curation for the free suite (`--windows-only`): POSIX-only
3 * source patterns, explicit known-incompatible files, and force-includes for
4 * pattern hits that do not apply. Pinned by test/test-free-shards.test.ts and
5 * test/windows-native-workflows.test.ts.
6 */
7import * as fs from 'node:fs';
8import * as path from 'node:path';
9
10const ROOT = path.resolve(import.meta.dir, '../..');
12// POSIX-only patterns that indicate a test will fail on windows-latest no
13// matter how the runner shards. Codex's v1.18.0.0 review flagged the first
14// three as concrete examples in the existing free suite (test/ship-version-sync.test.ts:72,
15// test/helpers/providers/claude.ts:22, package.json:12). We scan the test's
16// own content here so the filter stays automatic as new tests land. The
17// "Windows-incompatible APIs" patterns at the bottom were added after the
18// first windows-free-tests CI run surfaced concrete failure modes.
19const WINDOWS_FRAGILE_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
20 // Hardcoded POSIX shells / commands.
21 { pattern: /['"`]\/bin\/(?:ba)?sh/, reason: 'hardcoded /bin/sh or /bin/bash' },
22 { pattern: /spawnSync\(['"]sh['"],|spawn\(['"]sh['"],|exec\(['"]sh /, reason: 'spawn("sh", ...)' },
23 { pattern: /['"]bash -c['"]|['"]sh -c['"]/, reason: 'bash -c / sh -c' },
24 { pattern: /['"`]\/tmp\//, reason: 'raw /tmp/ path (use os.tmpdir())' },
25 { pattern: /['"]chmod\b/, reason: 'chmod shell command' },
26 { pattern: /['"]xargs\b/, reason: 'xargs pipeline' },
27 { pattern: /\bwhich claude\b/, reason: 'which claude (use Bun.which)' },
28 // Windows-incompatible APIs.
29 { pattern: /\.mode\s*&\s*0o[0-7]+/, reason: 'POSIX file mode bitmask (mode & 0o600 etc — Windows fakes mode bits)' },
30 { pattern: /\.endsWith\(['"]\//, reason: 'hardcoded forward-slash path assertion (Windows uses \\\\)' },
31 { pattern: /['"]\.\/[a-zA-Z][^"']*['"]\)\s*\.\s*toBe\(true\)/, reason: 'forward-slash path comparison' },
32 // Tests that spawn a bash shebang script in bin/ via spawnSync. Git Bash on
33 // Windows can run `bash /path/to/script` but spawnSync(scriptPath, ...)
34 // tries to execute the file directly via CreateProcess, which fails on the
35 // shebang. The pattern matches `, 'bin'` as a path-join argument (closing
36 // OR followed by another segment), which catches:
37 // - path.join(ROOT, 'bin', 'script-name') — typical
38 // - join(import.meta.dir, '..', 'bin', 'name') — destructured (diff-scope)
39 // - path.join(ROOT, 'bin') — bare BIN constant (brain-sync)
40 { pattern: /,\s*['"]bin['"]\s*[,)]|['"]\.?\/?bin\/[a-z][\w-]+['"]/, reason: 'spawns bin/ shebang script (Windows CreateProcess does not parse shebangs)' },
41 // Tests that spawn the browse server as a subprocess via `bun run server.ts`.
42 // The Bun → server.ts → Playwright path is the same one that doesn't work
43 // on Windows (PR #1238 windows-pty-bun-pty-fix). Tests typically set
44 // BROWSE_HEADLESS_SKIP=1 to skip the browser launch but still need a working
45 // server, which they don't get on Windows.
46 { pattern: /BROWSE_HEADLESS_SKIP|spawn\(\[['"]bun['"],\s*['"]run['"]/, reason: 'spawns the browse server subprocess (Bun-driven path is Windows-broken)' },
47];
49// Explicit known-Windows-incompatible test files that don't fit a regex
50// pattern. Listed here with the precise reason. Prefer adding a pattern above
51// when possible; this list is for environment-/runtime-specific tests where
52// the failure mode is structural rather than detectable via source-file scan.
53export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> = [
54 { file: 'test/ship-queue-land.test.ts', reason: 'its gh shim is an extensionless #!/usr/bin/env node script on a colon-joined PATH, which windows-latest cannot launch, so land reaches the real gh (no GH_TOKEN) and every case fails (windows-free-shard 6, run 40824965988); land is a controller command run on Linux and macOS' },
55 // Like test/paid-rerun.test.ts (excluded by pattern), it replays a gate capture through the paid lane's --report and --rerun-plan CLI.
56 { file: 'test/shared-libs-index-flags-readonly.test.ts', reason: 'its assume-unchanged replay seeds the review through test/helpers/shared-libs-path-fixture.ts, which launches bin/gstack-review-log (a bash script) directly; windows-latest refuses it as not found in $PATH (windows-free-shard 5, run 40727825859)' },
57 { file: 'test/credential-rejection.test.ts', reason: 'replays run 39696775546 through the --report and --rerun-plan CLI of the paid lane, which runs only on Linux CI; on windows-latest that CLI exits 1 with EINVAL in open() (windows-free-shard run 39982914852)' },
58 // Field list 2, W11: the review-voice install is a POSIX launcher contract, exercised end to end on Linux and macOS.
59 { file: 'test/capy-review-voice.test.ts', reason: 'copies the checkout with tar -C into the temp root (GNU tar reads a D: path as a remote host) and launches the bash --for review-voice installer with a colon-joined GSTACK_LAUNCH PATH' },
60 // C-pregate. Since fix wave PR 5 (item 10b) test/helpers/pregate-fixture.ts and restamp-fixture.ts
61 // runBin launch bins through lib/script-invocation.ts, so gstack-pregate runs on windows-latest
62 // (fix wave PR 5 windows-free-tests run). These still fail there on Linux-host expectations, not
63 // on launching: the fixture's test lanes are Linux workflow jobs, which a Windows host records as
64 // requires-remote obligations instead of running.
65 { file: 'test/pregate-guards.test.ts', reason: 'expects the Linux-host lanes verdict and POSIX-spelled Bun paths ($HOME/.bun/bin, /gone/bun) and the POSIX fixture runner\'s runs.log; gstack-pregate itself launches through lib/script-invocation.ts' },
66 { file: 'test/pregate-remote.test.ts', reason: 'expects the Linux-host lanes verdict: on Windows the fixture\'s Linux lane is a requires-remote obligation, so the small-machine and --remote cases see no local tier; gstack-pregate itself launches through lib/script-invocation.ts' },
67 { file: 'test/pregate-regate.test.ts', reason: 'its REGATE and MIGRATION_COLLISION verdicts differ on windows-latest, not diagnosed in fix wave PR 5; gstack-pregate regate itself launches through lib/script-invocation.ts' },
68 { file: 'test/pregate-invalidation.test.ts', reason: 'expects the Linux-host lanes verdict (the windows lane as the requires-remote obligation); on Windows the Linux lane is the obligation instead; gstack-pregate itself launches through lib/script-invocation.ts' },
69 { file: 'test/pregate-selection.test.ts', reason: 'its gstack-pregate (bin) cases expect the Linux-host lanes verdict (a local lane run); on Windows the fixture\'s Linux lane is a requires-remote obligation' },
70 { file: 'test/pregate-stamp-only.test.ts', reason: 'its gstack-pregate (bin) cases expect the Linux-host lanes verdict for the mapped-file and test-reads-manifest fixtures, and its mode-change case needs a POSIX exec bit and symlink; the stamp rule it drives is POSIX-run like the other pregate fixtures' },
71 { file: 'test/pregate-starter.test.ts', reason: 'expects the Linux-host lanes verdict (the fixture\'s Linux lane runs locally and the no-policy row is warn); on Windows that lane is a requires-remote obligation; gstack-pregate itself launches through lib/script-invocation.ts' },
72 { file: 'test/pregate-allowlist.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin and gstack-redact through the bin shebang; the allowlist rules it drives are POSIX-run like the other pregate fixtures' },
73 { file: 'test/pregate-environment.test.ts', reason: 'drives exit-127 and spawn-failure classification through a POSIX shell (bash\'s command-not-found exit) and expects the Linux-host lanes verdict; on Windows the fixture\'s Linux lane is a requires-remote obligation' },
74 { file: 'test/pregate-scratch.test.ts', reason: 'asserts the 0700 mode of the per-run scratch directory and the Linux-host lanes verdict (lane and guard logs written locally); Windows fakes mode bits and makes the Linux lane a requires-remote obligation' },
75 { file: 'test/pregate-bats-nested.test.ts', reason: 'expects the bats lane to run locally, which it does on a Linux host; on Windows the fixture\'s Linux lane is a requires-remote obligation' },
76 { file: 'test/pregate-docs-lane-review.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess); the docs-lane selection rules it drives are POSIX-run like the other pregate fixtures' },
77 { file: 'test/pregate-dart.test.ts', reason: 'its dart, flutter and fvm stand-ins are bash scripts on a colon-joined PATH, which Bun.which and CreateProcess cannot use on Windows, and its containment case needs a POSIX symlink; the Dart lane code it drives is POSIX-run like the other pregate fixtures' },
78 { file: 'test/pregate-non-js.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin and expects the Linux-host lanes verdict, like test/pregate-docs-lane-review.test.ts and test/pregate-selection.test.ts' },
79 { file: 'test/pregate-remote-gbrain.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs the gbrain-shaped ci:ubicloud package script through the POSIX lane runner, like test/pregate-remote.test.ts' },
80 { file: 'test/pregate-remote-stamp-only.test.ts', reason: 'expects the Linux-host lanes verdict like test/pregate-remote.test.ts (on Windows the fixture\'s Linux lane is a requires-remote obligation) and skips itself on win32 (describe.skipIf), so the curated lane would run an empty file (#3485, #3486)' },
81 // C-pregate: these drive bin/gstack-issue-links and bin/gstack-ship-receipt through
82 // test/helpers/pregate-fixture.ts runBin.
83 { file: 'test/stamp-templates.test.ts', reason: 'spawns bin/gstack-tree-receipt through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' },
84 { file: 'test/ship-issue-intent.test.ts', reason: 'spawns bin/gstack-issue-links and bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) with a `gh` shell shim on PATH' },
85 { file: 'test/ship-receipt-carry-pregate.test.ts', reason: 'passes a gstack-pregate result to gstack-ship-receipt write and carry; on Windows the fixture\'s Linux lane is a requires-remote obligation, so write refuses the pregate as uncleared and exits 1 (windows-free-shard run 40611810409), like the other --pregate receipt tests' },
86 // PR E2: lane ownership and evidence verification run bin/gstack-lane-check and bin/gstack-evidence
87 // through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess); the lib paths are
88 // exercised in the same file, so there is no Windows-safe split to keep.
89 { file: 'test/lane-coordination.test.ts', reason: 'spawns bin/gstack-lane-check and bin/gstack-evidence through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess) and reruns POSIX lane commands' },
90 {
91 file: 'test/ship-measure-seeded-fixture.test.ts',
92 reason: 'drives a POSIX shell stub project (`./evals.sh` with a shebang) that CreateProcess cannot exec; the measure runner itself is covered by ship-measure.test.ts with in-process fake runners',
93 },
94 {
95 file: 'test/qa-evidence-producer.test.ts',
96 reason: 'executes the registered Linux native actor and its inotify observer; portable capture and Windows job behavior are covered by qa-evidence.test.ts',
97 },
98 {
99 file: 'test/qa-functional-fixture.test.ts',
100 reason: 'executes graceful POSIX signal cancellation; Bun on Windows uses TerminateProcess and cannot run the fixture SIGTERM cleanup handler',
101 },
102 {
103 file: 'test/qa-functional-observer-atomic.test.ts',
104 reason: 'exercises real Linux inotify inode and directory watches through libc.so.6; Windows has no equivalent kernel interface',
105 },
106 {
107 file: 'test/docsync-report-interface.test.ts',
108 reason: 'executes registered native documentation callbacks with their real Linux inotify write observer before the model boundary',
109 },
110 {
111 file: 'test/setup-gbrain-fixture.test.ts',
112 reason: 'the fixture invokes real POSIX detector/verifier helpers through executable shebang wrappers',
113 },
114 {
115 file: 'test/setup-gbrain-windows-mcp.test.ts',
116 reason: 'drives the Windows MCP registration branch on POSIX by putting uname/claude shebang stubs on a colon-separated PATH and comparing POSIX path spellings; the console behavior it guards needs a manual Windows check',
117 },
118 {
119 file: 'test/hermetic-skills-seeding.test.ts',
120 reason: 'seeds the POSIX PTY skill runtime, whose embedded shell paths require a POSIX temporary root',
121 },
122 {
123 file: 'test/hermetic-wiring.test.ts',
124 reason: 'its runtime contract check seeds the POSIX PTY skill runtime; the curated Windows lane does not run that harness',
125 },
126 {
127 file: 'test/pty-workspace-trust.test.ts',
128 reason: 'launches the POSIX PTY harness with a fake executable and bound skill runtime',
129 },
130 {
131 file: 'test/host-config.test.ts',
132 reason: 'asserts "claude" binary on PATH (only true when running inside Claude Code, not on bare CI runner)',
133 },
134 {
135 file: 'browse/test/findport.test.ts',
136 reason: 'asserts Bun.serve.stop() is fire-and-forget — Bun behavior differs on Windows for this polyfill',
137 },
138 // First full run of the expanded lane (v1.66, 13 → ~258 files) surfaced
139 // seven POSIX-bound files the content patterns cannot see (their
140 // POSIX-ness is what they TEST, or arrives via a variable). Receipts:
141 // PR #2593 windows-free-tests run 31918591602.
142 {
143 file: 'test/codex-under-codex-detection.test.ts',
144 reason: 'drives the rendered preflight bash under a hardcoded POSIX PATH (/usr/bin:/bin) — bash is unreachable through that PATH on Windows, so every case sees empty output (v1.67 windows lane run 95234224148)',
145 },
146 {
147 file: 'test/regression-pr1169-build-app-sed.test.ts',
148 reason: 'tests sed escape sequences in build-app.sh — sed/bash are the subject under test',
149 },
150 {
151 file: 'test/setup-conductor-worktree.test.ts',
152 reason: 'tests ln -snf symlink semantics in the setup script — POSIX ln is the subject under test',
153 },
154 {
155 file: 'test/artifacts-init-migration.test.ts',
156 reason: 'runs a bash migration script + jq against a scaffolded git state — POSIX toolchain paths break under cmd spawn',
157 },
158 {
159 file: 'test/gstack-decision-semantic.test.ts',
160 reason: 'installs a fake gbrain SHEBANG SHIM on PATH; Windows spawn cannot exec shebang scripts',
161 },
162 {
163 file: 'test/question-log-hook.test.ts',
164 reason: 'spawns the PostToolUse hook script (bash shebang) directly; Windows spawn cannot exec it',
165 },
166 {
167 file: 'browse/test/browser-skills-e2e.test.ts',
168 reason: 'asserts forward-slash tier paths (<repo>/browser-skills/) that resolve with backslashes on Windows',
169 },
170 {
171 file: 'design/test/variants-retry-after.test.ts',
172 reason: 'wall-clock retry-timing assertions — flaky on the slow windows-latest runner even with widened bounds',
173 },
174 // Round-2 census (PR #2593 run 31919227507) after the first seven:
175 {
176 file: 'test/skill-census.test.ts',
177 reason: 'census walk throws at module load on Windows (skill-census.ts:63) — the skills-tree symlink layout needs Developer Mode that CI runners lack',
178 },
179 {
180 file: 'browse/test/browser-manager-unit.test.ts',
181 reason: 'wedges the shard to its wall deadline on windows-latest (in-flight at kill); needs a Windows repro to diagnose — macOS + Linux lanes cover the file',
182 },
183 // Round-3 census (PR #2593 run 31919871680): the round-2 wedge had been
184 // TRUNCATING its shard, so these seven only surfaced once shard 2 completed.
185 // All the same POSIX-environment classes: PID/cmdline identity probing,
186 // bash scripts as the subject under test, env-scrubbed child spawns.
187 {
188 file: 'browse/test/server-embedder-terminal-port.test.ts',
189 reason: 'identity-based terminal-agent kill probes PID/cmdline with POSIX semantics; teardown asserts fail on windows-latest',
190 },
191 {
192 file: 'design/test/daemon-discovery.test.ts',
193 reason: 'verifyIdentity matches a spawned daemon via /proc-style cmdline probing — POSIX identity semantics',
194 },
195 {
196 file: 'test/context-save-hardening.test.ts',
197 reason: 'bash context-save/migration scripts (HOME-unset semantics, random-suffix path) are the subject under test',
198 },
199 {
200 file: 'test/eval-list-cli.test.ts',
201 reason: 'spawns the eval:list CLI via bun with a constructed env — bun resolution fails under Windows spawn',
202 },
203 {
204 file: 'test/memory-cache-injection.test.ts',
205 reason: 'exercises hook/deny-enforcement shell scripts — POSIX toolchain is the subject under test',
206 },
207 {
208 file: 'test/migrations-v1.65.0.0.test.ts',
209 reason: 'bash migration script (bunx re-fetch, .done markers) is the subject under test',
210 },
211 {
212 file: 'test/question-preference-hook.test.ts',
213 reason: 'spawns the PreToolUse preference hook (shebang script) directly; Windows spawn cannot exec it',
214 },
215 // Round-4 census (PR #2593 run 31920052810): unhandled errors with no
216 // (fail) lines — attributed statically (the lane had no log artifact yet).
217 {
218 file: 'browse/test/browser-skill-commands.test.ts',
219 reason: 'spawnSkill spawns bun with a constructed env — bun resolution fails under Windows spawn (unhandled, no (fail) line)',
220 },
221 {
222 file: 'browse/test/security-audit-r2.test.ts',
223 reason: 'symlink-attack fixtures (evil-link) need Developer Mode CI runners lack; expect(toThrow) fires unhandled on Windows',
224 },
225 // CSO comprehensive execution is qualified only for Linux containers behind
226 // the POSIX watchdog and Unix-domain registry broker. Keep the portable
227 // static/parser contracts in the Windows lane while leaving these exact
228 // containment suites to the Linux and macOS gates.
229 {
230 file: 'test/cso-preparation-adversarial.test.ts',
231 reason: 'exercises POSIX prepared-tree and archive-cache containment for qualified Linux Docker execution, which Windows does not admit',
232 },
233 {
234 file: 'test/cso-preparation-container.test.ts',
235 reason: 'asserts POSIX permission and symlink semantics for inert exports consumed by qualified Linux Docker execution',
236 },
237 {
238 file: 'test/cso-preparation-executor.test.ts',
239 reason: 'executes the Linux Docker acquisition path and its Unix-domain registry broker; comprehensive execution is unavailable on Windows',
240 },
241 {
242 file: 'test/cso-verification-cleanup.test.ts',
243 reason: 'spawns the POSIX detached watchdog used by contained repair verification, which Windows intentionally leaves unavailable',
244 },
245 {
246 file: 'test/cso-witness.test.ts',
247 reason: 'tests the contained repair witness with POSIX private-directory and compiled-helper assumptions; comprehensive execution is unavailable on Windows',
248 },
249 {
250 file: 'test/shard-engine-equivalence.test.ts',
251 reason: 'its classification golden was recorded from the POSIX runners (process-group wall kill); the win32 engine path is pinned by the mocked-platform case in shard-engine.test.ts',
252 },
253 {
254 file: 'test/claude-overlay-setup-default.test.ts',
255 reason: 'every case runs the real ./setup under describe.skipIf(win32); its bin/ spawns live in test/helpers/claude-overlay-fixture.ts, so the source scan misses them',
256 },
257 {
258 file: 'test/claude-overlay-setup-installs.test.ts',
259 reason: 'every case runs the real ./setup under describe.skipIf(win32); its bin/ spawns live in test/helpers/claude-overlay-fixture.ts, so the source scan misses them',
260 },
261 {
262 file: 'test/cso-scanner-cli.test.ts',
263 reason: 'drives the prebuilt POSIX CSO launcher with /usr/bin/git and a POSIX-only PATH; native Windows launcher behavior is covered by the dedicated cso-windows-launcher gate',
264 },
265 // C-stamp (ship policy, restamp, tree receipt, evidence bundle, ship receipt):
266 // every case spawns bin/ shebang scripts and a #!/bin/sh `gh` shim through
267 // test/helpers/restamp-fixture.ts, so the source scan misses them.
268 {
269 file: 'test/gstack-ship-policy.test.ts',
270 reason: 'spawns bin/gstack-ship-policy (shebang) through test/helpers/restamp-fixture.ts; the policy schema and loader are pure lib code covered on the POSIX gates',
271 },
272 {
273 file: 'test/restamp-command-boundary.test.ts',
274 reason: 'spawns bin/gstack-restamp (shebang) and a POSIX release tool through test/helpers/restamp-fixture.ts',
275 },
276 {
277 file: 'test/tree-receipt-stamp-sensitive.test.ts',
278 reason: 'spawns bin/gstack-tree-receipt, bin/gstack-restamp and bin/gstack-evidence (shebangs) plus a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts',
279 },
280 {
281 file: 'test/evidence-bundle-portability.test.ts',
282 reason: 'spawns bin/gstack-evidence and bin/gstack-tree-receipt (shebangs) through test/helpers/restamp-fixture.ts',
283 },
284 {
285 file: 'test/gstack-ship-receipt.test.ts',
286 reason: 'spawns bin/gstack-ship-receipt and bin/gstack-artifact (shebangs) plus a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts',
287 },
288 // Field list 2, W1 (shared merge queue): same fixture helpers, same shebang spawns.
289 {
290 file: 'test/gstack-ship-queue.test.ts',
291 reason: 'spawns bin/gstack-ship-queue (shebang) and a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts and test/helpers/ship-queue-fixture.ts',
292 },
293 // GSTA-49 T1 (merge trains): the train lib logic runs on Windows in test/ship-queue-train-logic.test.ts.
294 {
295 file: 'test/ship-queue-train-land.test.ts',
296 reason: 'drives train land against test/helpers/ship-queue-train-fixture.ts plus an extensionless node gh wrapper on a colon-joined PATH; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead, as for test/ship-queue-train.test.ts and test/ship-queue-land.test.ts (windows-free-shard 6, run 40824965988)',
297 },
298 {
299 file: 'test/ship-queue-train.test.ts',
300 reason: 'drives bin/gstack-ship-queue against the stateful #!/bin/sh gh shim in test/helpers/ship-queue-train-fixture.ts; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead of the shim (windows-free-shard run 39152702056)',
301 },
302 {
303 file: 'test/ship-queue-train-cost.test.ts',
304 reason: 'drives bin/gstack-ship-queue train verdict against the same #!/bin/sh gh shim in test/helpers/ship-queue-train-fixture.ts; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead of the shim (windows-free-shard 2 on #3478 at d5e6a7d)',
305 },
306 {
307 file: 'test/ship-queue-train-ops.test.ts',
308 reason: 'drives bin/gstack-ship-queue (screen verdicts, scratch-merge screens, prune --stale, watch, forget, verify --post-merge, ready --dispatch-ci, --stack-tests) against the same #!/bin/sh gh shim in test/helpers/ship-queue-train-fixture.ts',
309 },
310 {
311 file: 'test/ship-queue-train-generic.test.ts',
312 reason: 'drives bin/gstack-ship-queue train build, verdict, land, watch, init and doctor against the same #!/bin/sh gh shim plus an extensionless node gh wrapper on a colon-joined PATH; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead (windows-free-shard 1, run 41218228855)',
313 },
314 {
315 file: 'test/ship-policy-compat.test.ts',
316 reason: 'spawns bin/gstack-ship-policy, bin/gstack-restamp and bin/gstack-tree-receipt (shebangs) through test/helpers/restamp-fixture.ts',
317 },
318 {
319 file: 'test/tree-receipt-gbrain-pair.test.ts',
320 reason: 'spawns bin/gstack-tree-receipt and bin/gstack-ship-policy (shebangs) through test/helpers/restamp-fixture.ts',
321 },
322 {
323 file: 'test/restamp-gate-ahead-changelog.test.ts',
324 reason: 'spawns bin/gstack-restamp (shebang) and a POSIX release tool through test/helpers/restamp-fixture.ts',
325 },
326 {
327 file: 'test/ship-release.test.ts',
328 reason: 'spawns bin/gstack-ship-release (shebang) directly and writes #!/bin/sh gh stubs through test/helpers/ship-bin-fixture.ts; windows-latest exits before the bin runs (windows-free-shard 3, run 41073592483)',
329 },
330];
332// Force-include overrides: files a WINDOWS_FRAGILE_PATTERNS regex excludes for
333// a reason that does not actually apply to them. Each entry documents WHY the
334// pattern hit is a false positive — the point of these files is Windows
335// coverage, so auto-excluding them defeats the regression tests they carry.
336export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [
337 // Named Windows coverage for fixes whose bug only bites on Windows: kept in
338 // the curated lane even if a future edit trips a content pattern.
339 { file: 'browse/test/cli-chain-stdin.test.ts', reason: 'piped-stdin browse chain against a stub daemon, and no-flow usage errors before any daemon boots; spawns the CLI through Bun argv' },
340 { file: 'browse/test/runtime-root-server-bundle.test.ts', reason: 'a runtime root copy without node_modules resolves the checkout bundle via .source-path and refuses on version skew; os.tmpdir layouts only' },
341 { file: 'test/cso-windows-docker.test.ts', reason: '/cso docker.exe discovery under known-folder roots (real path, no reparse points) and the native-transport-unsupported outcome; pure win32 path logic' },
342 { file: 'test/copilot-windows-bash.test.ts', reason: 'the Copilot glossary line that runs bash blocks in Git for Windows Bash; reads host config only' },
343 { file: 'test/ship-hook-windows-paths.test.ts', reason: 'runs bin/ helpers through explicit bash and Bun argv with forward-slash paths; never executes a shebang; the path-spelling simulation is skipIf win32' },
344 { file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' },
345 { file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' },
346 { file: 'test/bun-config-shell-bins.test.ts', reason: '#3415: runs the record bins through lib/script-invocation.ts (Git Bash on Windows) with MSYS path conversion off, the case it guards; its bun and cygpath stand-ins and simulated-OSTYPE cases are skipIf win32' },
347 { file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' },
348 { file: 'test/state-root-parity.test.ts', reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral' },
349 { file: 'test/generator-eexist.test.ts',
350 reason: 'E4: runs the generators through Bun argv with the Windows EEXIST emulation preload; no shebang execution' },
351 { file: 'test/gstack-config-gbrain-refresh.test.ts', reason: 'E6: runs bin/gstack-config through explicit bash; its gbrain/python3 shims are found by bash PATH lookup, never launched by CreateProcess' },
352 { file: 'test/hook-scripts.test.ts', reason: '#3067 PowerShell and NotebookEdit hook coverage: every hook runs through spawnSync("bash", [script]) with the payload on stdin, scratch state lives under os.tmpdir(); its /tmp/ literals are payload strings, never opened; symlink, newline-path and POSIX-PATH cases are skipIf win32' },
353 { file: 'test/gstack-doctor.test.ts', reason: 'runs bin/gstack-doctor and setup --status through explicit bash argv in an os.tmpdir fixture; its bun/codex/claude/probe stubs are found by bash PATH lookup, and expected paths use bash spellings' },
354 // Fix wave PR 5 (item 10b, #3197): the fixture runBin launches bins through lib/script-invocation.ts
355 // (bun bins as Bun argv, bash bins through Git Bash); passed on windows-latest in the fix wave PR 5 windows-free-tests run.
356 { file: 'test/gstack-regen.test.ts', reason: 'runs bin/gstack-regen through the fixture runBin (lib/script-invocation.ts) and its regen commands through the resolved Git Bash' },
357 {
358 file: 'test/qa-evidence.test.ts',
359 reason: 'invokes the production helper through Bun argv and exercises native Windows job cleanup, private file captures and backpressured receipt output',
360 },
361 {
362 file: 'test/qa-evidence-selection.test.ts',
363 reason: 'bin/ strings are literal dependency and Windows-selection assertions; no native actor or shebang command is launched',
364 },
365 {
366 file: 'test/qa-deadline.test.ts',
367 reason: 'launches the guard through Bun argv; mode assertions and POSIX signal cases are platform-gated, while Windows job cleanup must execute natively',
368 },
369 {
370 file: 'test/qa-deadline-selection.test.ts',
371 reason: 'bin/ strings are dependency-selection inputs; this suite never launches a shebang executable',
372 },
373 {
374 file: 'test/shared-libs-source-reads.test.ts',
375 reason: 'bin/ literal is a mocked launch assertion; actual worktree fingerprinting explicitly invokes Bash on Windows',
376 },
377 {
378 file: 'test/claude-code-windows-job.test.ts',
379 reason: 'invokes Bun directly; verifies Windows job containment at the standalone CLI boundary',
380 },
381 {
382 file: 'test/claude-code-runner.test.ts',
383 // The bin/ path is launched through process.execPath (Bun), never as a shebang; keep taskkill supervision in the Windows lane.
384 reason: 'invokes the runner via Bun argv; fake CLI and timeout descendant assertions cover native Windows taskkill',
385 },
386 {
387 file: 'test/setup-gbrain-remote-caller.test.ts',
388 // bin is an expected PATH component; the adapter injects the SDK boundary
389 // and never launches a shebang. Keep the native delimiter cases in CI.
390 reason: 'replays the registered SDK callback with fixture-only bin paths; covers native Windows PATH composition',
391 },
392 {
393 file: 'test/cso-windows-build-contract.test.ts',
394 // bin is a temporary staging directory. The adapter injects spawnSync;
395 // real PowerShell/native execution remains in cso-windows-launcher.
396 reason: 'replays the native build callbacks with an injected subprocess; bin paths are staging fixtures, not shebang launches',
397 },
398 {
399 file: 'test/setup-windows-rerun-refresh.test.ts',
400 // Trips the "spawns bin/ shebang script" pattern via path.join(..., 'bin',
401 // 'tool.sh') fixture paths, but every spawn goes through test/helpers/bash-script.ts
402 // (bash <tempfile>) — Git Bash executes it fine on windows-latest, with no argv-length ceiling. This file IS
403 // the #2444 Windows regression coverage (IS_WINDOWS=1 copy-refresh path);
404 // excluding it here would keep the bug class unexercised on the one
405 // platform it bites.
406 reason: 'bin/ hits are fixture path segments; spawns bash explicitly — the IS_WINDOWS=1 refresh path must run on windows-latest',
407 },
408 {
409 file: 'test/uninstall-windows-copies.test.ts',
410 // Trips the "spawns bin/ shebang script" pattern via the
411 // path.join(ROOT, 'bin', 'gstack-uninstall') constant, but the script is
412 // always spawned through spawnSync('bash', [UNINSTALL, ...]). This file
413 // carries the #2563 Windows real-dir-copy uninstall coverage — the bug
414 // ONLY reproduces on the copy install shape windows-latest exercises.
415 // The symlink-shape describe block self-skips on win32.
416 reason: 'bin/ hit is a bash-spawned script path; #2563 real-dir uninstall coverage must run on windows-latest',
417 },
418 {
419 file: 'browse/test/file-permissions.test.ts',
420 // Trips the POSIX-mode-bitmask pattern, but every `mode & 0o777` assertion
421 // is platform-guarded: win32-only tests return early, POSIX-only tests
422 // guard the bitmask behind `process.platform !== 'win32'`, and the
423 // symlink-skip regression test both wraps symlinkSync in try/catch
424 // (runners without Developer Mode can't create symlinks) and guards its
425 // bitmask — on win32 it asserts behavior (warns, skips, doesn't throw,
426 // target stays usable), never fake Windows mode bits (dirs stat 0o777
427 // there, so a 0o755 expectation fails on runner semantics, not our code).
428 // This file carries the win32-only icacls-by-SID regression tests, which
429 // can ONLY execute on windows-latest — excluding it here means the
430 // machine-account ACL lockout regression is never exercised on the one
431 // platform it bricks.
432 reason: 'every mode-bitmask assertion is guarded off win32 (behavior asserted instead); win32-only ACL regression tests must run on windows-latest',
433 },
434 {
435 file: 'browse/test/terminal-agent-owner-watchdog.test.ts',
436 // Trips the spawn(['bun','run',...]) pattern, whose reason is the
437 // Playwright-bound browse server. This test spawns terminal-agent.ts,
438 // which imports only fs/path/crypto + local helpers (no Playwright, no
439 // PTY at module scope) and boots under Bun on Windows — the owner-PID
440 // orphan leak it pins was reported on Windows (#2019).
441 reason: 'spawns terminal-agent (no Playwright), not the browse server; owner-orphan leak is a Windows defect',
442 },
443 {
444 file: 'test/timeline-stop-hook-slug.test.ts',
445 // Trips the "spawns bin/ shebang script" pattern via the fixture's
446 // path.join(tree, 'bin', 'gstack-slug') stub, but the hook runs through
447 // Bun argv and reaches the stub through runBin, which hands it to Git
448 // Bash on win32. #3103 is a Windows latency defect: this lane is the one
449 // place the hook's return-within-budget assertion meets real Windows
450 // process startup.
451 reason: '#3103: the hook runs through Bun argv and its gstack-slug stub through runBin (Git Bash on win32); the return-within-budget timing must run on windows-latest',
452 },
453];
455/**
456 * Pattern hits disproved by running each excluded file alone on windows-latest
457 * (temporary probe run 37221995077, Bun 1.4.0, Chromium installed): every case
458 * passed and at least one case executed. Their /tmp/ literals are fixture
459 * strings or platform-gated, never opened on Windows.
460 */
461export const WINDOWS_PROBE_SAFE: string[] = [
462 'browse/test/data-platform.test.ts',
463 'browse/test/dual-listener.test.ts',
464 'browse/test/error-handling.test.ts',
465 'browse/test/learnings-injection.test.ts',
466 'browse/test/pdf-flags.test.ts',
467 'browse/test/security-adversarial.test.ts',
468 'browse/test/tunnel-gate-unit.test.ts',
469 'browse/test/xprotect-heal.test.ts',
470 'design/test/feedback-roundtrip.test.ts',
471 'ios-qa/daemon/test/tailscale-localapi.test.ts',
472 'make-pdf/test/asideClient.test.ts',
473 'make-pdf/test/e2e/diagram-gate.test.ts',
474 'make-pdf/test/e2e/emoji-gate.test.ts',
475 'make-pdf/test/e2e/format-gate.test.ts',
476 'make-pdf/test/e2e/landscape-gate.test.ts',
477 'test/brain-preflight.test.ts',
478 'test/ceo-barless-submit.test.ts',
479 'test/ceo-split-collection.test.ts',
480 'test/ceo-split-question-policy.test.ts',
481 'test/ci-paid-coordination.test.ts',
482 'test/design-md.test.ts',
483 'test/document-skills-redaction.test.ts',
484 'test/evals-workflow-wiring.test.ts',
485 'test/gbrain-guards.test.ts',
486 'test/global-discover.test.ts',
487 'test/helpers-unit.test.ts',
488 'test/helpers/hermetic-env.test.ts',
489 'test/office-hours-review.test.ts',
490 'test/paid-run-manifest.test.ts',
491 'test/paid-shards.test.ts',
492 'test/plan-count-completion.test.ts',
493 'test/plan-design-floor-fixture.test.ts',
494 'test/pty-skill-seeding-wiring.test.ts',
495 'test/regression-issue2091-bsd-mktemp.test.ts',
496 'test/regression-pr1169-mktemp-fallbacks.test.ts',
497 'test/resolvers-gbrain-save-results.test.ts',
498 'test/skill-preflight-budget.test.ts',
499 'test/takes-fence-fallback.test.ts',
500 'test/terse-build.test.ts',
501 'test/typecheck-test-ratchet.test.ts',
502];
504/**
505 * Returns the first POSIX-only pattern hit in the file, or null if Windows-safe.
506 */
507export function detectWindowsFragility(absolutePath: string): { reason: string } | null {
508 let content: string;
509 try {
510 content = fs.readFileSync(absolutePath, 'utf-8');
511 } catch {
512 return null;
513 }
514 for (const { pattern, reason } of WINDOWS_FRAGILE_PATTERNS) {
515 if (pattern.test(content)) return { reason };
516 }
517 return null;
520export interface CurationResult {
521 safe: string[];
522 excluded: Array<{ file: string; reason: string }>;
525export function curateWindowsSafe(files: string[], rootDir = ROOT): CurationResult {
526 const safe: string[] = [];
527 const excluded: Array<{ file: string; reason: string }> = [];
528 const knownBad = new Map(KNOWN_WINDOWS_INCOMPATIBLE.map((e) => [e.file, e.reason]));
529 const knownSafe = new Set([...KNOWN_WINDOWS_SAFE.map((e) => e.file), ...WINDOWS_PROBE_SAFE]);
530 for (const relativePath of files) {
531 const knownReason = knownBad.get(relativePath);
532 if (knownReason) {
533 excluded.push({ file: relativePath, reason: knownReason });
534 continue;
535 }
536 if (knownSafe.has(relativePath)) {
537 safe.push(relativePath);
538 continue;
539 }
540 const absolute = path.join(rootDir, relativePath);
541 const fragility = detectWindowsFragility(absolute);
542 if (fragility) {
543 excluded.push({ file: relativePath, reason: fragility.reason });
544 } else {
545 safe.push(relativePath);
546 }
547 }
548 return { safe, excluded };