diff --git a/changelog.d/gsta208-ship-receipt-session-kind.md b/changelog.d/gsta208-ship-receipt-session-kind.md index 2de4746d9..e8873cb0f 100644 --- a/changelog.d/gsta208-ship-receipt-session-kind.md +++ b/changelog.d/gsta208-ship-receipt-session-kind.md @@ -4,4 +4,4 @@ - **`bin/gstack-ship-receipt`** starts `gstack-session-kind` with `runScriptSync`. ### Tests -- `test/gstack-ship-receipt.test.ts`: a receipt written without a session kind takes it from `gstack-session-kind`. +- `test/ship-receipt-session-kind.test.ts` (windows-free-tests too): a receipt written without a session kind records what `gstack-session-kind` prints. diff --git a/scripts/lib/windows-curation.ts b/scripts/lib/windows-curation.ts index 04e37cac3..d0ba7ee73 100644 --- a/scripts/lib/windows-curation.ts +++ b/scripts/lib/windows-curation.ts @@ -405,6 +405,7 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [ { 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' }, { 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' }, { 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' }, + { file: 'test/ship-receipt-session-kind.test.ts', reason: '#3541: runs gstack-ship-receipt write through lib/script-invocation.ts, which must reach the bash gstack-session-kind through Git Bash; os.tmpdir git fixture only' }, { 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' }, { 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/gstack-ship-receipt.test.ts b/test/gstack-ship-receipt.test.ts index 9eddd5710..7e0eb68c8 100644 --- a/test/gstack-ship-receipt.test.ts +++ b/test/gstack-ship-receipt.test.ts @@ -72,21 +72,6 @@ describe('gstack-ship-receipt write', () => { expect(rewritten).toContain('## Summary'); }); - test('without --session-kind or GSTACK_SESSION_KIND, session_kind comes from gstack-session-kind (#3541)', () => { - const repo = fixture(); - makePr(repo, 'pr-a', { expects: '1.2.4.0' }); - expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok); - repo.git('commit', '-qam', 'stamp'); - const body = path.join(repo.root, 'body.md'); - fs.writeFileSync(body, '## Summary\n\nProse.\n'); - const env = { GSTACK_SESSION_KIND: '' }; - const kind = runBin(repo, 'gstack-session-kind', [], env); - expect(kind.status, kind.err).toBe(0); - const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '44', '--base', 'main', '--body', body, '--json'], env); - expect(w.status, w.err).toBe(EXIT.ok); - expect(JSON.parse(w.out).session_kind).toBe(kind.out.trim()); - }); - test('refuses an invalid receipt before writing (RECEIPT_INVALID) and a non-integer --pr', () => { const repo = fixture(); const body = path.join(repo.root, 'body.md'); diff --git a/test/ship-receipt-session-kind.test.ts b/test/ship-receipt-session-kind.test.ts new file mode 100644 index 000000000..48defdd94 --- /dev/null +++ b/test/ship-receipt-session-kind.test.ts @@ -0,0 +1,56 @@ +/** + * #3541 (the /ship half): `gstack-ship-receipt write` with no --session-kind + * and no GSTACK_SESSION_KIND asks bin/gstack-session-kind, a bash script. + * Spawned by path it never starts on Windows (no shebangs), so every receipt + * written there carried no session_kind. + * + * Windows-safe by construction: both bins launch through + * lib/script-invocation.ts (Git Bash on win32), state lives under os.tmpdir(). + */ +import { afterEach, beforeEach, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { runScriptSync } from '../lib/script-invocation'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +let tmp = '', repo = '', env: NodeJS.ProcessEnv = {}; + +beforeEach(() => { + tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-receipt-kind-'))); + repo = path.join(tmp, 'repo'); + fs.mkdirSync(repo); + const gitconfig = path.join(tmp, 'gitconfig'); + fs.writeFileSync(gitconfig, ''); + env = { + ...process.env, HOME: tmp, USERPROFILE: tmp, GSTACK_STATE_ROOT: path.join(tmp, 'state'), GSTACK_HOME: path.join(tmp, 'state'), GSTACK_SESSION_KIND: '', + GIT_CONFIG_GLOBAL: gitconfig, GIT_CONFIG_NOSYSTEM: '1', GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined, + }; + const git = (...args: string[]) => { + const r = spawnSync('git', args, { cwd: repo, env, encoding: 'utf8', timeout: 30_000 }); + expect(r.status, r.stderr).toBe(0); + }; + git('init', '-q', '-b', 'main'); + fs.writeFileSync(path.join(repo, 'a.txt'), 'a\n'); + git('add', 'a.txt'); + git('-c', 'user.email=fixture@example.test', '-c', 'user.name=Fixture', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture'); + git('update-ref', 'refs/remotes/origin/main', 'HEAD'); + git('switch', '-q', '-c', 'feat'); +}); + +afterEach(() => fs.rmSync(tmp, { recursive: true, force: true })); + +test('write without a session kind records what gstack-session-kind prints (#3541)', () => { + const kind = runScriptSync(path.join(ROOT, 'bin', 'gstack-session-kind'), [], { cwd: repo, env, encoding: 'utf8', timeout: 30_000 }); + expect(kind.status, kind.stderr).toBe(0); + const body = path.join(tmp, 'body.md'); + fs.writeFileSync(body, '## Summary\n'); + const w = runScriptSync(path.join(ROOT, 'bin', 'gstack-ship-receipt'), ['write', '--pr', '1', '--base', 'main', '--body', body, '--json'], { + cwd: repo, env, encoding: 'utf8', timeout: 120_000, + }); + expect(w.error).toBeUndefined(); + expect(w.status, w.stderr).toBe(0); + expect(JSON.parse(w.stdout).session_kind).toBe(kind.stdout.trim()); +}, 150_000);