refactor: major codebase cleanup - BDD comments, file splitting, bug fixes (#1350)

* style(tests): normalize BDD comments from '// #given' to '// given'

- Replace 4,668 Python-style BDD comments across 107 test files
- Patterns changed: // #given -> // given, // #when -> // when, // #then -> // then
- Also handles no-space variants: //#given -> // given

* fix(rules-injector): prefer output.metadata.filePath over output.title

- Extract file path resolution to dedicated output-path.ts module
- Prefer metadata.filePath which contains actual file path
- Fall back to output.title only when metadata unavailable
- Fixes issue where rules weren't injected when tool output title was a label

* feat(slashcommand): add optional user_message parameter

- Add user_message optional parameter for command arguments
- Model can now call: command='publish' user_message='patch'
- Improves error messages with clearer format guidance
- Helps LLMs understand correct parameter usage

* feat(hooks): restore compaction-context-injector hook

- Restore hook deleted in cbbc7bd0 for session compaction context
- Injects 7 mandatory sections: User Requests, Final Goal, Work Completed,
  Remaining Tasks, Active Working Context, MUST NOT Do, Agent Verification State
- Re-register in hooks/index.ts and main plugin entry

* refactor(background-agent): split manager.ts into focused modules

- Extract constants.ts for TTL values and internal types (52 lines)
- Extract state.ts for TaskStateManager class (204 lines)
- Extract spawner.ts for task creation logic (244 lines)
- Extract result-handler.ts for completion handling (265 lines)
- Reduce manager.ts from 1377 to 755 lines (45% reduction)
- Maintain backward compatible exports

* refactor(agents): split prometheus-prompt.ts into subdirectory

- Move 1196-line prometheus-prompt.ts to prometheus/ subdirectory
- Organize prompt sections into separate files for maintainability
- Update agents/index.ts exports

* refactor(delegate-task): split tools.ts into focused modules

- Extract categories.ts for category definitions and routing
- Extract executor.ts for task execution logic
- Extract helpers.ts for utility functions
- Extract prompt-builder.ts for prompt construction
- Reduce tools.ts complexity with cleaner separation of concerns

* refactor(builtin-skills): split skills.ts into individual skill files

- Move each skill to dedicated file in skills/ subdirectory
- Create barrel export for backward compatibility
- Improve maintainability with focused skill modules

* chore: update import paths and lockfile

- Update prometheus import path after refactor
- Update bun.lock

* fix(tests): complete BDD comment normalization

- Fix remaining #when/#then patterns missed by initial sed
- Affected: state.test.ts, events.test.ts

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
YeonGyu-Kim
2026-02-01 16:47:50 +09:00
committed by GitHub
parent c83150d9ea
commit f146aeff0f
145 changed files with 10307 additions and 9562 deletions
+134 -134
View File
@@ -187,7 +187,7 @@ describe("todo-continuation-enforcer", () => {
})
test("should inject continuation when idle with incomplete todos", async () => {
// #given - main session with incomplete todos
// given - main session with incomplete todos
const sessionID = "main-123"
setMainSession(sessionID)
@@ -195,24 +195,24 @@ describe("todo-continuation-enforcer", () => {
backgroundManager: createMockBackgroundManager(false),
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #then - countdown toast shown
// then - countdown toast shown
await fakeTimers.advanceBy(100)
expect(toastCalls.length).toBeGreaterThanOrEqual(1)
expect(toastCalls[0].title).toBe("Todo Continuation")
// #then - after countdown, continuation injected
// then - after countdown, continuation injected
await fakeTimers.advanceBy(2500)
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).toContain("TODO CONTINUATION")
})
test("should not inject when all todos are complete", async () => {
// #given - session with all todos complete
// given - session with all todos complete
const sessionID = "main-456"
setMainSession(sessionID)
@@ -223,19 +223,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(mockInput, {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls).toHaveLength(0)
})
test("should not inject when background tasks are running", async () => {
// #given - session with running background tasks
// given - session with running background tasks
const sessionID = "main-789"
setMainSession(sessionID)
@@ -243,49 +243,49 @@ describe("todo-continuation-enforcer", () => {
backgroundManager: createMockBackgroundManager(true),
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls).toHaveLength(0)
})
test("should not inject for non-main session", async () => {
// #given - main session set, different session goes idle
// given - main session set, different session goes idle
setMainSession("main-session")
const otherSession = "other-session"
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - non-main session goes idle
// when - non-main session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID: otherSession } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls).toHaveLength(0)
})
test("should inject for background task session (subagent)", async () => {
// #given - main session set, background task session registered
// given - main session set, background task session registered
setMainSession("main-session")
const bgTaskSession = "bg-task-session"
subagentSessions.add(bgTaskSession)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - background task session goes idle
// when - background task session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID: bgTaskSession } },
})
// #then - continuation injected for background task session
// then - continuation injected for background task session
await fakeTimers.advanceBy(2500)
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].sessionID).toBe(bgTaskSession)
@@ -294,18 +294,18 @@ describe("todo-continuation-enforcer", () => {
test("should cancel countdown on user message after grace period", async () => {
// #given - session starting countdown
// given - session starting countdown
const sessionID = "main-cancel"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #when - wait past grace period (500ms), then user sends message
// when - wait past grace period (500ms), then user sends message
await fakeTimers.advanceBy(600, true)
await hook.handler({
event: {
@@ -314,24 +314,24 @@ describe("todo-continuation-enforcer", () => {
},
})
// #then - wait past countdown time and verify no injection (countdown was cancelled)
// then - wait past countdown time and verify no injection (countdown was cancelled)
await fakeTimers.advanceBy(2500)
expect(promptCalls).toHaveLength(0)
})
test("should ignore user message within grace period", async () => {
// #given - session starting countdown
// given - session starting countdown
const sessionID = "main-grace"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #when - user message arrives within grace period (immediately)
// when - user message arrives within grace period (immediately)
await hook.handler({
event: {
type: "message.updated",
@@ -339,25 +339,25 @@ describe("todo-continuation-enforcer", () => {
},
})
// #then - countdown should continue (message was ignored)
// then - countdown should continue (message was ignored)
// wait past 2s countdown and verify injection happens
await fakeTimers.advanceBy(2500)
expect(promptCalls).toHaveLength(1)
})
test("should cancel countdown on assistant activity", async () => {
// #given - session starting countdown
// given - session starting countdown
const sessionID = "main-assistant"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #when - assistant starts responding
// when - assistant starts responding
await fakeTimers.advanceBy(500)
await hook.handler({
event: {
@@ -368,23 +368,23 @@ describe("todo-continuation-enforcer", () => {
await fakeTimers.advanceBy(3000)
// #then - no continuation injected (cancelled)
// then - no continuation injected (cancelled)
expect(promptCalls).toHaveLength(0)
})
test("should cancel countdown on tool execution", async () => {
// #given - session starting countdown
// given - session starting countdown
const sessionID = "main-tool"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #when - tool starts executing
// when - tool starts executing
await fakeTimers.advanceBy(500)
await hook.handler({
event: { type: "tool.execute.before", properties: { sessionID } },
@@ -392,66 +392,66 @@ describe("todo-continuation-enforcer", () => {
await fakeTimers.advanceBy(3000)
// #then - no continuation injected (cancelled)
// then - no continuation injected (cancelled)
expect(promptCalls).toHaveLength(0)
})
test("should skip injection during recovery mode", async () => {
// #given - session in recovery mode
// given - session in recovery mode
const sessionID = "main-recovery"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - mark as recovering
// when - mark as recovering
hook.markRecovering(sessionID)
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation injected
// then - no continuation injected
expect(promptCalls).toHaveLength(0)
})
test("should inject after recovery complete", async () => {
// #given - session was in recovery, now complete
// given - session was in recovery, now complete
const sessionID = "main-recovery-done"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - mark as recovering then complete
// when - mark as recovering then complete
hook.markRecovering(sessionID)
hook.markRecoveryComplete(sessionID)
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected
// then - continuation injected
expect(promptCalls.length).toBe(1)
})
test("should cleanup on session deleted", async () => {
// #given - session starting countdown
// given - session starting countdown
const sessionID = "main-delete"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #when - session is deleted during countdown
// when - session is deleted during countdown
await fakeTimers.advanceBy(500)
await hook.handler({
event: { type: "session.deleted", properties: { info: { id: sessionID } } },
@@ -459,21 +459,21 @@ describe("todo-continuation-enforcer", () => {
await fakeTimers.advanceBy(3000)
// #then - no continuation injected (cleaned up)
// then - no continuation injected (cleaned up)
expect(promptCalls).toHaveLength(0)
})
test("should accept skipAgents option without error", async () => {
// #given - session with skipAgents configured for Prometheus
// given - session with skipAgents configured for Prometheus
const sessionID = "main-prometheus-option"
setMainSession(sessionID)
// #when - create hook with skipAgents option (should not throw)
// when - create hook with skipAgents option (should not throw)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {
skipAgents: ["Prometheus (Planner)", "custom-agent"],
})
// #then - handler works without error
// then - handler works without error
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
@@ -483,46 +483,46 @@ describe("todo-continuation-enforcer", () => {
})
test("should show countdown toast updates", async () => {
// #given - session with incomplete todos
// given - session with incomplete todos
const sessionID = "main-toast"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
// #then - multiple toast updates during countdown (2s countdown = 2 toasts: "2s" and "1s")
// then - multiple toast updates during countdown (2s countdown = 2 toasts: "2s" and "1s")
await fakeTimers.advanceBy(2500)
expect(toastCalls.length).toBeGreaterThanOrEqual(2)
expect(toastCalls[0].message).toContain("2s")
})
test("should not have 10s throttle between injections", async () => {
// #given - new hook instance (no prior state)
// given - new hook instance (no prior state)
const sessionID = "main-no-throttle"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - first idle cycle completes
// when - first idle cycle completes
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3500)
// #then - first injection happened
// then - first injection happened
expect(promptCalls.length).toBe(1)
// #when - immediately trigger second idle (no 10s wait needed)
// when - immediately trigger second idle (no 10s wait needed)
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3500)
// #then - second injection also happened (no throttle blocking)
// then - second injection also happened (no throttle blocking)
expect(promptCalls.length).toBe(2)
}, { timeout: 15000 })
@@ -533,13 +533,13 @@ describe("todo-continuation-enforcer", () => {
test("should NOT skip for non-abort errors even if immediately before idle", async () => {
// #given - session with incomplete todos
// given - session with incomplete todos
const sessionID = "main-noabort-error"
setMainSession(sessionID)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - non-abort error occurs (e.g., network error, API error)
// when - non-abort error occurs (e.g., network error, API error)
await hook.handler({
event: {
type: "session.error",
@@ -550,14 +550,14 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle immediately after
// when - session goes idle immediately after
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(2500)
// #then - continuation injected (non-abort errors don't block)
// then - continuation injected (non-abort errors don't block)
expect(promptCalls.length).toBe(1)
})
@@ -572,7 +572,7 @@ describe("todo-continuation-enforcer", () => {
// ============================================================
test("should skip injection when last assistant message has MessageAbortedError", async () => {
// #given - session where last assistant message was aborted
// given - session where last assistant message was aborted
const sessionID = "main-api-abort"
setMainSession(sessionID)
@@ -583,19 +583,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (last message was aborted)
// then - no continuation (last message was aborted)
expect(promptCalls).toHaveLength(0)
})
test("should inject when last assistant message has no error", async () => {
// #given - session where last assistant message completed normally
// given - session where last assistant message completed normally
const sessionID = "main-api-no-error"
setMainSession(sessionID)
@@ -606,19 +606,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (no abort)
// then - continuation injected (no abort)
expect(promptCalls.length).toBe(1)
})
test("should inject when last message is from user (not assistant)", async () => {
// #given - session where last message is from user
// given - session where last message is from user
const sessionID = "main-api-user-last"
setMainSession(sessionID)
@@ -629,19 +629,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (last message is user, not aborted assistant)
// then - continuation injected (last message is user, not aborted assistant)
expect(promptCalls.length).toBe(1)
})
test("should skip when last assistant message has any abort-like error", async () => {
// #given - session where last assistant message has AbortError (DOMException style)
// given - session where last assistant message has AbortError (DOMException style)
const sessionID = "main-api-abort-dom"
setMainSession(sessionID)
@@ -652,19 +652,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (abort error detected)
// then - no continuation (abort error detected)
expect(promptCalls).toHaveLength(0)
})
test("should skip injection when abort detected via session.error event (event-based, primary)", async () => {
// #given - session with incomplete todos
// given - session with incomplete todos
const sessionID = "main-event-abort"
setMainSession(sessionID)
mockMessages = [
@@ -674,7 +674,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error event fires
// when - abort error event fires
await hook.handler({
event: {
type: "session.error",
@@ -682,19 +682,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle immediately after
// when - session goes idle immediately after
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (abort detected via event)
// then - no continuation (abort detected via event)
expect(promptCalls).toHaveLength(0)
})
test("should skip injection when AbortError detected via session.error event", async () => {
// #given - session with incomplete todos
// given - session with incomplete todos
const sessionID = "main-event-abort-dom"
setMainSession(sessionID)
mockMessages = [
@@ -704,7 +704,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - AbortError event fires
// when - AbortError event fires
await hook.handler({
event: {
type: "session.error",
@@ -712,19 +712,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (abort detected via event)
// then - no continuation (abort detected via event)
expect(promptCalls).toHaveLength(0)
})
test("should inject when abort flag is stale (>3s old)", async () => {
// #given - session with incomplete todos and old abort timestamp
// given - session with incomplete todos and old abort timestamp
const sessionID = "main-stale-abort"
setMainSession(sessionID)
mockMessages = [
@@ -734,7 +734,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error fires
// when - abort error fires
await hook.handler({
event: {
type: "session.error",
@@ -742,7 +742,7 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - wait >3s then idle fires
// when - wait >3s then idle fires
await fakeTimers.advanceBy(3100, true)
await hook.handler({
@@ -751,12 +751,12 @@ describe("todo-continuation-enforcer", () => {
await fakeTimers.advanceBy(3000)
// #then - continuation injected (abort flag is stale)
// then - continuation injected (abort flag is stale)
expect(promptCalls.length).toBeGreaterThan(0)
}, 10000)
test("should clear abort flag on user message activity", async () => {
// #given - session with abort detected
// given - session with abort detected
const sessionID = "main-clear-on-user"
setMainSession(sessionID)
mockMessages = [
@@ -766,7 +766,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error fires
// when - abort error fires
await hook.handler({
event: {
type: "session.error",
@@ -774,7 +774,7 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - user sends new message (clears abort flag)
// when - user sends new message (clears abort flag)
await fakeTimers.advanceBy(600)
await hook.handler({
event: {
@@ -783,19 +783,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (abort flag was cleared by user activity)
// then - continuation injected (abort flag was cleared by user activity)
expect(promptCalls.length).toBeGreaterThan(0)
})
test("should clear abort flag on assistant message activity", async () => {
// #given - session with abort detected
// given - session with abort detected
const sessionID = "main-clear-on-assistant"
setMainSession(sessionID)
mockMessages = [
@@ -805,7 +805,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error fires
// when - abort error fires
await hook.handler({
event: {
type: "session.error",
@@ -813,7 +813,7 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - assistant starts responding (clears abort flag)
// when - assistant starts responding (clears abort flag)
await hook.handler({
event: {
type: "message.updated",
@@ -821,19 +821,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (abort flag was cleared by assistant activity)
// then - continuation injected (abort flag was cleared by assistant activity)
expect(promptCalls.length).toBeGreaterThan(0)
})
test("should clear abort flag on tool execution", async () => {
// #given - session with abort detected
// given - session with abort detected
const sessionID = "main-clear-on-tool"
setMainSession(sessionID)
mockMessages = [
@@ -843,7 +843,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error fires
// when - abort error fires
await hook.handler({
event: {
type: "session.error",
@@ -851,7 +851,7 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - tool executes (clears abort flag)
// when - tool executes (clears abort flag)
await hook.handler({
event: {
type: "tool.execute.before",
@@ -859,19 +859,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (abort flag was cleared by tool execution)
// then - continuation injected (abort flag was cleared by tool execution)
expect(promptCalls.length).toBeGreaterThan(0)
})
test("should use event-based detection even when API indicates no abort (event wins)", async () => {
// #given - session with abort event but API shows no error
// given - session with abort event but API shows no error
const sessionID = "main-event-wins"
setMainSession(sessionID)
mockMessages = [
@@ -881,7 +881,7 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - abort error event fires (but API doesn't have it yet)
// when - abort error event fires (but API doesn't have it yet)
await hook.handler({
event: {
type: "session.error",
@@ -889,19 +889,19 @@ describe("todo-continuation-enforcer", () => {
},
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (event-based detection wins over API)
// then - no continuation (event-based detection wins over API)
expect(promptCalls).toHaveLength(0)
})
test("should use API fallback when event is missed but API shows abort", async () => {
// #given - session where event was missed but API shows abort
// given - session where event was missed but API shows abort
const sessionID = "main-api-fallback"
setMainSession(sessionID)
mockMessages = [
@@ -911,19 +911,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - session goes idle without prior session.error event
// when - session goes idle without prior session.error event
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (API fallback detected the abort)
// then - no continuation (API fallback detected the abort)
expect(promptCalls).toHaveLength(0)
})
test("should pass model property in prompt call (undefined when no message context)", async () => {
// #given - session with incomplete todos, no prior message context available
// given - session with incomplete todos, no prior message context available
const sessionID = "main-model-preserve"
setMainSession(sessionID)
@@ -931,21 +931,21 @@ describe("todo-continuation-enforcer", () => {
backgroundManager: createMockBackgroundManager(false),
})
// #when - session goes idle and continuation is injected
// when - session goes idle and continuation is injected
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(2500)
// #then - prompt call made, model is undefined when no context (expected behavior)
// then - prompt call made, model is undefined when no context (expected behavior)
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].text).toContain("TODO CONTINUATION")
expect("model" in promptCalls[0]).toBe(true)
})
test("should extract model from assistant message with flat modelID/providerID", async () => {
// #given - session with assistant message that has flat modelID/providerID (OpenCode API format)
// given - session with assistant message that has flat modelID/providerID (OpenCode API format)
const sessionID = "main-assistant-model"
setMainSession(sessionID)
@@ -981,11 +981,11 @@ describe("todo-continuation-enforcer", () => {
backgroundManager: createMockBackgroundManager(false),
})
// #when - session goes idle
// when - session goes idle
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await fakeTimers.advanceBy(2500)
// #then - model should be extracted from assistant message's flat modelID/providerID
// then - model should be extracted from assistant message's flat modelID/providerID
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
})
@@ -997,7 +997,7 @@ describe("todo-continuation-enforcer", () => {
// ============================================================
test("should skip compaction agent messages when resolving agent info", async () => {
// #given - session where last message is from compaction agent but previous was Sisyphus
// given - session where last message is from compaction agent but previous was Sisyphus
const sessionID = "main-compaction-filter"
setMainSession(sessionID)
@@ -1033,17 +1033,17 @@ describe("todo-continuation-enforcer", () => {
backgroundManager: createMockBackgroundManager(false),
})
// #when - session goes idle
// when - session goes idle
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await fakeTimers.advanceBy(2500)
// #then - continuation uses Sisyphus (skipped compaction agent)
// then - continuation uses Sisyphus (skipped compaction agent)
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].agent).toBe("sisyphus")
})
test("should skip injection when only compaction agent messages exist", async () => {
// #given - session with only compaction agent (post-compaction, no prior agent info)
// given - session with only compaction agent (post-compaction, no prior agent info)
const sessionID = "main-only-compaction"
setMainSession(sessionID)
@@ -1075,19 +1075,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(mockInput, {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (compaction is in default skipAgents)
// then - no continuation (compaction is in default skipAgents)
expect(promptCalls).toHaveLength(0)
})
test("should skip injection when prometheus agent is after compaction", async () => {
// #given - prometheus session that was compacted
// given - prometheus session that was compacted
const sessionID = "main-prometheus-compacted"
setMainSession(sessionID)
@@ -1121,19 +1121,19 @@ describe("todo-continuation-enforcer", () => {
const hook = createTodoContinuationEnforcer(mockInput, {})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation (prometheus found after filtering compaction, prometheus is in skipAgents)
// then - no continuation (prometheus found after filtering compaction, prometheus is in skipAgents)
expect(promptCalls).toHaveLength(0)
})
test("should inject when agent info is undefined but skipAgents is empty", async () => {
// #given - session with no agent info but skipAgents is empty
// given - session with no agent info but skipAgents is empty
const sessionID = "main-no-agent-no-skip"
setMainSession(sessionID)
@@ -1168,19 +1168,19 @@ describe("todo-continuation-enforcer", () => {
skipAgents: [],
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (no agents to skip)
// then - continuation injected (no agents to skip)
expect(promptCalls.length).toBe(1)
})
test("should not inject when isContinuationStopped returns true", async () => {
// #given - session with continuation stopped
// given - session with continuation stopped
const sessionID = "main-stopped"
setMainSession(sessionID)
@@ -1188,19 +1188,19 @@ describe("todo-continuation-enforcer", () => {
isContinuationStopped: (id) => id === sessionID,
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - no continuation injected (stopped flag is true)
// then - no continuation injected (stopped flag is true)
expect(promptCalls).toHaveLength(0)
})
test("should inject when isContinuationStopped returns false", async () => {
// #given - session with continuation not stopped
// given - session with continuation not stopped
const sessionID = "main-not-stopped"
setMainSession(sessionID)
@@ -1208,38 +1208,38 @@ describe("todo-continuation-enforcer", () => {
isContinuationStopped: () => false,
})
// #when - session goes idle
// when - session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID } },
})
await fakeTimers.advanceBy(3000)
// #then - continuation injected (stopped flag is false)
// then - continuation injected (stopped flag is false)
expect(promptCalls.length).toBe(1)
})
test("should cancel all countdowns via cancelAllCountdowns", async () => {
// #given - multiple sessions with running countdowns
// given - multiple sessions with running countdowns
const session1 = "main-cancel-all-1"
const session2 = "main-cancel-all-2"
setMainSession(session1)
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
// #when - first session goes idle
// when - first session goes idle
await hook.handler({
event: { type: "session.idle", properties: { sessionID: session1 } },
})
await fakeTimers.advanceBy(500)
// #when - cancel all countdowns
// when - cancel all countdowns
hook.cancelAllCountdowns()
// #when - advance past countdown time
// when - advance past countdown time
await fakeTimers.advanceBy(3000)
// #then - no continuation injected (all countdowns cancelled)
// then - no continuation injected (all countdowns cancelled)
expect(promptCalls).toHaveLength(0)
})
})