import { describe, expect, test } from 'vitest'; const loadDocker = async () => { process.env.SPOON_WORKER_TOKEN = 'test-worker-token'; process.env.GITHUB_APP_ID = '123'; process.env.GITHUB_APP_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----'; return await import('../../src/runtime/docker'); }; describe('in-box process-group marker + kill helpers', () => { test('buildMarkedCommand wraps argv as a setsid bash session carrying the marker', async () => { const { buildMarkedCommand } = await loadDocker(); const argv = buildMarkedCommand('spoon-turn-abc', [ 'codex', 'exec', '--json', 'hi', ]); expect(argv[0]).toBe('setsid'); expect(argv[1]).toBe('bash'); expect(argv[2]).toBe('-lc'); const script = argv[3] ?? ''; expect(script).toContain('# spoon-turn-abc'); expect(script).toContain("'codex' 'exec' '--json' 'hi'"); }); test('buildMarkedCommand keeps a trailing exit after the CLI so bash survives tail-exec while propagating the exit code (C1 guard)', async () => { const { buildMarkedCommand } = await loadDocker(); const script = buildMarkedCommand('spoon-turn-abc', ['codex', 'run'])[3] ?? ''; // The CLI must NOT be the final simple command (bash would tail-exec it, // replacing the marker-carrying bash). The exit code must still propagate. expect(script).toContain("'codex' 'run'\nrc=$?\nexit \"$rc\""); expect(script.split('\n').at(-1)).toBe('exit "$rc"'); expect(script.split('\n').at(-1)).not.toBe("'codex' 'run'"); }); test('buildMarkedCommand rejects a marker that could break out of the comment', async () => { const { buildMarkedCommand } = await loadDocker(); expect(() => buildMarkedCommand('bad\nmarker', ['codex'])).toThrow(); }); test('buildKillScript TERM-then-KILLs every group matching the marker', async () => { const { buildKillScript } = await loadDocker(); const script = buildKillScript('spoon-turn-abc'); expect(script).toContain("pgrep -f 'spoon-turn-abc'"); expect(script).toContain('kill -TERM -"$pgid"'); expect(script).toContain('kill -KILL -"$pgid"'); }); test('buildKillScript excludes its own process group so it does not kill itself (C2 guard)', async () => { const { buildKillScript } = await loadDocker(); const script = buildKillScript('spoon-turn-abc'); // Computes its own pgid and skips any match sharing it (the kill script's // own bash + its command-substitution subshells all carry the marker). expect(script).toContain('self_pgid=$(ps -o pgid= -p $$ | tr -d \' \')'); expect(script).toContain('[ "$pgid" = "$self_pgid" ] && continue'); }); test('buildKillScript escalates TERM -> KILL with a sleep 2 in between (order matters)', async () => { const { buildKillScript } = await loadDocker(); const script = buildKillScript('spoon-turn-abc'); const termIdx = script.indexOf('kill -TERM -"$pgid"'); const sleepIdx = script.indexOf('\nsleep 2\n'); const killIdx = script.indexOf('kill -KILL -"$pgid"'); expect(termIdx).toBeGreaterThanOrEqual(0); expect(sleepIdx).toBeGreaterThan(termIdx); expect(killIdx).toBeGreaterThan(sleepIdx); }); test('buildKillScript rejects a marker that could break out of the pgrep pattern', async () => { const { buildKillScript } = await loadDocker(); expect(() => buildKillScript('bad\nmarker')).toThrow(); }); });