diff --git a/src/tools/delegate-task/sync-continuation-deps.ts b/src/tools/delegate-task/sync-continuation-deps.ts new file mode 100644 index 00000000..1ad12f5c --- /dev/null +++ b/src/tools/delegate-task/sync-continuation-deps.ts @@ -0,0 +1,9 @@ +import { pollSyncSession } from "./sync-session-poller" +import { fetchSyncResult } from "./sync-result-fetcher" + +export const syncContinuationDeps = { + pollSyncSession, + fetchSyncResult, +} + +export type SyncContinuationDeps = typeof syncContinuationDeps diff --git a/src/tools/delegate-task/sync-continuation.test.ts b/src/tools/delegate-task/sync-continuation.test.ts index 58ffd58c..0f218519 100644 --- a/src/tools/delegate-task/sync-continuation.test.ts +++ b/src/tools/delegate-task/sync-continuation.test.ts @@ -34,15 +34,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { spyOn(toastManager, "removeTask").mockImplementation((id: string) => { removeTaskCalls.push(id) }) - - //#given - mock other dependencies - mock.module("./sync-session-poller.ts", () => ({ - pollSyncSession: async () => null, - })) - - mock.module("./sync-result-fetcher.ts", () => ({ - fetchSyncResult: async () => ({ ok: true, textContent: "Result" }), - })) }) afterEach(() => { @@ -51,18 +42,12 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { __resetTimingConfig() mock.restore() + resetToastManager?.() resetToastManager = null }) test("removes toast when fetchSyncResult throws", async () => { - //#given - mock fetchSyncResult to throw an error - mock.module("./sync-result-fetcher.ts", () => ({ - fetchSyncResult: async () => { - throw new Error("Network error") - }, - })) - const mockClient = { session: { messages: async () => ({ @@ -83,6 +68,13 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { const { executeSyncContinuation } = require("./sync-continuation") + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => { + throw new Error("Network error") + }, + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -105,7 +97,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { let error: any = null let result: string | null = null try { - result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) + result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) } catch (e) { error = e } @@ -118,13 +110,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { }) test("removes toast when pollSyncSession throws", async () => { - //#given - mock pollSyncSession to throw an error - mock.module("./sync-session-poller.ts", () => ({ - pollSyncSession: async () => { - throw new Error("Poll error") - }, - })) - const mockClient = { session: { messages: async () => ({ @@ -145,6 +130,13 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { const { executeSyncContinuation } = require("./sync-continuation") + const deps = { + pollSyncSession: async () => { + throw new Error("Poll error") + }, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -167,7 +159,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { let error: any = null let result: string | null = null try { - result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) + result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) } catch (e) { error = e } @@ -206,6 +198,11 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { const { executeSyncContinuation } = require("./sync-continuation") + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -225,7 +222,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { } //#when - executeSyncContinuation completes successfully - const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) + const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) //#then - toast should be removed exactly once expect(removeTaskCalls.length).toBe(1) @@ -235,16 +232,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { }) test("removes toast when abort happens", async () => { - //#given - mock pollSyncSession to detect abort and remove toast - mock.module("./sync-session-poller.ts", () => ({ - pollSyncSession: async (ctx: any, client: any, input: any) => { - if (input.toastManager && input.taskId) { - input.toastManager.removeTask(input.taskId) - } - return "Task aborted.\n\nSession ID: ses_test_12345678" - }, - })) - //#given - create a context with abort signal const controller = new AbortController() controller.abort() @@ -269,6 +256,16 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { const { executeSyncContinuation } = require("./sync-continuation") + const deps = { + pollSyncSession: async (_ctx: any, _client: any, input: any) => { + if (input.toastManager && input.taskId) { + input.toastManager.removeTask(input.taskId) + } + return "Task aborted.\n\nSession ID: ses_test_12345678" + }, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -289,7 +286,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { } //#when - executeSyncContinuation with abort signal - const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) + const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) //#then - removeTask should be called at least once (poller and finally may both call it) expect(removeTaskCalls.length).toBeGreaterThanOrEqual(1) @@ -322,6 +319,11 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { const { executeSyncContinuation } = require("./sync-continuation") + const deps = { + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -344,7 +346,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => { let error: any = null let result: string | null = null try { - result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) + result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps) } catch (e) { error = e } diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index a3f70b45..2b811438 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -8,13 +8,13 @@ import { getMessageDir } from "../../shared/session-utils" import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { formatDuration } from "./time-formatter" -import { pollSyncSession } from "./sync-session-poller" -import { fetchSyncResult } from "./sync-result-fetcher" +import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continuation-deps" export async function executeSyncContinuation( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, - executorCtx: ExecutorContext + executorCtx: ExecutorContext, + deps: SyncContinuationDeps = syncContinuationDeps ): Promise { const { client } = executorCtx const toastManager = getTaskToastManager() @@ -102,7 +102,7 @@ export async function executeSyncContinuation( } try { - const pollError = await pollSyncSession(ctx, client, { + const pollError = await deps.pollSyncSession(ctx, client, { sessionID: args.session_id!, agentToUse: resumeAgent ?? "continue", toastManager, @@ -113,10 +113,10 @@ export async function executeSyncContinuation( return pollError } - const result = await fetchSyncResult(client, args.session_id!, anchorMessageCount) - if (!result.ok) { - return result.error - } + const result = await deps.fetchSyncResult(client, args.session_id!, anchorMessageCount) + if (!result.ok) { + return result.error + } const duration = formatDuration(startTime) diff --git a/src/tools/delegate-task/sync-task-deps.ts b/src/tools/delegate-task/sync-task-deps.ts new file mode 100644 index 00000000..ffae77fc --- /dev/null +++ b/src/tools/delegate-task/sync-task-deps.ts @@ -0,0 +1,13 @@ +import { createSyncSession } from "./sync-session-creator" +import { sendSyncPrompt } from "./sync-prompt-sender" +import { pollSyncSession } from "./sync-session-poller" +import { fetchSyncResult } from "./sync-result-fetcher" + +export const syncTaskDeps = { + createSyncSession, + sendSyncPrompt, + pollSyncSession, + fetchSyncResult, +} + +export type SyncTaskDeps = typeof syncTaskDeps diff --git a/src/tools/delegate-task/sync-task.test.ts b/src/tools/delegate-task/sync-task.test.ts index 68466954..7f832ebd 100644 --- a/src/tools/delegate-task/sync-task.test.ts +++ b/src/tools/delegate-task/sync-task.test.ts @@ -48,22 +48,6 @@ describe("executeSyncTask - cleanup on error paths", () => { deleteCalls.push(id) }) - //#given - mock other dependencies - mock.module("./sync-session-creator.ts", () => ({ - createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), - })) - - mock.module("./sync-prompt-sender.ts", () => ({ - sendSyncPrompt: async () => null, - })) - - mock.module("./sync-session-poller.ts", () => ({ - pollSyncSession: async () => null, - })) - - mock.module("./sync-result-fetcher.ts", () => ({ - fetchSyncResult: async () => ({ ok: true, textContent: "Result" }), - })) }) afterEach(() => { @@ -77,11 +61,6 @@ describe("executeSyncTask - cleanup on error paths", () => { }) test("cleans up toast and subagentSessions when fetchSyncResult returns ok: false", async () => { - //#given - mock fetchSyncResult to return error - mock.module("./sync-result-fetcher.ts", () => ({ - fetchSyncResult: async () => ({ ok: false, error: "Fetch failed" }), - })) - const mockClient = { session: { create: async () => ({ data: { id: "ses_test_12345678" } }), @@ -90,6 +69,13 @@ describe("executeSyncTask - cleanup on error paths", () => { const { executeSyncTask } = require("./sync-task") + const deps = { + createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), + sendSyncPrompt: async () => null, + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: false as const, error: "Fetch failed" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -114,7 +100,7 @@ describe("executeSyncTask - cleanup on error paths", () => { //#when - executeSyncTask with fetchSyncResult failing const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { sessionID: "parent-session", - }, "test-agent", undefined, undefined) + }, "test-agent", undefined, undefined, undefined, deps) //#then - should return error and cleanup resources expect(result).toBe("Fetch failed") @@ -125,11 +111,6 @@ describe("executeSyncTask - cleanup on error paths", () => { }) test("cleans up toast and subagentSessions when pollSyncSession returns error", async () => { - //#given - mock pollSyncSession to return error - mock.module("./sync-session-poller.ts", () => ({ - pollSyncSession: async () => "Poll error", - })) - const mockClient = { session: { create: async () => ({ data: { id: "ses_test_12345678" } }), @@ -138,6 +119,13 @@ describe("executeSyncTask - cleanup on error paths", () => { const { executeSyncTask } = require("./sync-task") + const deps = { + createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), + sendSyncPrompt: async () => null, + pollSyncSession: async () => "Poll error", + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -162,7 +150,7 @@ describe("executeSyncTask - cleanup on error paths", () => { //#when - executeSyncTask with pollSyncSession failing const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { sessionID: "parent-session", - }, "test-agent", undefined, undefined) + }, "test-agent", undefined, undefined, undefined, deps) //#then - should return error and cleanup resources expect(result).toBe("Poll error") @@ -181,6 +169,13 @@ describe("executeSyncTask - cleanup on error paths", () => { const { executeSyncTask } = require("./sync-task") + const deps = { + createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), + sendSyncPrompt: async () => null, + pollSyncSession: async () => null, + fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), + } + const mockCtx = { sessionID: "parent-session", callID: "call-123", @@ -205,7 +200,7 @@ describe("executeSyncTask - cleanup on error paths", () => { //#when - executeSyncTask completes successfully const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { sessionID: "parent-session", - }, "test-agent", undefined, undefined) + }, "test-agent", undefined, undefined, undefined, deps) //#then - should complete and cleanup resources expect(result).toContain("Task completed") @@ -214,4 +209,4 @@ describe("executeSyncTask - cleanup on error paths", () => { expect(deleteCalls.length).toBe(1) expect(deleteCalls[0]).toBe("ses_test_12345678") }) -}) \ No newline at end of file +}) diff --git a/src/tools/delegate-task/sync-task.ts b/src/tools/delegate-task/sync-task.ts index 5f602c3b..dfc2f43a 100644 --- a/src/tools/delegate-task/sync-task.ts +++ b/src/tools/delegate-task/sync-task.ts @@ -7,10 +7,7 @@ import { subagentSessions } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" import { formatDuration } from "./time-formatter" import { formatDetailedError } from "./error-formatting" -import { createSyncSession } from "./sync-session-creator" -import { sendSyncPrompt } from "./sync-prompt-sender" -import { pollSyncSession } from "./sync-session-poller" -import { fetchSyncResult } from "./sync-result-fetcher" +import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps" export async function executeSyncTask( args: DelegateTaskArgs, @@ -20,7 +17,8 @@ export async function executeSyncTask( agentToUse: string, categoryModel: { providerID: string; modelID: string; variant?: string } | undefined, systemContent: string | undefined, - modelInfo?: ModelFallbackInfo + modelInfo?: ModelFallbackInfo, + deps: SyncTaskDeps = syncTaskDeps ): Promise { const { client, directory, onSyncSessionCreated } = executorCtx const toastManager = getTaskToastManager() @@ -28,7 +26,7 @@ export async function executeSyncTask( let syncSessionID: string | undefined try { - const createSessionResult = await createSyncSession(client, { + const createSessionResult = await deps.createSyncSession(client, { parentSessionID: parentContext.sessionID, agentToUse, description: args.description, @@ -89,7 +87,7 @@ export async function executeSyncTask( storeToolMetadata(ctx.sessionID, ctx.callID, syncTaskMeta) } - const promptError = await sendSyncPrompt(client, { + const promptError = await deps.sendSyncPrompt(client, { sessionID, agentToUse, args, @@ -103,7 +101,7 @@ export async function executeSyncTask( } try { - const pollError = await pollSyncSession(ctx, client, { + const pollError = await deps.pollSyncSession(ctx, client, { sessionID, agentToUse, toastManager, @@ -113,7 +111,7 @@ export async function executeSyncTask( return pollError } - const result = await fetchSyncResult(client, sessionID) + const result = await deps.fetchSyncResult(client, sessionID) if (!result.ok) { return result.error }