diff --git a/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts b/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts new file mode 100644 index 000000000..b456daf30 --- /dev/null +++ b/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts @@ -0,0 +1,82 @@ +/** + * Regression test for #3696 — prompt step ordering and runtime fixes + * + * 1. complete-milestone.md: gsd_requirement_update (step 9) before + * gsd_complete_milestone (step 10) + * 2. complete-slice.md: uses gsd_requirement_update + * 3. register-extension.ts: _gsdEpipeGuard logs instead of re-throwing + * 4. register-hooks.ts: session_before_compact only checks isAutoActive + */ + +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const completeMilestoneMd = readFileSync( + join(__dirname, '..', 'prompts', 'complete-milestone.md'), + 'utf-8', +); +const completeSliceMd = readFileSync( + join(__dirname, '..', 'prompts', 'complete-slice.md'), + 'utf-8', +); +const registerExtSrc = readFileSync( + join(__dirname, '..', 'bootstrap', 'register-extension.ts'), + 'utf-8', +); +const registerHooksSrc = readFileSync( + join(__dirname, '..', 'bootstrap', 'register-hooks.ts'), + 'utf-8', +); + +describe('prompt step ordering (#3696)', () => { + test('gsd_requirement_update appears before gsd_complete_milestone', () => { + const reqUpdateIdx = completeMilestoneMd.indexOf('gsd_requirement_update'); + const completeMilestoneIdx = completeMilestoneMd.indexOf('gsd_complete_milestone'); + assert.ok(reqUpdateIdx > -1, 'gsd_requirement_update should be in complete-milestone.md'); + assert.ok(completeMilestoneIdx > -1, 'gsd_complete_milestone should be in complete-milestone.md'); + assert.ok( + reqUpdateIdx < completeMilestoneIdx, + 'gsd_requirement_update (step 9) must come before gsd_complete_milestone (step 10)', + ); + }); + + test('complete-slice.md uses gsd_requirement_update', () => { + assert.match(completeSliceMd, /gsd_requirement_update/, + 'complete-slice.md should reference gsd_requirement_update'); + }); +}); + +describe('register-extension _gsdEpipeGuard (#3696)', () => { + test('_gsdEpipeGuard exists and does not re-throw', () => { + assert.match(registerExtSrc, /_gsdEpipeGuard/, + '_gsdEpipeGuard should be defined in register-extension.ts'); + // After the fix, the handler logs instead of throwing + assert.ok( + !registerExtSrc.includes('throw err'), + '_gsdEpipeGuard should NOT contain "throw err"', + ); + }); +}); + +describe('register-hooks session_before_compact (#3696)', () => { + test('session_before_compact only checks isAutoActive', () => { + // Extract the session_before_compact handler + const compactIdx = registerHooksSrc.indexOf('session_before_compact'); + assert.ok(compactIdx > -1, 'session_before_compact hook should exist'); + // The first check in the handler should be isAutoActive(), not isAutoPaused() + const afterCompact = registerHooksSrc.slice(compactIdx, compactIdx + 300); + assert.match(afterCompact, /isAutoActive\(\)/, + 'session_before_compact should check isAutoActive()'); + // Should NOT block compaction when paused + assert.ok( + !afterCompact.includes('isAutoPaused()'), + 'session_before_compact should not check isAutoPaused', + ); + }); +});