diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index e797486fc..5807cb222 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -5,8 +5,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundTask, ResumeInput } from "./types" import { BackgroundManager } from "./manager" import { ConcurrencyManager } from "./concurrency" -import { TaskStateManager } from "./state" -import { tryCompleteTask as tryCompleteTaskFn } from "./result-handler" const TASK_TTL_MS = 30 * 60 * 1000 @@ -183,33 +181,17 @@ function getConcurrencyManager(manager: BackgroundManager): ConcurrencyManager { } function getTaskMap(manager: BackgroundManager): Map { - return (manager as unknown as { state: { tasks: Map } }).state.tasks -} - -function getManagerInternals(manager: BackgroundManager): { - client: unknown - concurrencyManager: ConcurrencyManager - state: TaskStateManager -} { - return manager as unknown as { - client: unknown - concurrencyManager: ConcurrencyManager - state: TaskStateManager - } + return (manager as unknown as { tasks: Map }).tasks } async function tryCompleteTaskForTest(manager: BackgroundManager, task: BackgroundTask): Promise { - const internals = getManagerInternals(manager) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return tryCompleteTaskFn(task, "test", { - client: internals.client as any, - concurrencyManager: internals.concurrencyManager, - state: internals.state, - }) + return (manager as unknown as { tryCompleteTask: (task: BackgroundTask, source: string) => Promise }) + .tryCompleteTask(task, "test") } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function stubNotifyParentSession(_manager: BackgroundManager): void {} +function stubNotifyParentSession(manager: BackgroundManager): void { + ;(manager as unknown as { notifyParentSession: () => Promise }).notifyParentSession = async () => {} +} function getCleanupSignals(): Array { const signals: Array = ["SIGINT", "SIGTERM", "beforeExit", "exit"] @@ -993,6 +975,7 @@ describe("BackgroundManager.tryCompleteTask", () => { abortedSessionIDs.push(args.path.id) return {} }, + messages: async () => ({ data: [] }), }, } manager.shutdown() @@ -1758,7 +1741,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -1790,7 +1773,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -1805,6 +1788,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) + stubNotifyParentSession(manager) const task: BackgroundTask = { id: "task-3", @@ -1822,7 +1806,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -1840,6 +1824,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 60_000 }) + stubNotifyParentSession(manager) const task: BackgroundTask = { id: "task-4", @@ -1857,7 +1842,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -1873,6 +1858,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) + stubNotifyParentSession(manager) const task: BackgroundTask = { id: "task-5", @@ -1891,7 +1877,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { concurrencyKey: "test-agent", } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -1907,6 +1893,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) + stubNotifyParentSession(manager) const task1: BackgroundTask = { id: "task-6", @@ -1940,8 +1927,8 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task1.id, task1) - manager["state"].tasks.set(task2.id, task2) + getTaskMap(manager).set(task1.id, task1) + getTaskMap(manager).set(task2.id, task2) await manager["checkAndInterruptStaleTasks"]() @@ -1957,6 +1944,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + stubNotifyParentSession(manager) const task: BackgroundTask = { id: "task-8", @@ -1974,7 +1962,7 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }, } - manager["state"].tasks.set(task.id, task) + getTaskMap(manager).set(task.id, task) await manager["checkAndInterruptStaleTasks"]() @@ -2143,7 +2131,7 @@ describe("BackgroundManager.shutdown session abort", () => { describe("BackgroundManager.completionTimers - Memory Leak Fix", () => { function getCompletionTimers(manager: BackgroundManager): Map> { - return (manager as unknown as { state: { completionTimers: Map> } }).state.completionTimers + return (manager as unknown as { completionTimers: Map> }).completionTimers } function setCompletionTimer(manager: BackgroundManager, taskId: string): void { diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index 60dbd9fdc..109ed3de9 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" -import { createAtlasHook } from "./index" +import { randomUUID } from "node:crypto" import { writeBoulderState, clearBoulderState, @@ -10,11 +10,22 @@ import { } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" +const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-message-storage-${randomUUID()}`) +const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") +const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part") + +mock.module("../../features/hook-message-injector/constants", () => ({ + OPENCODE_STORAGE: TEST_STORAGE_ROOT, + MESSAGE_STORAGE: TEST_MESSAGE_STORAGE, + PART_STORAGE: TEST_PART_STORAGE, +})) + +const { createAtlasHook } = await import("./index") +const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") describe("atlas hook", () => { - const TEST_DIR = join(tmpdir(), "atlas-test-" + Date.now()) - const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus") + let TEST_DIR: string + let SISYPHUS_DIR: string function createMockPluginInput(overrides?: { promptMock?: ReturnType }) { const promptMock = overrides?.promptMock ?? mock(() => Promise.resolve()) @@ -49,6 +60,8 @@ describe("atlas hook", () => { } beforeEach(() => { + TEST_DIR = join(tmpdir(), `atlas-test-${randomUUID()}`) + SISYPHUS_DIR = join(TEST_DIR, ".sisyphus") if (!existsSync(TEST_DIR)) { mkdirSync(TEST_DIR, { recursive: true }) } @@ -63,6 +76,7 @@ describe("atlas hook", () => { if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }) } + rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true }) }) describe("tool.execute.after handler", () => { diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index ac058b606..dea1a15d5 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -1,11 +1,24 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" -import { createPrometheusMdOnlyHook } from "./index" -import { MESSAGE_STORAGE } from "../../features/hook-message-injector" -import { SYSTEM_DIRECTIVE_PREFIX, createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" +import { tmpdir } from "node:os" +import { randomUUID } from "node:crypto" +import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" import { clearSessionAgent } from "../../features/claude-code-session-state" +const TEST_STORAGE_ROOT = join(tmpdir(), `prometheus-md-only-${randomUUID()}`) +const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") +const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part") + +mock.module("../../features/hook-message-injector/constants", () => ({ + OPENCODE_STORAGE: TEST_STORAGE_ROOT, + MESSAGE_STORAGE: TEST_MESSAGE_STORAGE, + PART_STORAGE: TEST_PART_STORAGE, +})) + +const { createPrometheusMdOnlyHook } = await import("./index") +const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") + describe("prometheus-md-only", () => { const TEST_SESSION_ID = "test-session-prometheus" let testMessageDir: string @@ -39,6 +52,7 @@ describe("prometheus-md-only", () => { // ignore } } + rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true }) }) describe("with Prometheus agent in message storage", () => { @@ -342,7 +356,7 @@ describe("prometheus-md-only", () => { describe("cross-platform path validation", () => { beforeEach(() => { - setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)") + setupMessageStorage(TEST_SESSION_ID, "prometheus") }) test("should allow Windows-style backslash paths under .sisyphus/", async () => { diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts index 679107640..e633e85a9 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import { tmpdir, homedir } from "node:os" +import { randomUUID } from "node:crypto" import { createStartWorkHook } from "./index" import { writeBoulderState, @@ -11,30 +12,32 @@ import type { BoulderState } from "../../features/boulder-state" import * as sessionState from "../../features/claude-code-session-state" describe("start-work hook", () => { - const TEST_DIR = join(tmpdir(), "start-work-test-" + Date.now()) - const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus") + let testDir: string + let sisyphusDir: string function createMockPluginInput() { return { - directory: TEST_DIR, + directory: testDir, client: {}, } as Parameters[0] } beforeEach(() => { - if (!existsSync(TEST_DIR)) { - mkdirSync(TEST_DIR, { recursive: true }) + testDir = join(tmpdir(), `start-work-test-${randomUUID()}`) + sisyphusDir = join(testDir, ".sisyphus") + if (!existsSync(testDir)) { + mkdirSync(testDir, { recursive: true }) } - if (!existsSync(SISYPHUS_DIR)) { - mkdirSync(SISYPHUS_DIR, { recursive: true }) + if (!existsSync(sisyphusDir)) { + mkdirSync(sisyphusDir, { recursive: true }) } - clearBoulderState(TEST_DIR) + clearBoulderState(testDir) }) afterEach(() => { - clearBoulderState(TEST_DIR) - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }) + clearBoulderState(testDir) + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }) } }) @@ -80,7 +83,7 @@ describe("start-work hook", () => { test("should inject resume info when existing boulder state found", async () => { // given - existing boulder state with incomplete plan - const planPath = join(TEST_DIR, "test-plan.md") + const planPath = join(testDir, "test-plan.md") writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2") const state: BoulderState = { @@ -89,7 +92,7 @@ describe("start-work hook", () => { session_ids: ["session-1"], plan_name: "test-plan", } - writeBoulderState(TEST_DIR, state) + writeBoulderState(testDir, state) const hook = createStartWorkHook(createMockPluginInput()) const output = { @@ -155,7 +158,7 @@ describe("start-work hook", () => { test("should auto-select when only one incomplete plan among multiple plans", async () => { // given - multiple plans but only one incomplete - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) // Plan 1: complete (all checked) @@ -185,7 +188,7 @@ describe("start-work hook", () => { test("should wrap multiple plans message in system-reminder tag", async () => { // given - multiple incomplete plans - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) const plan1Path = join(plansDir, "plan-a.md") @@ -213,7 +216,7 @@ describe("start-work hook", () => { test("should use 'ask user' prompt style for multiple plans", async () => { // given - multiple incomplete plans - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) const plan1Path = join(plansDir, "plan-x.md") @@ -240,7 +243,7 @@ describe("start-work hook", () => { test("should select explicitly specified plan name from user-request, ignoring existing boulder state", async () => { // given - existing boulder state pointing to old plan - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) // Old plan (in boulder state) @@ -258,7 +261,7 @@ describe("start-work hook", () => { session_ids: ["old-session"], plan_name: "old-plan", } - writeBoulderState(TEST_DIR, staleState) + writeBoulderState(testDir, staleState) const hook = createStartWorkHook(createMockPluginInput()) const output = { @@ -286,7 +289,7 @@ describe("start-work hook", () => { test("should strip ultrawork/ulw keywords from plan name argument", async () => { // given - plan with ultrawork keyword in user-request - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) const planPath = join(plansDir, "my-feature-plan.md") @@ -317,7 +320,7 @@ describe("start-work hook", () => { test("should strip ulw keyword from plan name argument", async () => { // given - plan with ulw keyword in user-request - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) const planPath = join(plansDir, "api-refactor.md") @@ -348,7 +351,7 @@ describe("start-work hook", () => { test("should match plan by partial name", async () => { // given - user specifies partial plan name - const plansDir = join(TEST_DIR, ".sisyphus", "plans") + const plansDir = join(testDir, ".sisyphus", "plans") mkdirSync(plansDir, { recursive: true }) const planPath = join(plansDir, "2026-01-15-feature-implementation.md") diff --git a/src/hooks/todo-continuation-enforcer.test.ts b/src/hooks/todo-continuation-enforcer.test.ts index 8043de5bb..23ae8a777 100644 --- a/src/hooks/todo-continuation-enforcer.test.ts +++ b/src/hooks/todo-continuation-enforcer.test.ts @@ -117,6 +117,8 @@ function createFakeTimers(): FakeTimers { return { advanceBy, restore } } +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + describe("todo-continuation-enforcer", () => { let promptCalls: Array<{ sessionID: string; agent?: string; model?: { providerID?: string; modelID?: string }; text: string }> let toastCalls: Array<{ title: string; message: string }> @@ -187,6 +189,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject continuation when idle with incomplete todos", async () => { + fakeTimers.restore() // given - main session with incomplete todos const sessionID = "main-123" setMainSession(sessionID) @@ -201,15 +204,15 @@ describe("todo-continuation-enforcer", () => { }) // then - countdown toast shown - await fakeTimers.advanceBy(100) + await wait(50) expect(toastCalls.length).toBeGreaterThanOrEqual(1) expect(toastCalls[0].title).toBe("Todo Continuation") // then - after countdown, continuation injected - await fakeTimers.advanceBy(2500) + await wait(2500) expect(promptCalls.length).toBe(1) expect(promptCalls[0].text).toContain("TODO CONTINUATION") - }) + }, { timeout: 15000 }) test("should not inject when all todos are complete", async () => { // given - session with all todos complete @@ -273,6 +276,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject for background task session (subagent)", async () => { + fakeTimers.restore() // given - main session set, background task session registered setMainSession("main-session") const bgTaskSession = "bg-task-session" @@ -286,10 +290,10 @@ describe("todo-continuation-enforcer", () => { }) // then - continuation injected for background task session - await fakeTimers.advanceBy(2500) + await wait(2500) expect(promptCalls.length).toBe(1) expect(promptCalls[0].sessionID).toBe(bgTaskSession) - }) + }, { timeout: 15000 }) @@ -320,6 +324,7 @@ describe("todo-continuation-enforcer", () => { }) test("should ignore user message within grace period", async () => { + fakeTimers.restore() // given - session starting countdown const sessionID = "main-grace" setMainSession(sessionID) @@ -341,9 +346,9 @@ describe("todo-continuation-enforcer", () => { // then - countdown should continue (message was ignored) // wait past 2s countdown and verify injection happens - await fakeTimers.advanceBy(2500) + await wait(2500) expect(promptCalls).toHaveLength(1) - }) + }, { timeout: 15000 }) test("should cancel countdown on assistant activity", async () => { // given - session starting countdown @@ -418,6 +423,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject after recovery complete", async () => { + fakeTimers.restore() // given - session was in recovery, now complete const sessionID = "main-recovery-done" setMainSession(sessionID) @@ -433,11 +439,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(3000) // then - continuation injected expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) test("should cleanup on session deleted", async () => { // given - session starting countdown @@ -483,6 +489,7 @@ describe("todo-continuation-enforcer", () => { }) test("should show countdown toast updates", async () => { + fakeTimers.restore() // given - session with incomplete todos const sessionID = "main-toast" setMainSession(sessionID) @@ -495,10 +502,10 @@ describe("todo-continuation-enforcer", () => { }) // then - multiple toast updates during countdown (2s countdown = 2 toasts: "2s" and "1s") - await fakeTimers.advanceBy(2500) + await wait(2500) expect(toastCalls.length).toBeGreaterThanOrEqual(2) expect(toastCalls[0].message).toContain("2s") - }) + }, { timeout: 15000 }) test("should not have 10s throttle between injections", async () => { // given - new hook instance (no prior state) @@ -533,6 +540,7 @@ describe("todo-continuation-enforcer", () => { test("should NOT skip for non-abort errors even if immediately before idle", async () => { + fakeTimers.restore() // given - session with incomplete todos const sessionID = "main-noabort-error" setMainSession(sessionID) @@ -555,11 +563,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(2500) + await wait(2500) // then - continuation injected (non-abort errors don't block) expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) @@ -595,6 +603,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject when last assistant message has no error", async () => { + fakeTimers.restore() // given - session where last assistant message completed normally const sessionID = "main-api-no-error" setMainSession(sessionID) @@ -611,13 +620,14 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (no abort) expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) test("should inject when last message is from user (not assistant)", async () => { + fakeTimers.restore() // given - session where last message is from user const sessionID = "main-api-user-last" setMainSession(sessionID) @@ -634,11 +644,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (last message is user, not aborted assistant) expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) test("should skip when last assistant message has any abort-like error", async () => { // given - session where last assistant message has AbortError (DOMException style) @@ -724,6 +734,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject when abort flag is stale (>3s old)", async () => { + fakeTimers.restore() // given - session with incomplete todos and old abort timestamp const sessionID = "main-stale-abort" setMainSession(sessionID) @@ -743,19 +754,20 @@ describe("todo-continuation-enforcer", () => { }) // when - wait >3s then idle fires - await fakeTimers.advanceBy(3100, true) + await wait(3100) await hook.handler({ event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(3000) // then - continuation injected (abort flag is stale) expect(promptCalls.length).toBeGreaterThan(0) - }, 10000) + }, { timeout: 15000 }) test("should clear abort flag on user message activity", async () => { + fakeTimers.restore() // given - session with abort detected const sessionID = "main-clear-on-user" setMainSession(sessionID) @@ -775,7 +787,7 @@ describe("todo-continuation-enforcer", () => { }) // when - user sends new message (clears abort flag) - await fakeTimers.advanceBy(600) + await wait(600) await hook.handler({ event: { type: "message.updated", @@ -788,13 +800,14 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (abort flag was cleared by user activity) expect(promptCalls.length).toBeGreaterThan(0) - }) + }, { timeout: 15000 }) test("should clear abort flag on assistant message activity", async () => { + fakeTimers.restore() // given - session with abort detected const sessionID = "main-clear-on-assistant" setMainSession(sessionID) @@ -826,13 +839,14 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (abort flag was cleared by assistant activity) expect(promptCalls.length).toBeGreaterThan(0) - }) + }, { timeout: 15000 }) test("should clear abort flag on tool execution", async () => { + fakeTimers.restore() // given - session with abort detected const sessionID = "main-clear-on-tool" setMainSession(sessionID) @@ -864,11 +878,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (abort flag was cleared by tool execution) expect(promptCalls.length).toBeGreaterThan(0) - }) + }, { timeout: 15000 }) 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 @@ -923,6 +937,7 @@ describe("todo-continuation-enforcer", () => { }) test("should pass model property in prompt call (undefined when no message context)", async () => { + fakeTimers.restore() // given - session with incomplete todos, no prior message context available const sessionID = "main-model-preserve" setMainSession(sessionID) @@ -936,13 +951,13 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(2500) + await wait(2500) // 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) - }) + }, { timeout: 15000 }) 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) @@ -1133,6 +1148,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject when agent info is undefined but skipAgents is empty", async () => { + fakeTimers.restore() // given - session with no agent info but skipAgents is empty const sessionID = "main-no-agent-no-skip" setMainSession(sessionID) @@ -1173,11 +1189,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (no agents to skip) expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) test("should not inject when isContinuationStopped returns true", async () => { // given - session with continuation stopped @@ -1200,6 +1216,7 @@ describe("todo-continuation-enforcer", () => { }) test("should inject when isContinuationStopped returns false", async () => { + fakeTimers.restore() // given - session with continuation not stopped const sessionID = "main-not-stopped" setMainSession(sessionID) @@ -1213,11 +1230,11 @@ describe("todo-continuation-enforcer", () => { event: { type: "session.idle", properties: { sessionID } }, }) - await fakeTimers.advanceBy(3000) + await wait(2500) // then - continuation injected (stopped flag is false) expect(promptCalls.length).toBe(1) - }) + }, { timeout: 15000 }) test("should cancel all countdowns via cancelAllCountdowns", async () => { // given - multiple sessions with running countdowns diff --git a/src/tools/session-manager/storage.test.ts b/src/tools/session-manager/storage.test.ts index 5cc1bc8d8..76507867a 100644 --- a/src/tools/session-manager/storage.test.ts +++ b/src/tools/session-manager/storage.test.ts @@ -2,8 +2,9 @@ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test" import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" +import { randomUUID } from "node:crypto" -const TEST_DIR = join(tmpdir(), "omo-test-session-manager") +const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`) const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message") const TEST_PART_STORAGE = join(TEST_DIR, "part") const TEST_SESSION_STORAGE = join(TEST_DIR, "session")