From 9de8be3df1320dfc5eccc815946f9f754f07ff1f Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 6 Oct 2026 20:40:32 +0000 Subject: [PATCH] test(windows): name the B2, B4, B5 and B12 tests in the curated Windows lane The runtime-root bundle tests lived in browse/test/config.test.ts and the Copilot glossary test in test/host-config.test.ts, both excluded from the Windows lane, so the two Windows-only fixes were never checked on Windows. Move each into its own Windows-safe file (unchanged assertions) and list it, the piped-stdin chain test and the /cso docker.exe discovery test as named Windows coverage in scripts/lib/windows-curation.ts. --- browse/test/config.test.ts | 80 ----------------- .../test/runtime-root-server-bundle.test.ts | 89 +++++++++++++++++++ scripts/lib/windows-curation.ts | 6 ++ test/copilot-windows-bash.test.ts | 17 ++++ test/host-config.test.ts | 10 --- 5 files changed, 112 insertions(+), 90 deletions(-) create mode 100644 browse/test/runtime-root-server-bundle.test.ts create mode 100644 test/copilot-windows-bash.test.ts diff --git a/browse/test/config.test.ts b/browse/test/config.test.ts index e5028a592..53a2081a6 100644 --- a/browse/test/config.test.ts +++ b/browse/test/config.test.ts @@ -297,86 +297,6 @@ describe('resolveNodeServerScript', () => { }); }); -describe('#3026: a Windows runtime root without node_modules runs the source checkout bundle', () => { - const { resolveNodeServerScript } = require('../src/cli'); - const fs = require('fs'); - const os = require('os'); - - function layout(opts: { runtimeModules: boolean; sourcePath: boolean; sourceModules: boolean }) { - const base = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-runtime-root-')); - const source = path.join(base, 'gstack'); - const runtime = path.join(base, 'home', '.codex', 'skills', 'gstack'); - for (const dir of [source, runtime]) { - fs.mkdirSync(path.join(dir, 'browse', 'dist'), { recursive: true }); - fs.writeFileSync(path.join(dir, 'browse', 'dist', 'server-node.mjs'), ''); - } - const playwright = (root: string) => { - fs.mkdirSync(path.join(root, 'node_modules', 'playwright'), { recursive: true }); - fs.writeFileSync(path.join(root, 'node_modules', 'playwright', 'package.json'), '{}'); - }; - if (opts.sourceModules) playwright(source); - if (opts.runtimeModules) playwright(runtime); - if (opts.sourcePath) fs.writeFileSync(path.join(runtime, '.source-path'), `${source}\n`); - return { base, source, runtime, exe: path.join(runtime, 'browse', 'dist', 'browse.exe') }; - } - - test('no reachable playwright beside the copy -> the checkout bundle named by .source-path', () => { - const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); - try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); } - finally { fs.rmSync(l.base, { recursive: true, force: true }); } - }); - - test('a runtime root that reaches playwright keeps its own bundle', () => { - const l = layout({ runtimeModules: true, sourcePath: true, sourceModules: true }); - try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.runtime, 'browse', 'dist', 'server-node.mjs')); } - finally { fs.rmSync(l.base, { recursive: true, force: true }); } - }); - - test('without .source-path, or a checkout without node_modules, the adjacent bundle is unchanged', () => { - for (const opts of [{ runtimeModules: false, sourcePath: false, sourceModules: true }, { runtimeModules: false, sourcePath: true, sourceModules: false }]) { - const l = layout(opts); - try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.runtime, 'browse', 'dist', 'server-node.mjs')); } - finally { fs.rmSync(l.base, { recursive: true, force: true }); } - } - }); - - test('same build on both sides -> the checkout bundle; different builds -> refusal with the ./setup fix and anchor', () => { - const { BROWSE_VERSION_SKEW_ANCHOR } = require('../src/cli'); - const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); - const version = (root: string, v: string) => fs.writeFileSync(path.join(root, 'browse', 'dist', '.version'), `${v}\n`); - try { - version(l.runtime, 'a'.repeat(40)); - version(l.source, 'a'.repeat(40)); - expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); - version(l.source, 'b'.repeat(40)); - let message = ''; - try { resolveNodeServerScript('/$bunfs/root', l.exe); } catch (err: any) { message = err.message; } - expect(message).toContain(`build ${'a'.repeat(12)}`); - expect(message).toContain(`build ${'b'.repeat(12)}`); - expect(message).toContain('are from different builds, so the server was not started'); - expect(message).toContain(`Fix: cd "${l.source}" && ./setup`); - expect(message).toContain(BROWSE_VERSION_SKEW_ANCHOR); - expect(BROWSE_VERSION_SKEW_ANCHOR).toBe('https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#browse-runtime-version-skew'); - // resolveServerLaunch surfaces the same refusal on Windows. - expect(() => require('../src/cli').resolveServerLaunch('win32', {}, '/$bunfs/root', l.exe)).toThrow('different builds'); - } finally { fs.rmSync(l.base, { recursive: true, force: true }); } - }); - - test('an unversioned build on either side is not called skew', () => { - const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); - try { - fs.writeFileSync(path.join(l.runtime, 'browse', 'dist', '.version'), 'abc\n'); - expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); - } finally { fs.rmSync(l.base, { recursive: true, force: true }); } - }); - - test('setup records the source checkout in every runtime root it links', () => { - const setup = fs.readFileSync(path.resolve(__dirname, '../../setup'), 'utf8'); - const fn = setup.slice(setup.indexOf('_link_runtime_dists() {'), setup.indexOf('\n}\n', setup.indexOf('_link_runtime_dists() {'))); - expect(fn).toContain(`printf '%s\\n' "$1" > "$2/.source-path"`); - }); -}); - describe('version mismatch detection', () => { test('detects when versions differ', () => { const stateVersion = 'abc123'; diff --git a/browse/test/runtime-root-server-bundle.test.ts b/browse/test/runtime-root-server-bundle.test.ts new file mode 100644 index 000000000..584e2d9c8 --- /dev/null +++ b/browse/test/runtime-root-server-bundle.test.ts @@ -0,0 +1,89 @@ +/** + * A Windows host runtime root (~/.codex/skills/gstack and the other env-var + * hosts) holds a copy of browse/dist with no node_modules, so its CLI starts + * the source checkout's Node server bundle named by .source-path, and refuses + * when the two come from different builds. Windows-safe (os.tmpdir paths + * only), so the curated Windows lane runs it natively. + */ +import { describe, expect, test } from 'bun:test'; +import * as path from 'path'; + +describe('#3026: a Windows runtime root without node_modules runs the source checkout bundle', () => { + const { resolveNodeServerScript } = require('../src/cli'); + const fs = require('fs'); + const os = require('os'); + + function layout(opts: { runtimeModules: boolean; sourcePath: boolean; sourceModules: boolean }) { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-runtime-root-')); + const source = path.join(base, 'gstack'); + const runtime = path.join(base, 'home', '.codex', 'skills', 'gstack'); + for (const dir of [source, runtime]) { + fs.mkdirSync(path.join(dir, 'browse', 'dist'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'browse', 'dist', 'server-node.mjs'), ''); + } + const playwright = (root: string) => { + fs.mkdirSync(path.join(root, 'node_modules', 'playwright'), { recursive: true }); + fs.writeFileSync(path.join(root, 'node_modules', 'playwright', 'package.json'), '{}'); + }; + if (opts.sourceModules) playwright(source); + if (opts.runtimeModules) playwright(runtime); + if (opts.sourcePath) fs.writeFileSync(path.join(runtime, '.source-path'), `${source}\n`); + return { base, source, runtime, exe: path.join(runtime, 'browse', 'dist', 'browse.exe') }; + } + + test('no reachable playwright beside the copy -> the checkout bundle named by .source-path', () => { + const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); + try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); } + finally { fs.rmSync(l.base, { recursive: true, force: true }); } + }); + + test('a runtime root that reaches playwright keeps its own bundle', () => { + const l = layout({ runtimeModules: true, sourcePath: true, sourceModules: true }); + try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.runtime, 'browse', 'dist', 'server-node.mjs')); } + finally { fs.rmSync(l.base, { recursive: true, force: true }); } + }); + + test('without .source-path, or a checkout without node_modules, the adjacent bundle is unchanged', () => { + for (const opts of [{ runtimeModules: false, sourcePath: false, sourceModules: true }, { runtimeModules: false, sourcePath: true, sourceModules: false }]) { + const l = layout(opts); + try { expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.runtime, 'browse', 'dist', 'server-node.mjs')); } + finally { fs.rmSync(l.base, { recursive: true, force: true }); } + } + }); + + test('same build on both sides -> the checkout bundle; different builds -> refusal with the ./setup fix and anchor', () => { + const { BROWSE_VERSION_SKEW_ANCHOR } = require('../src/cli'); + const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); + const version = (root: string, v: string) => fs.writeFileSync(path.join(root, 'browse', 'dist', '.version'), `${v}\n`); + try { + version(l.runtime, 'a'.repeat(40)); + version(l.source, 'a'.repeat(40)); + expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); + version(l.source, 'b'.repeat(40)); + let message = ''; + try { resolveNodeServerScript('/$bunfs/root', l.exe); } catch (err: any) { message = err.message; } + expect(message).toContain(`build ${'a'.repeat(12)}`); + expect(message).toContain(`build ${'b'.repeat(12)}`); + expect(message).toContain('are from different builds, so the server was not started'); + expect(message).toContain(`Fix: cd "${l.source}" && ./setup`); + expect(message).toContain(BROWSE_VERSION_SKEW_ANCHOR); + expect(BROWSE_VERSION_SKEW_ANCHOR).toBe('https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#browse-runtime-version-skew'); + // resolveServerLaunch surfaces the same refusal on Windows. + expect(() => require('../src/cli').resolveServerLaunch('win32', {}, '/$bunfs/root', l.exe)).toThrow('different builds'); + } finally { fs.rmSync(l.base, { recursive: true, force: true }); } + }); + + test('an unversioned build on either side is not called skew', () => { + const l = layout({ runtimeModules: false, sourcePath: true, sourceModules: true }); + try { + fs.writeFileSync(path.join(l.runtime, 'browse', 'dist', '.version'), 'abc\n'); + expect(resolveNodeServerScript('/$bunfs/root', l.exe)).toBe(path.join(l.source, 'browse', 'dist', 'server-node.mjs')); + } finally { fs.rmSync(l.base, { recursive: true, force: true }); } + }); + + test('setup records the source checkout in every runtime root it links', () => { + const setup = fs.readFileSync(path.resolve(__dirname, '../../setup'), 'utf8'); + const fn = setup.slice(setup.indexOf('_link_runtime_dists() {'), setup.indexOf('\n}\n', setup.indexOf('_link_runtime_dists() {'))); + expect(fn).toContain(`printf '%s\\n' "$1" > "$2/.source-path"`); + }); +}); diff --git a/scripts/lib/windows-curation.ts b/scripts/lib/windows-curation.ts index f358412d8..661cc8d56 100644 --- a/scripts/lib/windows-curation.ts +++ b/scripts/lib/windows-curation.ts @@ -225,6 +225,12 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> // pattern hit is a false positive — the point of these files is Windows // coverage, so auto-excluding them defeats the regression tests they carry. export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [ + // Named Windows coverage for fixes whose bug only bites on Windows: kept in + // the curated lane even if a future edit trips a content pattern. + { 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' }, + { 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' }, + { 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' }, + { 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' }, { 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' }, { 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' }, { file: 'test/generator-eexist.test.ts', diff --git a/test/copilot-windows-bash.test.ts b/test/copilot-windows-bash.test.ts new file mode 100644 index 000000000..4b95026dc --- /dev/null +++ b/test/copilot-windows-bash.test.ts @@ -0,0 +1,17 @@ +/** + * Copilot CLI on Windows ran gstack's extensionless helpers straight from + * PowerShell. The Copilot tool glossary (rendered into every Copilot skill) + * names Git for Windows Bash by full path. Kept out of host-config.test.ts, + * which the curated Windows lane excludes, so Windows runs this check. + */ +import { describe, expect, test } from 'bun:test'; + +describe('#3047: Copilot on Windows runs bash blocks in Git for Windows Bash', () => { + test('the Copilot tool glossary names Git Bash by full path and forbids PowerShell translation', async () => { + const { default: copilot } = await import('../hosts/copilot'); + const glossary = JSON.stringify(copilot.toolRewrites); + expect(glossary).toContain('Git for Windows Bash by its full path'); + expect(glossary).toContain('bare `bash` can start WSL'); + expect(glossary).toContain('Never translate a block into PowerShell'); + }); +}); diff --git a/test/host-config.test.ts b/test/host-config.test.ts index 7a20b3146..f1f17526f 100644 --- a/test/host-config.test.ts +++ b/test/host-config.test.ts @@ -790,13 +790,3 @@ describe('host renders name the host\'s own tools and identities (#2626, #2015, } }); }); - -describe('#3047: Copilot on Windows runs bash blocks in Git for Windows Bash', () => { - test('the Copilot tool glossary names Git Bash by full path and forbids PowerShell translation', async () => { - const { default: copilot } = await import('../hosts/copilot'); - const glossary = JSON.stringify(copilot.toolRewrites); - expect(glossary).toContain('Git for Windows Bash by its full path'); - expect(glossary).toContain('bare `bash` can start WSL'); - expect(glossary).toContain('Never translate a block into PowerShell'); - }); -});