refactor(delegate-task): inject sync task deps for test isolation

This commit is contained in:
YeonGyu-Kim 2026-02-10 22:54:30 +09:00
parent 967058fe3d
commit 087ce06055
6 changed files with 102 additions and 85 deletions

View File

@ -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

View File

@ -34,15 +34,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
spyOn(toastManager, "removeTask").mockImplementation((id: string) => { spyOn(toastManager, "removeTask").mockImplementation((id: string) => {
removeTaskCalls.push(id) 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(() => { afterEach(() => {
@ -51,18 +42,12 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
__resetTimingConfig() __resetTimingConfig()
mock.restore() mock.restore()
resetToastManager?.() resetToastManager?.()
resetToastManager = null resetToastManager = null
}) })
test("removes toast when fetchSyncResult throws", async () => { 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 = { const mockClient = {
session: { session: {
messages: async () => ({ messages: async () => ({
@ -83,6 +68,13 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
const { executeSyncContinuation } = require("./sync-continuation") const { executeSyncContinuation } = require("./sync-continuation")
const deps = {
pollSyncSession: async () => null,
fetchSyncResult: async () => {
throw new Error("Network error")
},
}
const mockCtx = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -105,7 +97,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
let error: any = null let error: any = null
let result: string | null = null let result: string | null = null
try { try {
result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
} catch (e) { } catch (e) {
error = e error = e
} }
@ -118,13 +110,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
}) })
test("removes toast when pollSyncSession throws", async () => { 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 = { const mockClient = {
session: { session: {
messages: async () => ({ messages: async () => ({
@ -145,6 +130,13 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
const { executeSyncContinuation } = require("./sync-continuation") const { executeSyncContinuation } = require("./sync-continuation")
const deps = {
pollSyncSession: async () => {
throw new Error("Poll error")
},
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
}
const mockCtx = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -167,7 +159,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
let error: any = null let error: any = null
let result: string | null = null let result: string | null = null
try { try {
result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
} catch (e) { } catch (e) {
error = e error = e
} }
@ -206,6 +198,11 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
const { executeSyncContinuation } = require("./sync-continuation") const { executeSyncContinuation } = require("./sync-continuation")
const deps = {
pollSyncSession: async () => null,
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
}
const mockCtx = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -225,7 +222,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
} }
//#when - executeSyncContinuation completes successfully //#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 //#then - toast should be removed exactly once
expect(removeTaskCalls.length).toBe(1) expect(removeTaskCalls.length).toBe(1)
@ -235,16 +232,6 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
}) })
test("removes toast when abort happens", async () => { 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 //#given - create a context with abort signal
const controller = new AbortController() const controller = new AbortController()
controller.abort() controller.abort()
@ -269,6 +256,16 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
const { executeSyncContinuation } = require("./sync-continuation") 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 = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -289,7 +286,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
} }
//#when - executeSyncContinuation with abort signal //#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) //#then - removeTask should be called at least once (poller and finally may both call it)
expect(removeTaskCalls.length).toBeGreaterThanOrEqual(1) expect(removeTaskCalls.length).toBeGreaterThanOrEqual(1)
@ -322,6 +319,11 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
const { executeSyncContinuation } = require("./sync-continuation") const { executeSyncContinuation } = require("./sync-continuation")
const deps = {
pollSyncSession: async () => null,
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
}
const mockCtx = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -344,7 +346,7 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
let error: any = null let error: any = null
let result: string | null = null let result: string | null = null
try { try {
result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx) result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
} catch (e) { } catch (e) {
error = e error = e
} }

View File

@ -8,13 +8,13 @@ import { getMessageDir } from "../../shared/session-utils"
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { findNearestMessageWithFields } from "../../features/hook-message-injector"
import { formatDuration } from "./time-formatter" import { formatDuration } from "./time-formatter"
import { pollSyncSession } from "./sync-session-poller" import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continuation-deps"
import { fetchSyncResult } from "./sync-result-fetcher"
export async function executeSyncContinuation( export async function executeSyncContinuation(
args: DelegateTaskArgs, args: DelegateTaskArgs,
ctx: ToolContextWithMetadata, ctx: ToolContextWithMetadata,
executorCtx: ExecutorContext executorCtx: ExecutorContext,
deps: SyncContinuationDeps = syncContinuationDeps
): Promise<string> { ): Promise<string> {
const { client } = executorCtx const { client } = executorCtx
const toastManager = getTaskToastManager() const toastManager = getTaskToastManager()
@ -102,7 +102,7 @@ export async function executeSyncContinuation(
} }
try { try {
const pollError = await pollSyncSession(ctx, client, { const pollError = await deps.pollSyncSession(ctx, client, {
sessionID: args.session_id!, sessionID: args.session_id!,
agentToUse: resumeAgent ?? "continue", agentToUse: resumeAgent ?? "continue",
toastManager, toastManager,
@ -113,10 +113,10 @@ export async function executeSyncContinuation(
return pollError return pollError
} }
const result = await fetchSyncResult(client, args.session_id!, anchorMessageCount) const result = await deps.fetchSyncResult(client, args.session_id!, anchorMessageCount)
if (!result.ok) { if (!result.ok) {
return result.error return result.error
} }
const duration = formatDuration(startTime) const duration = formatDuration(startTime)

View File

@ -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

View File

@ -48,22 +48,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
deleteCalls.push(id) 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(() => { afterEach(() => {
@ -77,11 +61,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
}) })
test("cleans up toast and subagentSessions when fetchSyncResult returns ok: false", async () => { 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 = { const mockClient = {
session: { session: {
create: async () => ({ data: { id: "ses_test_12345678" } }), create: async () => ({ data: { id: "ses_test_12345678" } }),
@ -90,6 +69,13 @@ describe("executeSyncTask - cleanup on error paths", () => {
const { executeSyncTask } = require("./sync-task") 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 = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -114,7 +100,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask with fetchSyncResult failing //#when - executeSyncTask with fetchSyncResult failing
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session", sessionID: "parent-session",
}, "test-agent", undefined, undefined) }, "test-agent", undefined, undefined, undefined, deps)
//#then - should return error and cleanup resources //#then - should return error and cleanup resources
expect(result).toBe("Fetch failed") 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 () => { 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 = { const mockClient = {
session: { session: {
create: async () => ({ data: { id: "ses_test_12345678" } }), create: async () => ({ data: { id: "ses_test_12345678" } }),
@ -138,6 +119,13 @@ describe("executeSyncTask - cleanup on error paths", () => {
const { executeSyncTask } = require("./sync-task") 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 = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -162,7 +150,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask with pollSyncSession failing //#when - executeSyncTask with pollSyncSession failing
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session", sessionID: "parent-session",
}, "test-agent", undefined, undefined) }, "test-agent", undefined, undefined, undefined, deps)
//#then - should return error and cleanup resources //#then - should return error and cleanup resources
expect(result).toBe("Poll error") expect(result).toBe("Poll error")
@ -181,6 +169,13 @@ describe("executeSyncTask - cleanup on error paths", () => {
const { executeSyncTask } = require("./sync-task") 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 = { const mockCtx = {
sessionID: "parent-session", sessionID: "parent-session",
callID: "call-123", callID: "call-123",
@ -205,7 +200,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask completes successfully //#when - executeSyncTask completes successfully
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session", sessionID: "parent-session",
}, "test-agent", undefined, undefined) }, "test-agent", undefined, undefined, undefined, deps)
//#then - should complete and cleanup resources //#then - should complete and cleanup resources
expect(result).toContain("Task completed") expect(result).toContain("Task completed")
@ -214,4 +209,4 @@ describe("executeSyncTask - cleanup on error paths", () => {
expect(deleteCalls.length).toBe(1) expect(deleteCalls.length).toBe(1)
expect(deleteCalls[0]).toBe("ses_test_12345678") expect(deleteCalls[0]).toBe("ses_test_12345678")
}) })
}) })

View File

@ -7,10 +7,7 @@ import { subagentSessions } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { formatDuration } from "./time-formatter" import { formatDuration } from "./time-formatter"
import { formatDetailedError } from "./error-formatting" import { formatDetailedError } from "./error-formatting"
import { createSyncSession } from "./sync-session-creator" import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps"
import { sendSyncPrompt } from "./sync-prompt-sender"
import { pollSyncSession } from "./sync-session-poller"
import { fetchSyncResult } from "./sync-result-fetcher"
export async function executeSyncTask( export async function executeSyncTask(
args: DelegateTaskArgs, args: DelegateTaskArgs,
@ -20,7 +17,8 @@ export async function executeSyncTask(
agentToUse: string, agentToUse: string,
categoryModel: { providerID: string; modelID: string; variant?: string } | undefined, categoryModel: { providerID: string; modelID: string; variant?: string } | undefined,
systemContent: string | undefined, systemContent: string | undefined,
modelInfo?: ModelFallbackInfo modelInfo?: ModelFallbackInfo,
deps: SyncTaskDeps = syncTaskDeps
): Promise<string> { ): Promise<string> {
const { client, directory, onSyncSessionCreated } = executorCtx const { client, directory, onSyncSessionCreated } = executorCtx
const toastManager = getTaskToastManager() const toastManager = getTaskToastManager()
@ -28,7 +26,7 @@ export async function executeSyncTask(
let syncSessionID: string | undefined let syncSessionID: string | undefined
try { try {
const createSessionResult = await createSyncSession(client, { const createSessionResult = await deps.createSyncSession(client, {
parentSessionID: parentContext.sessionID, parentSessionID: parentContext.sessionID,
agentToUse, agentToUse,
description: args.description, description: args.description,
@ -89,7 +87,7 @@ export async function executeSyncTask(
storeToolMetadata(ctx.sessionID, ctx.callID, syncTaskMeta) storeToolMetadata(ctx.sessionID, ctx.callID, syncTaskMeta)
} }
const promptError = await sendSyncPrompt(client, { const promptError = await deps.sendSyncPrompt(client, {
sessionID, sessionID,
agentToUse, agentToUse,
args, args,
@ -103,7 +101,7 @@ export async function executeSyncTask(
} }
try { try {
const pollError = await pollSyncSession(ctx, client, { const pollError = await deps.pollSyncSession(ctx, client, {
sessionID, sessionID,
agentToUse, agentToUse,
toastManager, toastManager,
@ -113,7 +111,7 @@ export async function executeSyncTask(
return pollError return pollError
} }
const result = await fetchSyncResult(client, sessionID) const result = await deps.fetchSyncResult(client, sessionID)
if (!result.ok) { if (!result.ok) {
return result.error return result.error
} }