diff --git a/test/gstack-capy-install.test.ts b/test/gstack-capy-install.test.ts index e801a18f4..3baf26a36 100644 --- a/test/gstack-capy-install.test.ts +++ b/test/gstack-capy-install.test.ts @@ -477,6 +477,31 @@ describe('gstack-capy-install: launch PATH, Bun reuse, CI pins, setup skip, verd fs.chmodSync(dir, 0o755); } }); + + // protects: when the launch file's temp file IS created and the final mv fails, write_launch_file + // removes its own temp file and nothing else (a sibling temp file from a concurrent run survives), + // the run says so, prints no GSTACK_LAUNCH_FILE line and keeps the earlier launch file, exit code + // and trailer. fails_when: the `|| { rm -f "$tmp"; return 1; }` cleanup goes (the temp file stays), + // or a glob cleanup returns (the sibling disappears). why_existing_misses: the unwritable-directory + // case above fails at mktemp, before any temp file exists. seam: an mv shim first on PATH that + // refuses only the launch-file target. + test('a launch-file move that fails after the temp file exists removes only that temp file', () => { + const f = fixture(); + const first = run(f, ['--for', 'autoplan']); + expect(first.status, first.stdout + first.stderr).toBe(0); + const launchFile = first.stdout.match(/^GSTACK_LAUNCH_FILE: (.+)$/m)?.[1] ?? ''; + const before = fs.readFileSync(launchFile, 'utf8'); + const sibling = `${launchFile}.tmp.OTHER1`; + fs.writeFileSync(sibling, ''); + script(path.join(f.stub, 'mv'), [`for a in "$@"; do [ "$a" = "${launchFile}" ] && exit 1; done`, 'exec /bin/mv "$@"']); + const c = run(f, ['--check', '--for', 'autoplan']); + expect(c.status, c.stdout + c.stderr).toBe(0); + expect(c.stdout).toContain(`gstack-capy-install: could not write the launch file ${launchFile}`); + expect(c.stdout).not.toMatch(/^GSTACK_LAUNCH_FILE:/m); + expect(lastLine(c.stdout)).toBe('gstack: ok 1.2.3 for=autoplan'); + expect(fs.readdirSync(path.dirname(launchFile)).filter(n => n.includes('.tmp.'))).toEqual([path.basename(sibling)]); + expect(fs.readFileSync(launchFile, 'utf8')).toBe(before); + }); }); // Plan A1/A3/A5/A6/A-f: install and --check resolve through one contract, diff --git a/test/launch-env-echo-ratchet.test.ts b/test/launch-env-echo-ratchet.test.ts index f6bc6dcf8..c74f6b159 100644 --- a/test/launch-env-echo-ratchet.test.ts +++ b/test/launch-env-echo-ratchet.test.ts @@ -111,13 +111,13 @@ function markdownTargets(): string[] { /^capy\/skill\/gstack\/[^/]+\.md$/.test(rel)); } -/** Every shell helper under bin/, the setup script and the install scripts callers run first. */ +/** Every shell helper under bin/, the setup script and the scripts/*.sh build and doctor scripts. */ function shellTargets(): string[] { const candidates = [...fs.readdirSync(path.join(ROOT, 'bin')).map(name => path.join('bin', name)), 'setup', ...fs.readdirSync(path.join(ROOT, 'scripts')).filter(name => name.endsWith('.sh')).map(name => path.join('scripts', name))]; return candidates.filter(rel => { const full = path.join(ROOT, rel); - return fs.existsSync(full) && fs.statSync(full).isFile() && isShell(rel, fs.readFileSync(full, 'utf8')); + return fs.statSync(full).isFile() && isShell(rel, fs.readFileSync(full, 'utf8')); }); } @@ -164,7 +164,7 @@ describe('launch-env echo ratchet', () => { for (const want of ['ship/SKILL.md', 'ship/sections/documentation.md', 'review/SKILL.md', 'qa/SKILL.md', 'document-release/SKILL.md', 'autoplan/DRIVER.md', 'capy/skill/gstack/SKILL.md']) expect(md).toContain(want); const sh = shellTargets(); - for (const want of ['bin/gstack-capy-install', 'bin/gstack-capy-session', 'bin/gstack-skill-start']) expect(sh).toContain(want); + for (const want of ['bin/gstack-capy-install', 'bin/gstack-capy-session', 'bin/gstack-skill-start', 'setup', 'scripts/build.sh', 'scripts/sandbox-doctor.sh']) expect(sh).toContain(want); }); test('no skill fence prints the environment', () => { @@ -172,7 +172,7 @@ describe('launch-env echo ratchet', () => { expect(violations.map(v => `${v.file}:${v.line} [${v.rule}] ${v.text}`)).toEqual([]); }); - test('no shell helper under bin/ prints the environment', () => { + test('no shell helper under bin/, setup or install script prints the environment', () => { const violations = shellTargets().flatMap(file => scanText(file, fs.readFileSync(path.join(ROOT, file), 'utf8'), 'whole')); expect(violations.map(v => `${v.file}:${v.line} [${v.rule}] ${v.text}`)).toEqual([]); });