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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user