From 23211159c91a9064f630766c7c89079b85ad2717 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 12 May 2026 14:22:05 +0900 Subject: [PATCH] test(hooks): remove unsafe test assertions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../executor.test.ts | 6 +- .../recovery-deduplication.test.ts | 2 +- .../summarize-retry-strategy.test.ts | 6 +- src/hooks/atlas/background-task-retry.test.ts | 102 +++++++++--------- .../boulder-continuation-injector.test.ts | 28 ++--- .../atlas/idle-event-complete-boulder.test.ts | 4 +- src/hooks/atlas/idle-event-lineage.test.ts | 4 +- .../idle-event-persisted-lineage.test.ts | 8 +- src/hooks/atlas/idle-event.test.ts | 4 +- src/hooks/atlas/recent-model-resolver.test.ts | 4 +- ...ol-execute-after-background-launch.test.ts | 24 ++--- .../checker/cached-version.test.ts | 2 +- .../category-skill-reminder/index.test.ts | 4 +- .../execute-http-hook-security.test.ts | 2 +- .../execute-http-hook.test.ts | 14 +-- .../tool-input-cache.test.ts | 6 +- src/hooks/comment-checker/cli.test.ts | 4 +- .../comment-checker/pending-calls.test.ts | 22 ++-- src/hooks/edit-error-recovery/index.test.ts | 4 +- .../keyword-detector/hook-ralph-loop.test.ts | 4 +- .../hyperplan-ultrawork.test.ts | 4 +- src/hooks/keyword-detector/index.test.ts | 8 +- .../ultrawork-edge-trigger.test.ts | 4 +- .../ultrawork-runtime-variant.test.ts | 4 +- src/hooks/model-fallback/hook.test.ts | 40 +++---- src/hooks/no-hephaestus-non-gpt/index.test.ts | 20 ++-- src/hooks/no-sisyphus-gpt/index.test.ts | 4 +- src/hooks/question-label-truncator/hook.ts | 10 +- .../question-label-truncator/index.test.ts | 20 ++-- .../runtime-fallback/fallback-models.test.ts | 12 +-- src/hooks/runtime-fallback/index.test.ts | 4 +- src/hooks/session-notification-sender.test.ts | 48 ++++----- .../recover-tool-result-missing.ts | 10 +- .../storage/readers-from-sdk.test.ts | 4 +- src/hooks/start-work/index.test.ts | 22 ++-- .../stop-continuation-guard/index.test.ts | 4 +- src/hooks/task-resume-info/index.test.ts | 2 +- 37 files changed, 242 insertions(+), 232 deletions(-) diff --git a/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts b/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts index 28dd23415..753abe388 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts @@ -37,7 +37,7 @@ function createFakeTimeouts(): FakeTimeouts { callback, args, }) - return id as unknown as ReturnType + return testCoerce>(id) }) as typeof setTimeout globalThis.clearTimeout = ((id?: number) => { @@ -243,7 +243,7 @@ describe("executeCompact lock management", () => { await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig) // then: Toast should be shown - const toastCalls = (mockClient.tui.showToast as any).mock.calls + const toastCalls = (testCoerce(mockClient.tui.showToast)).mock.calls const blockedToast = toastCalls.find( (call: any) => call[0]?.body?.title === "Compact In Progress", ) @@ -276,7 +276,7 @@ describe("executeCompact lock management", () => { await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig) // then: Should show failure toast - const toastCalls = (mockClient.tui.showToast as any).mock.calls + const toastCalls = (testCoerce(mockClient.tui.showToast)).mock.calls const failureToast = toastCalls.find( (call: any) => call[0]?.body?.title === "Auto Compact Failed", ) diff --git a/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts b/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts index 68f23b3b0..184691e71 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/recovery-deduplication.test.ts @@ -20,7 +20,7 @@ function createImmediateTimeouts(): () => void { globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number, ...args: unknown[]) => { callback(...args) - return 0 as unknown as ReturnType + return testCoerce>(0) }) as typeof setTimeout globalThis.clearTimeout = ((_: ReturnType) => {}) as typeof clearTimeout diff --git a/src/hooks/anthropic-context-window-limit-recovery/summarize-retry-strategy.test.ts b/src/hooks/anthropic-context-window-limit-recovery/summarize-retry-strategy.test.ts index 332aeda20..c7091b151 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/summarize-retry-strategy.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/summarize-retry-strategy.test.ts @@ -95,7 +95,7 @@ describe("runSummarizeRetryStrategy", () => { //#given const timeoutCalls: TimeoutCall[] = [] globalThis.setTimeout = ((_: (...args: unknown[]) => void, delay?: number) => { - const handle = timeoutCalls.length + 1 as unknown as ReturnType + const handle = testCoerce>(timeoutCalls.length + 1) timeoutCalls.push({ handle, delay: delay ?? 0 }) return handle }) as typeof setTimeout @@ -132,7 +132,7 @@ describe("runSummarizeRetryStrategy", () => { let scheduledCallback: (() => void) | undefined globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number) => { scheduledCallback = () => callback() - return 1 as unknown as ReturnType + return testCoerce>(1) }) as typeof setTimeout autoCompactState.pendingCompact.add(sessionID) @@ -176,7 +176,7 @@ describe("runSummarizeRetryStrategy", () => { autoCompactState.emptyContentAttemptBySession.set(sessionID, 3) autoCompactState.retryTimerBySession.set( sessionID, - 1 as unknown as ReturnType, + testCoerce>(1), ) //#when diff --git a/src/hooks/atlas/background-task-retry.test.ts b/src/hooks/atlas/background-task-retry.test.ts index e8a9cded6..1a050b0ed 100644 --- a/src/hooks/atlas/background-task-retry.test.ts +++ b/src/hooks/atlas/background-task-retry.test.ts @@ -79,7 +79,7 @@ describe("atlas background task retry", () => { callback: () => (callback as LongTimerCallback)(...args), cleared: false, }) - return id as unknown as ReturnType + return testCoerce>(id) } return originalSetTimeout(callback, delay, ...args) @@ -120,7 +120,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -128,13 +128,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: testCoerce[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -161,7 +161,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -169,13 +169,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: testCoerce[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -204,7 +204,7 @@ describe("atlas background task retry", () => { let remainingRunningRetries = 2 const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -212,9 +212,11 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: testCoerce[1]>["backgroundManager"] & { + getTasksByParentSession: (sessionID: string) => Array<{ status: string }> + }>({ getTasksByParentSession: () => { if (remainingRunningRetries > 0) { remainingRunningRetries -= 1 @@ -223,9 +225,7 @@ describe("atlas background task retry", () => { return [] }, - } as unknown as NonNullable[1]>["backgroundManager"] & { - getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }), }) // when @@ -258,7 +258,7 @@ describe("atlas background task retry", () => { const promptAsyncMock = mock(async () => ({})) let backgroundCheckCount = 0 - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -266,9 +266,11 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: testCoerce[1]>["backgroundManager"] & { + getTasksByParentSession: (sessionID: string) => Array<{ status: string }> + }>({ getTasksByParentSession: () => { backgroundCheckCount += 1 if (backgroundCheckCount === 1) { @@ -281,9 +283,7 @@ describe("atlas background task retry", () => { return [] }, - } as unknown as NonNullable[1]>["backgroundManager"] & { - getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }), }) // when @@ -313,7 +313,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptAsyncMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -321,13 +321,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: testCoerce[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -366,7 +366,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true let descendantAgent = "atlas" const promptAsyncMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -384,18 +384,18 @@ describe("atlas background task retry", () => { }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: testCoerce[1]>["backgroundManager"] & { + getTasksByParentSession: (sessionID: string) => Array<{ status: string }> + }>({ getTasksByParentSession: (currentSessionID: string) => { if (currentSessionID !== descendantSessionID) { return [] } return backgroundRunning ? [{ status: "running" }] : [] }, - } as unknown as NonNullable[1]>["backgroundManager"] & { - getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }), }) // when @@ -424,7 +424,7 @@ describe("atlas background task retry", () => { const deferredPrompt = createDeferred<{}>() const promptAsyncMock = mock(() => deferredPrompt.promise) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -432,7 +432,7 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput) + })) // when const firstIdle = hook.handler({ event: { type: "session.idle", properties: { sessionID } } }) @@ -462,7 +462,7 @@ describe("atlas background task retry", () => { promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise) promptAsyncMock.mockImplementationOnce(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -470,13 +470,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: testCoerce[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => [], + }), }) // when @@ -515,7 +515,7 @@ describe("atlas background task retry", () => { }) promptAsyncMock.mockImplementationOnce(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce({ directory: testDir, client: { session: { @@ -523,13 +523,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: testCoerce[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when diff --git a/src/hooks/atlas/boulder-continuation-injector.test.ts b/src/hooks/atlas/boulder-continuation-injector.test.ts index d26b4b850..6aae87713 100644 --- a/src/hooks/atlas/boulder-continuation-injector.test.ts +++ b/src/hooks/atlas/boulder-continuation-injector.test.ts @@ -20,7 +20,7 @@ describe("injectBoulderContinuation", () => { const promptAsyncMock = mock(async (_request: unknown) => undefined) const messagesMock = mock(async () => ({ data: [] })) - const ctx = { + const ctx = testCoerce({ directory: "/tmp", client: { session: { @@ -28,7 +28,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -60,7 +60,7 @@ describe("injectBoulderContinuation", () => { const messagesMock = mock(async () => ({ data: [] })) const sessionState = { promptFailureCount: 2, lastContinuationInjectedAt: 123 } - const ctx = { + const ctx = testCoerce({ directory: "/tmp", client: { session: { @@ -68,7 +68,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -78,9 +78,9 @@ describe("injectBoulderContinuation", () => { remaining: 1, total: 2, agent: "atlas", - backgroundManager: { + backgroundManager: testCoerce[0]["backgroundManager"]>({ getTasksByParentSession: () => [{ status: "running" }], - } as unknown as Parameters[0]["backgroundManager"], + }), sessionState, }) @@ -98,7 +98,7 @@ describe("injectBoulderContinuation", () => { const messagesMock = mock(async () => ({ data: [] })) const sessionState = { promptFailureCount: 1, lastContinuationInjectedAt: 456 } - const ctx = { + const ctx = testCoerce({ directory: "/tmp", client: { session: { @@ -106,7 +106,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -116,9 +116,9 @@ describe("injectBoulderContinuation", () => { remaining: 1, total: 2, agent: "atlas", - backgroundManager: { + backgroundManager: testCoerce[0]["backgroundManager"]>({ getTasksByParentSession: () => [{ status: "pending" }], - } as unknown as Parameters[0]["backgroundManager"], + }), sessionState, }) @@ -134,7 +134,7 @@ describe("injectBoulderContinuation", () => { const promptAsyncMock = mock(async (_request: unknown) => undefined) const messagesMock = mock(async () => ({ data: [] })) - const ctx = { + const ctx = testCoerce({ directory: "/tmp", client: { session: { @@ -142,7 +142,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -189,7 +189,7 @@ describe("injectBoulderContinuation", () => { }], })) - const ctx = { + const ctx = testCoerce({ directory: "/tmp", client: { session: { @@ -197,7 +197,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ diff --git a/src/hooks/atlas/idle-event-complete-boulder.test.ts b/src/hooks/atlas/idle-event-complete-boulder.test.ts index a03b27fe7..85577ab57 100644 --- a/src/hooks/atlas/idle-event-complete-boulder.test.ts +++ b/src/hooks/atlas/idle-event-complete-boulder.test.ts @@ -49,7 +49,7 @@ describe("atlas hook idle-event complete boulder", () => { }, }) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce[0]>({ directory: testDirectory, client: { session: { @@ -59,7 +59,7 @@ describe("atlas hook idle-event complete boulder", () => { promptAsync: async () => ({ data: {} }), }, }, - } as unknown as Parameters[0]) + })) // when await hook.handler({ diff --git a/src/hooks/atlas/idle-event-lineage.test.ts b/src/hooks/atlas/idle-event-lineage.test.ts index 5beea6397..77e781376 100644 --- a/src/hooks/atlas/idle-event-lineage.test.ts +++ b/src/hooks/atlas/idle-event-lineage.test.ts @@ -32,7 +32,7 @@ describe("atlas hook idle-event session lineage", () => { } function createHook(parentSessionIDs?: Record) { - return createAtlasHook({ + return createAtlasHook(testCoerce[0]>({ directory: testDirectory, client: { session: { @@ -52,7 +52,7 @@ describe("atlas hook idle-event session lineage", () => { }, }, }, - } as unknown as Parameters[0]) + })) } beforeEach(() => { diff --git a/src/hooks/atlas/idle-event-persisted-lineage.test.ts b/src/hooks/atlas/idle-event-persisted-lineage.test.ts index a079bf5a0..16841ccd3 100644 --- a/src/hooks/atlas/idle-event-persisted-lineage.test.ts +++ b/src/hooks/atlas/idle-event-persisted-lineage.test.ts @@ -58,7 +58,7 @@ describe("atlas hook idle-event persisted lineage", () => { parentSessionIDs?: Record, messagesBySession?: Record>, ) { - return createAtlasHook({ + return createAtlasHook(testCoerce[0]>({ directory: testDirectory, client: { session: { @@ -79,7 +79,7 @@ describe("atlas hook idle-event persisted lineage", () => { }, }, }, - } as unknown as Parameters[0]) + })) } beforeEach(() => { @@ -173,7 +173,7 @@ describe("atlas hook idle-event persisted lineage", () => { }, }) - const hook = createAtlasHook({ + const hook = createAtlasHook(testCoerce[0]>({ directory: testDirectory, client: { session: { @@ -193,7 +193,7 @@ describe("atlas hook idle-event persisted lineage", () => { }, }, }, - } as unknown as Parameters[0]) + })) // when await hook.handler({ diff --git a/src/hooks/atlas/idle-event.test.ts b/src/hooks/atlas/idle-event.test.ts index d97783c4e..57a3be1d8 100644 --- a/src/hooks/atlas/idle-event.test.ts +++ b/src/hooks/atlas/idle-event.test.ts @@ -76,14 +76,14 @@ describe("handleAtlasSessionIdle completion nudge", () => { return { data: {} } }) - const ctx = { + const ctx = testCoerce({ directory: testDirectory, client: { session: { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) const sessionStateById = new Map() const getState = (sessionId: string): SessionState => { diff --git a/src/hooks/atlas/recent-model-resolver.test.ts b/src/hooks/atlas/recent-model-resolver.test.ts index 81db7dbe4..9da710cbb 100644 --- a/src/hooks/atlas/recent-model-resolver.test.ts +++ b/src/hooks/atlas/recent-model-resolver.test.ts @@ -5,7 +5,7 @@ import { resolveRecentPromptContextForSession } from "./recent-model-resolver" describe("resolveRecentPromptContextForSession", () => { test("uses message time.created rather than SDK array order for recent prompt context", async () => { // given - const ctx = { + const ctx = testCoerce({ client: { session: { messages: mock(async () => ({ @@ -32,7 +32,7 @@ describe("resolveRecentPromptContextForSession", () => { })), }, }, - } as unknown as PluginInput + }) // when const result = await resolveRecentPromptContextForSession(ctx, "ses_123") diff --git a/src/hooks/atlas/tool-execute-after-background-launch.test.ts b/src/hooks/atlas/tool-execute-after-background-launch.test.ts index 1a7d55894..6fccf6a89 100644 --- a/src/hooks/atlas/tool-execute-after-background-launch.test.ts +++ b/src/hooks/atlas/tool-execute-after-background-launch.test.ts @@ -80,11 +80,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { function createHandler(parentSessionIDs?: Record) { const project = createProject() - const client = { + const client = testCoerce({ session: { get: async (input: SessionGetInput) => createSessionGetResult(parentSessionIDs?.[input.path.id]), }, - } as unknown as PluginInput["client"] + }) if (parentSessionIDs) { spyOn(client.session, "get").mockImplementation((input) => Promise.resolve( @@ -141,11 +141,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { const childSessionID = "ses_child123" const planPath = join(testDirectory, "background-launch-plan.md") const project = createProject() - const client = { + const client = testCoerce({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => Promise.resolve( createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined), @@ -215,11 +215,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { const childSessionID = "ses_child_lookup_failure" const planPath = join(testDirectory, "background-launch-plan.md") const project = createProject() - const client = { + const client = testCoerce({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => { if (input?.path?.id === childSessionID) { @@ -288,11 +288,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { const childSessionID = "ses_outside_lineage" const planPath = join(testDirectory, "background-launch-plan.md") const project = createProject() - const client = { + const client = testCoerce({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => Promise.resolve( createSessionGetResult(input?.path?.id === childSessionID ? "ses_unrelated_parent" : undefined), @@ -358,11 +358,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { const childSessionID = "ses_unrelated_child" const planPath = join(testDirectory, "background-launch-plan.md") const project = createProject() - const client = { + const client = testCoerce({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => Promise.resolve( createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined), @@ -431,11 +431,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { const planPathA = join(testDirectory, "background-launch-work-a.md") const planPathB = join(testDirectory, "background-launch-work-b.md") const project = createProject() - const client = { + const client = testCoerce({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => Promise.resolve( createSessionGetResult(input?.path?.id === childSessionID ? parentSessionID : undefined), diff --git a/src/hooks/auto-update-checker/checker/cached-version.test.ts b/src/hooks/auto-update-checker/checker/cached-version.test.ts index 352de6d19..b686883db 100644 --- a/src/hooks/auto-update-checker/checker/cached-version.test.ts +++ b/src/hooks/auto-update-checker/checker/cached-version.test.ts @@ -15,7 +15,7 @@ mock.module("../constants", () => ({ const current = mockState.candidates // Forward array methods/properties to the mutable candidates list // so getCachedVersion's `for (... of ...)` sees fresh data per test. - const value = (current as unknown as Record)[prop] + const value = (testCoerce>(current))[prop] if (typeof value === "function") { return (value as (...args: unknown[]) => unknown).bind(current) } diff --git a/src/hooks/category-skill-reminder/index.test.ts b/src/hooks/category-skill-reminder/index.test.ts index 08d6118b8..83e1136da 100644 --- a/src/hooks/category-skill-reminder/index.test.ts +++ b/src/hooks/category-skill-reminder/index.test.ts @@ -21,13 +21,13 @@ describe("category-skill-reminder hook", () => { }) function createMockPluginInput() { - return { + return testCoerce({ client: { tui: { showToast: async () => {}, }, }, - } as any + }) } function createHook(availableSkills: AvailableSkill[] = []) { diff --git a/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts b/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts index 65ee6f37d..e9ee0db93 100644 --- a/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts +++ b/src/hooks/claude-code-hooks/execute-http-hook-security.test.ts @@ -31,7 +31,7 @@ describe("executeHttpHook TLS security", () => { let logCalls: Array<{ message: string; data?: unknown }> beforeEach(() => { - globalThis.fetch = mockFetch as unknown as typeof fetch + globalThis.fetch = testCoerce(mockFetch) mockFetch.mockReset() mockFetch.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) diff --git a/src/hooks/claude-code-hooks/execute-http-hook.test.ts b/src/hooks/claude-code-hooks/execute-http-hook.test.ts index 682611875..4dfe5cb3d 100644 --- a/src/hooks/claude-code-hooks/execute-http-hook.test.ts +++ b/src/hooks/claude-code-hooks/execute-http-hook.test.ts @@ -9,7 +9,7 @@ const originalFetch = globalThis.fetch describe("executeHttpHook", () => { beforeEach(() => { - globalThis.fetch = mockFetch as unknown as typeof fetch + globalThis.fetch = testCoerce(mockFetch) mockFetch.mockReset() mockFetch.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) @@ -33,7 +33,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, stdinData) expect(mockFetch).toHaveBeenCalledTimes(1) - const [url, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [url, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) expect(url).toBe("http://localhost:8080/hooks/pre-tool-use") expect(options.method).toBe("POST") expect(options.body).toBe(stdinData) @@ -44,7 +44,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, stdinData) - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Content-Type"]).toBe("application/json") }) @@ -72,7 +72,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer secret-123") }) @@ -88,7 +88,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer secret-123") }) @@ -104,7 +104,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer ") }) @@ -121,7 +121,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) expect(options.signal).toBeDefined() }) }) diff --git a/src/hooks/claude-code-hooks/tool-input-cache.test.ts b/src/hooks/claude-code-hooks/tool-input-cache.test.ts index 409c56897..345916ada 100644 --- a/src/hooks/claude-code-hooks/tool-input-cache.test.ts +++ b/src/hooks/claude-code-hooks/tool-input-cache.test.ts @@ -33,11 +33,11 @@ describe("tool-input-cache", () => { test("#given cleanup timer started #when stop cleanup runs #then interval is cleared and cache is emptied", async () => { //#given - const intervalHandle = { unref: mock(() => {}) } as unknown as ReturnType + const intervalHandle = testCoerce>({ unref: mock(() => {}) }) const setIntervalMock = mock(() => intervalHandle) const clearIntervalMock = mock(() => {}) - globalThis.setInterval = setIntervalMock as unknown as typeof setInterval - globalThis.clearInterval = clearIntervalMock as unknown as typeof clearInterval + globalThis.setInterval = testCoerce(setIntervalMock) + globalThis.clearInterval = testCoerce(clearIntervalMock) const modulePath = new URL("./tool-input-cache.ts", import.meta.url).pathname const cacheModule = await import(`${modulePath}?stop-clear`) diff --git a/src/hooks/comment-checker/cli.test.ts b/src/hooks/comment-checker/cli.test.ts index c10a34a4a..c8c736cb4 100644 --- a/src/hooks/comment-checker/cli.test.ts +++ b/src/hooks/comment-checker/cli.test.ts @@ -74,7 +74,7 @@ done const originalSetTimeout = globalThis.setTimeout globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() - return 0 as unknown as ReturnType + return testCoerce>(0) }) as typeof setTimeout try { @@ -102,7 +102,7 @@ done const originalSetTimeout = globalThis.setTimeout globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() - return 0 as unknown as ReturnType + return testCoerce>(0) }) as typeof setTimeout try { diff --git a/src/hooks/comment-checker/pending-calls.test.ts b/src/hooks/comment-checker/pending-calls.test.ts index 31f01d2fe..d703820fd 100644 --- a/src/hooks/comment-checker/pending-calls.test.ts +++ b/src/hooks/comment-checker/pending-calls.test.ts @@ -7,18 +7,18 @@ describe("pending-calls cleanup interval", () => { const setIntervalCalls: number[] = [] let unrefCalled = 0 - globalThis.setInterval = (( + globalThis.setInterval = testCoerce((( _handler: TimerHandler, timeout?: number, - ..._args: any[] + ..._args: unknown[] ) => { setIntervalCalls.push(timeout as number) - return { + return testCoerce>({ unref: () => { unrefCalled += 1 }, - } as unknown as ReturnType - }) as unknown as typeof setInterval + }) + })) try { const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname @@ -43,20 +43,20 @@ describe("pending-calls cleanup interval", () => { let intervalHandle: ReturnType | undefined let clearCalls = 0 - globalThis.setInterval = (( + globalThis.setInterval = testCoerce((( _handler: TimerHandler, _timeout?: number, - ..._args: any[] + ..._args: unknown[] ) => { - intervalHandle = { unref: () => {} } as unknown as ReturnType + intervalHandle = testCoerce>({ unref: () => {} }) return intervalHandle - }) as unknown as typeof setInterval + })) - globalThis.clearInterval = ((handle?: ReturnType) => { + globalThis.clearInterval = testCoerce(((handle?: ReturnType) => { if (handle === intervalHandle) { clearCalls += 1 } - }) as unknown as typeof clearInterval + })) try { const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname diff --git a/src/hooks/edit-error-recovery/index.test.ts b/src/hooks/edit-error-recovery/index.test.ts index ab8627056..48bfca516 100644 --- a/src/hooks/edit-error-recovery/index.test.ts +++ b/src/hooks/edit-error-recovery/index.test.ts @@ -5,7 +5,7 @@ describe("createEditErrorRecoveryHook", () => { let hook: ReturnType beforeEach(() => { - hook = createEditErrorRecoveryHook({} as any) + hook = createEditErrorRecoveryHook(testCoerce({})) }) describe("tool.execute.after", () => { @@ -108,7 +108,7 @@ describe("createEditErrorRecoveryHook", () => { const input = createInput("Edit") const output = { title: "Edit", - output: undefined as unknown as string, + output: testCoerce(undefined), metadata: {}, } diff --git a/src/hooks/keyword-detector/hook-ralph-loop.test.ts b/src/hooks/keyword-detector/hook-ralph-loop.test.ts index 0cb5972d8..09d3d21e7 100644 --- a/src/hooks/keyword-detector/hook-ralph-loop.test.ts +++ b/src/hooks/keyword-detector/hook-ralph-loop.test.ts @@ -11,13 +11,13 @@ type StartLoopCall = { type CancelLoopCall = { sessionID: string } function createMockPluginInput() { - return { + return testCoerce({ client: { tui: { showToast: async () => {}, }, }, - } as any + }) } function createMockRalphLoop(startLoopCalls: StartLoopCall[], cancelLoopCalls: CancelLoopCall[] = []) { diff --git a/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts b/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts index 37b938171..06f57d40b 100644 --- a/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts +++ b/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts @@ -22,7 +22,7 @@ describe("keyword-detector hyperplan-ultrawork combo", () => { function createMockPluginInput(options: { toastCalls?: string[] } = {}) { const toastCalls = options.toastCalls ?? [] - return { + return testCoerce({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -30,7 +30,7 @@ describe("keyword-detector hyperplan-ultrawork combo", () => { }, }, }, - } as unknown as PluginInput + }) } test("should inject combo message when user types 'hpp ulw' (forward order)", async () => { diff --git a/src/hooks/keyword-detector/index.test.ts b/src/hooks/keyword-detector/index.test.ts index 5d566b674..12c0a9fa8 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -881,13 +881,13 @@ describe("keyword-detector team mode", () => { }) function createMockPluginInput() { - return { + return testCoerce({ client: { tui: { showToast: async () => {}, }, }, - } as unknown as PluginInput + }) } test("should inject team-mode message when user types 'team mode'", async () => { @@ -1063,7 +1063,7 @@ describe("keyword-detector disabled_keywords config", () => { function createMockPluginInput(options: { toastCalls?: string[] } = {}) { const toastCalls = options.toastCalls ?? [] - return { + return testCoerce({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -1071,7 +1071,7 @@ describe("keyword-detector disabled_keywords config", () => { }, }, }, - } as unknown as PluginInput + }) } test("should NOT inject search-mode when disabled_keywords includes 'search'", async () => { diff --git a/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts index f2fbfefa4..abbbfb51b 100644 --- a/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts +++ b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts @@ -11,7 +11,7 @@ type StartLoopCall = { } function createMockPluginInput(toastCalls: string[] = []) { - return { + return testCoerce({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -19,7 +19,7 @@ function createMockPluginInput(toastCalls: string[] = []) { }, }, }, - } as unknown as PluginInput + }) } function createMockRalphLoop(startLoopCalls: StartLoopCall[]) { diff --git a/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts b/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts index 13c8c8943..8a83bf1a7 100644 --- a/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts +++ b/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts @@ -3,7 +3,7 @@ import { createKeywordDetectorHook } from "./index" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" function createMockPluginInput(toastMessages: string[]) { - return { + return testCoerce({ client: { tui: { showToast: async (opts: { body: { message: string } }) => { @@ -11,7 +11,7 @@ function createMockPluginInput(toastMessages: string[]) { }, }, }, - } as any + }) } describe("keyword-detector ultrawork runtime variant gating", () => { diff --git a/src/hooks/model-fallback/hook.test.ts b/src/hooks/model-fallback/hook.test.ts index e2f2c850f..47df5cfb6 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -86,12 +86,12 @@ describe("model fallback hook", () => { }) test("applies pending fallback on chat.message by overriding model", async () => { - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) const set = setPendingModelFallback( modelFallback, @@ -122,12 +122,12 @@ describe("model fallback hook", () => { }) test("preserves fallback progression across repeated session.error retries", async () => { - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) const sessionID = "ses_model_fallback_main" expect( @@ -212,12 +212,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_noop_skip" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) setSessionFallbackChain(modelFallback, sessionID, [ { providers: ["anthropic"], model: "claude-opus-4-7" }, @@ -254,12 +254,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_noop_variant_skip" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) setSessionFallbackChain(modelFallback, sessionID, [ { providers: ["quotio"], model: "claude-opus-4-7", variant: "max" }, @@ -299,12 +299,12 @@ describe("model fallback hook", () => { clearPendingModelFallback(modelFallback, sessionID) readConnectedProvidersCacheMock.mockReturnValue(["provider-x"]) - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) setSessionFallbackChain(modelFallback, sessionID, [ { providers: ["provider-y"], model: "fallback-model" }, @@ -355,16 +355,16 @@ describe("model fallback hook", () => { test("shows toast when fallback is applied", async () => { const toastCalls: Array<{ title: string; message: string }> = [] - const hook = createModelFallbackHook({ - toast: async ({ title, message }) => { - toastCalls.push({ title, message }) - }, - }) as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(createModelFallbackHook({ + toast: async ({ title, message }) => { + toastCalls.push({ title, message }) + }, + })) const set = setPendingModelFallback( hook, @@ -393,12 +393,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_ghcp" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) setSessionFallbackChain(modelFallback, sessionID, [ { providers: ["github-copilot"], model: "claude-sonnet-4-6" }, @@ -434,12 +434,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_google" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = testCoerce<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) setSessionFallbackChain(modelFallback, sessionID, [ { providers: ["google"], model: "gemini-3.1-pro-preview" }, diff --git a/src/hooks/no-hephaestus-non-gpt/index.test.ts b/src/hooks/no-hephaestus-non-gpt/index.test.ts index 6ca505f3c..350564afc 100644 --- a/src/hooks/no-hephaestus-non-gpt/index.test.ts +++ b/src/hooks/no-hephaestus-non-gpt/index.test.ts @@ -19,9 +19,9 @@ describe("no-hephaestus-non-gpt hook", () => { test("shows toast on every chat.message when hephaestus uses non-gpt model", async () => { // given - hephaestus with claude model const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") - const hook = createNoHephaestusNonGptHook({ + const hook = createNoHephaestusNonGptHook(testCoerce({ client: { tui: { showToast } }, - } as any) + })) const output1 = createOutput() const output2 = createOutput() @@ -54,9 +54,9 @@ describe("no-hephaestus-non-gpt hook", () => { test("shows warning and does not switch agent when allow_non_gpt_model is enabled", async () => { // given - hephaestus with claude model and opt-out enabled const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") - const hook = createNoHephaestusNonGptHook({ + const hook = createNoHephaestusNonGptHook(testCoerce({ client: { tui: { showToast } }, - } as any, { + }), { allowNonGptModel: true, }) @@ -83,9 +83,9 @@ describe("no-hephaestus-non-gpt hook", () => { test("does not show toast when hephaestus uses gpt model", async () => { // given - hephaestus with gpt model const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") - const hook = createNoHephaestusNonGptHook({ + const hook = createNoHephaestusNonGptHook(testCoerce({ client: { tui: { showToast } }, - } as any) + })) const output = createOutput() @@ -104,9 +104,9 @@ describe("no-hephaestus-non-gpt hook", () => { test("does not show toast for non-hephaestus agent", async () => { // given - sisyphus with claude model (non-gpt) const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") - const hook = createNoHephaestusNonGptHook({ + const hook = createNoHephaestusNonGptHook(testCoerce({ client: { tui: { showToast } }, - } as any) + })) const output = createOutput() @@ -127,9 +127,9 @@ describe("no-hephaestus-non-gpt hook", () => { _resetForTesting() updateSessionAgent("ses_4", HEPHAESTUS_DISPLAY) const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") - const hook = createNoHephaestusNonGptHook({ + const hook = createNoHephaestusNonGptHook(testCoerce({ client: { tui: { showToast } }, - } as any) + })) const output = createOutput() diff --git a/src/hooks/no-sisyphus-gpt/index.test.ts b/src/hooks/no-sisyphus-gpt/index.test.ts index 125c57432..c507aa5d7 100644 --- a/src/hooks/no-sisyphus-gpt/index.test.ts +++ b/src/hooks/no-sisyphus-gpt/index.test.ts @@ -22,9 +22,9 @@ function createOutput(): HookOutput { } function createHookContext(showToast: (input: unknown) => Promise): PluginInput { - return { + return testCoerce({ client: { tui: { showToast } }, - } as unknown as PluginInput + }) } describe("no-sisyphus-gpt hook", () => { diff --git a/src/hooks/question-label-truncator/hook.ts b/src/hooks/question-label-truncator/hook.ts index 03e72b23c..a43fa4fc7 100644 --- a/src/hooks/question-label-truncator/hook.ts +++ b/src/hooks/question-label-truncator/hook.ts @@ -41,6 +41,10 @@ function truncateQuestionLabels(args: AskUserQuestionArgs): AskUserQuestionArgs }; } +function hasQuestions(args: Record): args is Record & AskUserQuestionArgs { + return Array.isArray(args.questions); +} + export function createQuestionLabelTruncatorHook() { return { "tool.execute.before": async ( @@ -50,10 +54,8 @@ export function createQuestionLabelTruncatorHook() { const toolName = input.tool?.toLowerCase(); if (toolName === "askuserquestion" || toolName === "ask_user_question") { - const args = output.args as unknown as AskUserQuestionArgs | undefined; - - if (args?.questions) { - const truncatedArgs = truncateQuestionLabels(args); + if (hasQuestions(output.args)) { + const truncatedArgs = truncateQuestionLabels(output.args); Object.assign(output.args, truncatedArgs); } } diff --git a/src/hooks/question-label-truncator/index.test.ts b/src/hooks/question-label-truncator/index.test.ts index 520bd74ae..fac49527d 100644 --- a/src/hooks/question-label-truncator/index.test.ts +++ b/src/hooks/question-label-truncator/index.test.ts @@ -23,10 +23,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); // then - const truncatedLabel = (output.args as any).questions[0].options[0].label; + const truncatedLabel = (testCoerce(output.args)).questions[0].options[0].label; expect(truncatedLabel.length).toBeLessThanOrEqual(30); expect(truncatedLabel).toBe("This is a very long label t..."); expect(truncatedLabel.endsWith("...")).toBe(true); @@ -50,10 +50,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); // then - const resultLabel = (output.args as any).questions[0].options[0].label; + const resultLabel = (testCoerce(output.args)).questions[0].options[0].label; expect(resultLabel).toBe(shortLabel); }); @@ -74,10 +74,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); // then - const resultLabel = (output.args as any).questions[0].options[0].label; + const resultLabel = (testCoerce(output.args)).questions[0].options[0].label; expect(resultLabel).toBe(exactLabel); }); @@ -90,7 +90,7 @@ describe("createQuestionLabelTruncatorHook", () => { const originalArgs = { ...output.args }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); // then expect(output.args).toEqual(originalArgs); @@ -120,11 +120,11 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); // then - const q1opts = (output.args as any).questions[0].options; - const q2opts = (output.args as any).questions[1].options; + const q1opts = (testCoerce(output.args)).questions[0].options; + const q2opts = (testCoerce(output.args)).questions[1].options; expect(q1opts[0].label).toBe("Very long label number one ..."); expect(q1opts[0].label.length).toBeLessThanOrEqual(30); diff --git a/src/hooks/runtime-fallback/fallback-models.test.ts b/src/hooks/runtime-fallback/fallback-models.test.ts index ebfa8fbc9..401cf9dc0 100644 --- a/src/hooks/runtime-fallback/fallback-models.test.ts +++ b/src/hooks/runtime-fallback/fallback-models.test.ts @@ -12,13 +12,13 @@ describe("runtime-fallback fallback-models", () => { //#given const sessionID = "ses_runtime_fallback_category" SessionCategoryRegistry.register(sessionID, "quick") - const pluginConfig = { + const pluginConfig = testCoerce({ categories: { quick: { fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], }, }, - } as any + }) //#when const result = getFallbackModelsForSession(sessionID, undefined, pluginConfig) @@ -29,13 +29,13 @@ describe("runtime-fallback fallback-models", () => { test("uses agent-specific fallback_models when agent is resolved", () => { //#given - const pluginConfig = { + const pluginConfig = testCoerce({ agents: { oracle: { fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], }, }, - } as any + }) //#when const result = getFallbackModelsForSession("ses_runtime_fallback_agent", "oracle", pluginConfig) @@ -46,7 +46,7 @@ describe("runtime-fallback fallback-models", () => { test("does not fall back to another agent chain when agent cannot be resolved", () => { //#given - const pluginConfig = { + const pluginConfig = testCoerce({ agents: { sisyphus: { fallback_models: ["quotio/gpt-5.2", "quotio/glm-5", "quotio/kimi-k2.5"], @@ -55,7 +55,7 @@ describe("runtime-fallback fallback-models", () => { fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], }, }, - } as any + }) //#when const result = getFallbackModelsForSession("ses_runtime_fallback_unknown", undefined, pluginConfig) diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index d96f6d211..a2ed188f4 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -41,7 +41,7 @@ describe("runtime-fallback", () => { abort?: (args: unknown) => Promise } }) { - return { + return testCoerce({ client: { tui: { showToast: async (opts: { body: { title: string; message: string; variant: string; duration: number } }) => { @@ -59,7 +59,7 @@ describe("runtime-fallback", () => { }, }, directory: "/test/dir", - } as any + }) } function createMockConfig(overrides?: Partial): RuntimeFallbackConfig { diff --git a/src/hooks/session-notification-sender.test.ts b/src/hooks/session-notification-sender.test.ts index 931443b4b..977a8e467 100644 --- a/src/hooks/session-notification-sender.test.ts +++ b/src/hooks/session-notification-sender.test.ts @@ -80,7 +80,7 @@ describe("session-notification-sender", () => { describe("#when calling ctx.$ for notifications", () => { test("#then should call .quiet() on all shell commands to suppress stdout/stderr", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -95,7 +95,7 @@ describe("session-notification-sender", () => { promise.nothrow = () => promise return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -107,7 +107,7 @@ describe("session-notification-sender", () => { spyOn(utils, "getTerminalNotifierPath").mockResolvedValue(null) const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -130,7 +130,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -142,9 +142,9 @@ describe("session-notification-sender", () => { spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") const calls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: createShellPromise((cmdStr) => { calls.push(cmdStr) }), - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -157,9 +157,9 @@ describe("session-notification-sender", () => { test("#then should fall back to terminal-notifier when cmux fails", async () => { spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") - const mockCtx = { + const mockCtx = testCoerce({ $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")), - } as unknown as PluginInput + }) const originalFactory = mockCtx.$ const trackingCalls: string[] = [] @@ -180,9 +180,9 @@ describe("session-notification-sender", () => { spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") const trackingCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")), - } as unknown as PluginInput + }) const originalFactory = mockCtx.$ mockCtx.$ = ((cmd: TemplateStringsArray, ...values: unknown[]) => { @@ -200,9 +200,9 @@ describe("session-notification-sender", () => { test("#then should skip cmux when not available and use terminal-notifier", async () => { const calls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: createShellPromise((cmdStr) => { calls.push(cmdStr) }), - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -213,7 +213,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on linux notify-send", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -236,7 +236,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "linux", "Test", "Message") @@ -246,7 +246,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on win32 powershell", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -269,7 +269,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "win32", "Test", "Message") @@ -283,7 +283,7 @@ describe("session-notification-sender", () => { describe("#when calling ctx.$ for sound playback", () => { test("#then should call .quiet() on darwin afplay", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -306,7 +306,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "darwin", "/sound.aiff") @@ -316,7 +316,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on linux paplay", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -339,7 +339,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga") @@ -351,7 +351,7 @@ describe("session-notification-sender", () => { spyOn(utils, "getPaplayPath").mockResolvedValue(null) const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -374,7 +374,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga") @@ -384,7 +384,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on win32 powershell sound", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = testCoerce({ $: (cmd: TemplateStringsArray, ...values: unknown[]) => { const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } @@ -407,7 +407,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "win32", "C:\\sound.wav") diff --git a/src/hooks/session-recovery/recover-tool-result-missing.ts b/src/hooks/session-recovery/recover-tool-result-missing.ts index 0e7912571..6a1a8e6b9 100644 --- a/src/hooks/session-recovery/recover-tool-result-missing.ts +++ b/src/hooks/session-recovery/recover-tool-result-missing.ts @@ -11,6 +11,10 @@ type ClientWithPromptAsync = { } } +function hasPromptAsync(client: Client): client is Client & ClientWithPromptAsync { + return "promptAsync" in client.session && typeof client.session.promptAsync === "function" +} + interface ToolUsePart { type: "tool_use" @@ -111,7 +115,11 @@ export async function recoverToolResultMissing( } try { - await (client as unknown as ClientWithPromptAsync).session.promptAsync(promptInput) + if (!hasPromptAsync(client)) { + return false + } + + await client.session.promptAsync(promptInput) return true } catch { diff --git a/src/hooks/session-recovery/storage/readers-from-sdk.test.ts b/src/hooks/session-recovery/storage/readers-from-sdk.test.ts index 4b63cad6b..fc3e944b4 100644 --- a/src/hooks/session-recovery/storage/readers-from-sdk.test.ts +++ b/src/hooks/session-recovery/storage/readers-from-sdk.test.ts @@ -13,7 +13,7 @@ function createMockClient(handlers: { messages?: (sessionID: string) => unknown[] message?: (sessionID: string, messageID: string) => unknown }) { - return { + return testCoerce({ session: { messages: async (opts: { path: { id: string } }) => { if (handlers.messages) { @@ -28,7 +28,7 @@ function createMockClient(handlers: { throw new Error("not implemented") }, }, - } as unknown + }) } describe("session-recovery storage SDK readers", () => { diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts index e33a4f6a7..56afe2ccf 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -738,7 +738,7 @@ You are starting a Sisyphus work session. const promptAsyncMock = spyOn({ promptAsync: async (_request: unknown) => undefined, }, "promptAsync") - const ctx = { + const ctx = testCoerce[0]>({ directory: testDir, client: { session: { @@ -747,7 +747,7 @@ You are starting a Sisyphus work session. messages: async () => ({ data: [] }), }, }, - } as unknown as Parameters[0] + }) const startWorkHook = createStartWorkHook(ctx) const atlasHook = createAtlasHook(ctx) const output = { @@ -784,18 +784,18 @@ You are starting a Sisyphus work session. promptAsync: async (_request: unknown) => undefined, }, "promptAsync") - globalThis.setTimeout = ((callback: Function, delay?: number, ...args: unknown[]) => { + globalThis.setTimeout = testCoerce(((callback: Function, delay?: number, ...args: unknown[]) => { const normalized = typeof delay === "number" ? delay : 0 if (normalized >= 5000) { const id = nextTimerId++ capturedTimers.set(id, { callback: () => callback(...args), cleared: false }) - return id as unknown as ReturnType + return testCoerce>(id) } return originalSetTimeout(callback as Parameters[0], delay) - }) as unknown as typeof setTimeout + })) - globalThis.clearTimeout = ((id?: number | ReturnType) => { + globalThis.clearTimeout = testCoerce(((id?: number | ReturnType) => { if (typeof id === "number" && capturedTimers.has(id)) { capturedTimers.get(id)!.cleared = true capturedTimers.delete(id) @@ -803,11 +803,11 @@ You are starting a Sisyphus work session. } originalClearTimeout(id as Parameters[0]) - }) as unknown as typeof clearTimeout + })) Date.now = () => fakeNow - const ctx = { + const ctx = testCoerce[0]>({ directory: testDir, client: { session: { @@ -816,13 +816,13 @@ You are starting a Sisyphus work session. messages: async () => ({ data: [] }), }, }, - } as unknown as Parameters[0] + }) const startWorkHook = createStartWorkHook(ctx) const atlasHook = createAtlasHook(ctx, { directory: testDir, - backgroundManager: { + backgroundManager: testCoerce[1]>["backgroundManager"]>({ getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], - } as unknown as NonNullable[1]>["backgroundManager"], + }), }) const output = { message: {} as Record, diff --git a/src/hooks/stop-continuation-guard/index.test.ts b/src/hooks/stop-continuation-guard/index.test.ts index 8fa0a11a7..ba8986ebe 100644 --- a/src/hooks/stop-continuation-guard/index.test.ts +++ b/src/hooks/stop-continuation-guard/index.test.ts @@ -31,14 +31,14 @@ describe("stop-continuation-guard", () => { }) function createMockPluginInput() { - return { + return testCoerce({ client: { tui: { showToast: async () => ({}), }, }, directory: createTempDir(), - } as unknown as PluginInput + }) } function createBackgroundTask(status: BackgroundTask["status"], id: string): BackgroundTask { diff --git a/src/hooks/task-resume-info/index.test.ts b/src/hooks/task-resume-info/index.test.ts index 30708380b..c9262eb7f 100644 --- a/src/hooks/task-resume-info/index.test.ts +++ b/src/hooks/task-resume-info/index.test.ts @@ -19,7 +19,7 @@ describe("createTaskResumeInfoHook", () => { const input = createInput("task") const output = { title: "delegate_task", - output: undefined as unknown as string, + output: testCoerce(undefined), metadata: {}, }