From 3fa37ba225a226785101b9b1a19d71ca04b9485a Mon Sep 17 00:00:00 2001 From: garrytan Date: Fri, 2 Oct 2026 21:18:26 +0000 Subject: [PATCH] fix(windows): LF for Dockerfiles, normalized home-guard exclusions, path.relative fixture containment, ship push test spawn timeouts --- .gitattributes | 4 ++++ scripts/lib/free-home-guard.ts | 2 +- test/helpers/install-fixture.ts | 13 +++++++++---- test/ship-plan-completion-invariants.test.ts | 10 +++++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index 10a4ff7fd..65dc0f77a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,6 +24,10 @@ setup text eol=lf bin/* text eol=lf **/scripts/* text eol=lf +# Dockerfiles build on Linux, and test/ci-image-cli-pin.test.ts joins their +# `\`-continued RUN lines with a `\\\n` match; CRLF left each RUN split. +Dockerfile* text eol=lf + # TypeScript/JavaScript: LF for portability across the bun toolchain. *.ts text eol=lf *.tsx text eol=lf diff --git a/scripts/lib/free-home-guard.ts b/scripts/lib/free-home-guard.ts index 981a85336..69b4bfd17 100644 --- a/scripts/lib/free-home-guard.ts +++ b/scripts/lib/free-home-guard.ts @@ -38,7 +38,7 @@ export function freeHomeDir(env: NodeJS.ProcessEnv = process.env): string { export function snapshotFreeHome(home: string): FreeHomeSnapshot { const entries: FreeHomeSnapshot = new Map(); - const volatile = new Set(FREE_HOME_VOLATILE); + const volatile = new Set(FREE_HOME_VOLATILE.map(entry => path.normalize(entry))); const visit = (relative: string, depth: number) => { if (volatile.has(relative)) return; const absolute = path.join(home, relative); diff --git a/test/helpers/install-fixture.ts b/test/helpers/install-fixture.ts index 2c427e730..78fd6fb22 100644 --- a/test/helpers/install-fixture.ts +++ b/test/helpers/install-fixture.ts @@ -13,7 +13,7 @@ import { spawnSync, type SpawnSyncReturns } from 'node:child_process'; import { createHash } from 'node:crypto'; import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; export const ROOT = resolve(import.meta.dir, '../..'); const listed = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: ROOT, encoding: 'utf8', timeout: 10_000 }); @@ -25,13 +25,18 @@ export interface Fixture { dir: string; home: string; commands: string; env: Rec const roots: string[] = []; +/** `target` is `base` or below it, compared by the platform's own path rules (separators, Windows drive case). */ +const within = (base: string, target: string) => { + const rel = relative(base, target); + return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)); +}; + /** Write inside the fixture only (never through a link out of it). */ export function put(file: string, content: string | Buffer, mode = 0o644) { - const root = roots.find(r => file.startsWith(r + '/')); + const root = roots.find(r => file !== r && within(r, file)); if (!root) throw new Error(`fixture write outside a fixture root: ${file}`); mkdirSync(dirname(file), { recursive: true }); - const parent = realpathSync(dirname(file)); - if (parent !== realpathSync(root) && !parent.startsWith(realpathSync(root) + '/')) throw new Error(`fixture write escapes its root: ${file}`); + if (!within(realpathSync(root), realpathSync(dirname(file)))) throw new Error(`fixture write escapes its root: ${file}`); writeFileSync(file, content, { mode }); } diff --git a/test/ship-plan-completion-invariants.test.ts b/test/ship-plan-completion-invariants.test.ts index e6bf1a55d..885807761 100644 --- a/test/ship-plan-completion-invariants.test.ts +++ b/test/ship-plan-completion-invariants.test.ts @@ -239,7 +239,7 @@ test('push idempotency requires the live remote SHA and fails closed on transpor GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.invalid', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.invalid' }; const git = (...args: string[]) => { - const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 5000 }); + const r = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 30_000 }); if (r.status !== 0) throw new Error(r.stderr || String(r.error)); }; try { @@ -251,7 +251,11 @@ test('push idempotency requires the live remote SHA and fails closed on transpor const source = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md.tmpl'), 'utf8'); const block = source.slice(source.indexOf('**Idempotency check:** Check if the branch')) .match(/```bash\n([\s\S]*?)\n```/)![1].replaceAll('', 'feature'); - const inspect = () => spawnSync('bash', ['-c', block], { cwd, env, encoding: 'utf8', timeout: 5000 }); + const inspect = () => { + const r = spawnSync('bash', ['-c', block], { cwd, env, encoding: 'utf8', timeout: 30_000 }); + if (r.error) throw new Error(`idempotency check did not run: ${r.error.message}; stderr: ${r.stderr}`); + return r; + }; expect(inspect().stdout).toContain('PUSH_NEEDED'); git('push', '-u', 'origin', 'feature'); expect(inspect().stdout).toContain('ALREADY_PUSHED'); @@ -264,4 +268,4 @@ test('push idempotency requires the live remote SHA and fails closed on transpor expect(unavailable.stdout).not.toContain('ALREADY_PUSHED'); expect(unavailable.stdout).toContain('BLOCKED'); } finally { fs.rmSync(cwd, { recursive: true, force: true }); } -}); +}, 120_000);