diff --git a/src/__tests__/perf/plugin-init-team-mode-resume-defer.test.ts b/src/__tests__/perf/plugin-init-team-mode-resume-defer.test.ts index 8f5d490a4..db37df00b 100644 --- a/src/__tests__/perf/plugin-init-team-mode-resume-defer.test.ts +++ b/src/__tests__/perf/plugin-init-team-mode-resume-defer.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path" import type { PluginInput } from "@opencode-ai/plugin" import { describe, expect, it } from "bun:test" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const HUNG_LEAD_SESSION_ID = "ses_999999999fffeeRegrTestHang0" @@ -16,11 +17,11 @@ function makeHangingClient(): { hangCount.value += 1 return new Promise(() => {}) } - const client = { + const client = unsafeTestValue({ session: { get: sessionGet, }, - } as unknown as PluginInput["client"] + }) return { hangCount, client } } diff --git a/src/cli/config-manager/npm-dist-tags.test.ts b/src/cli/config-manager/npm-dist-tags.test.ts index 3de417290..d98e4e4a1 100644 --- a/src/cli/config-manager/npm-dist-tags.test.ts +++ b/src/cli/config-manager/npm-dist-tags.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, mock, test } from "bun:test" import { fetchNpmDistTags } from "../config-manager" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("fetchNpmDistTags", () => { const originalFetch = globalThis.fetch @@ -13,12 +14,12 @@ describe("fetchNpmDistTags", () => { test("returns dist-tags on success", async () => { //#given - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }), } as Response) - ) as unknown as typeof fetch + )) //#when const result = await fetchNpmDistTags("oh-my-openagent") @@ -29,7 +30,7 @@ describe("fetchNpmDistTags", () => { test("returns null on network failure", async () => { //#given - globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(mock(() => Promise.reject(new Error("Network error")))) //#when const result = await fetchNpmDistTags("oh-my-openagent") @@ -40,12 +41,12 @@ describe("fetchNpmDistTags", () => { test("returns null on non-ok response", async () => { //#given - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: false, status: 404, } as Response) - ) as unknown as typeof fetch + )) //#when const result = await fetchNpmDistTags("oh-my-openagent") diff --git a/src/cli/config-manager/opencode-binary.test.ts b/src/cli/config-manager/opencode-binary.test.ts index 27171db5b..31e9afb91 100644 --- a/src/cli/config-manager/opencode-binary.test.ts +++ b/src/cli/config-manager/opencode-binary.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test" import * as configContext from "./config-context" import * as spawnHelpers from "../../shared/spawn-with-windows-hide" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type OpenCodeBinaryModule = typeof import("./opencode-binary") @@ -92,12 +93,12 @@ describe("getOpenCodeVersion (installer)", () => { }), ) - const immediateSetTimeout = ((handler: TimerHandler) => { + const immediateSetTimeout = unsafeTestValue(((handler: TimerHandler) => { if (typeof handler === "function") { handler() } - return 1 as unknown as ReturnType - }) as unknown as typeof globalThis.setTimeout + return unsafeTestValue>(1) + })) const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout) const result = await getOpenCodeVersion() @@ -124,12 +125,12 @@ describe("getOpenCodeVersion (installer)", () => { }), ) - const immediateSetTimeout = ((handler: TimerHandler) => { + const immediateSetTimeout = unsafeTestValue(((handler: TimerHandler) => { if (typeof handler === "function") { handler() } - return 1 as unknown as ReturnType - }) as unknown as typeof globalThis.setTimeout + return unsafeTestValue>(1) + })) const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout) const result = await getOpenCodeVersion() diff --git a/src/cli/config-manager/plugin-name-with-version.test.ts b/src/cli/config-manager/plugin-name-with-version.test.ts index 7da003338..d696efeaa 100644 --- a/src/cli/config-manager/plugin-name-with-version.test.ts +++ b/src/cli/config-manager/plugin-name-with-version.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, mock, test } from "bun:test" import { getPluginNameWithVersion } from "../config-manager" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("getPluginNameWithVersion", () => { const originalFetch = globalThis.fetch @@ -13,12 +14,12 @@ describe("getPluginNameWithVersion", () => { test("returns the canonical latest tag when current version matches latest", async () => { //#given - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }), } as Response) - ) as unknown as typeof fetch + )) //#when const result = await getPluginNameWithVersion("3.13.1") @@ -29,7 +30,7 @@ describe("getPluginNameWithVersion", () => { test("preserves the canonical prerelease channel when fetch fails", async () => { //#given - globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(mock(() => Promise.reject(new Error("Network error")))) //#when const result = await getPluginNameWithVersion("3.14.0-beta.1") @@ -40,12 +41,12 @@ describe("getPluginNameWithVersion", () => { test("returns the canonical bare package name for stable fallback", async () => { //#given - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: false, status: 404, } as Response) - ) as unknown as typeof fetch + )) //#when const result = await getPluginNameWithVersion("3.13.1") diff --git a/src/cli/install.test.ts b/src/cli/install.test.ts index 61bcf645f..d84dc7060 100644 --- a/src/cli/install.test.ts +++ b/src/cli/install.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path" import { install } from "./install" import * as configManager from "./config-manager" import type { InstallArgs } from "./types" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" // Mock console methods to capture output const mockConsoleLog = mock(() => {}) @@ -57,12 +58,12 @@ describe("install CLI - binary check behavior", () => { getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) // given mock npm fetch - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ latest: "3.0.0" }), } as Response) - ) as unknown as typeof fetch + )) const args: InstallArgs = { tui: false, @@ -92,12 +93,12 @@ describe("install CLI - binary check behavior", () => { getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) // given mock npm fetch - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ latest: "3.0.0" }), } as Response) - ) as unknown as typeof fetch + )) const args: InstallArgs = { tui: false, @@ -131,12 +132,12 @@ describe("install CLI - binary check behavior", () => { getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0") // given mock npm fetch - globalThis.fetch = mock(() => + globalThis.fetch = unsafeTestValue(mock(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ latest: "3.0.0" }), } as Response) - ) as unknown as typeof fetch + )) const args: InstallArgs = { tui: false, diff --git a/src/cli/run/completion-continuation.test.ts b/src/cli/run/completion-continuation.test.ts index 6fd553527..f707b08f4 100644 --- a/src/cli/run/completion-continuation.test.ts +++ b/src/cli/run/completion-continuation.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os" import type { RunContext } from "./types" import { _resetForTesting, setSessionAgent } from "../../features/claude-code-session-state" import { writeState as writeRalphLoopState } from "../../hooks/ralph-loop/storage" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const testDirs: string[] = [] @@ -26,7 +27,7 @@ function createTempDir(): string { function createMockContext(directory: string): RunContext { return { - client: { + client: unsafeTestValue({ session: { todo: mock(() => Promise.resolve({ data: [] })), children: mock(() => Promise.resolve({ data: [] })), @@ -39,7 +40,7 @@ function createMockContext(directory: string): RunContext { })), messages: mock(async () => ({ data: [] })), }, - } as unknown as RunContext["client"], + }), sessionID: "test-session", directory, abortController: new AbortController(), @@ -155,17 +156,17 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "child-session" setSessionAgent("child-session", "atlas") - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "child-session" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "child-session" ? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -187,13 +188,13 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "lineage-only-session" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "lineage-only-session" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async () => ({ data: [] })) as unknown as RunContext["client"]["session"]["messages"] + }))) + ctx.client.session.messages = unsafeTestValue(mock(async () => ({ data: [] }))) const { checkCompletionConditions } = await import("./completion") @@ -218,17 +219,17 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "mismatch-subagent-session" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "mismatch-subagent-session" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "mismatch-subagent-session" ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -253,17 +254,17 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "appended-mismatch-session" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "appended-mismatch-session" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "appended-mismatch-session" ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -288,14 +289,14 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_appended_descendant" - ctx.client.session.get = mock(async () => { + ctx.client.session.get = unsafeTestValue(mock(async () => { throw new Error("session lookup failed") - }) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + })) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "ses_appended_descendant" ? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -317,12 +318,12 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_direct_child" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "ses_direct_child" ? "ses_parent" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -347,12 +348,12 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_direct_tracked" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -374,9 +375,9 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_unknown_child" - ctx.client.session.get = mock(async () => { + ctx.client.session.get = unsafeTestValue(mock(async () => { throw new Error("lineage unavailable") - }) as unknown as RunContext["client"]["session"]["get"] + })) const { checkCompletionConditions } = await import("./completion") @@ -401,17 +402,17 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_direct_child" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "ses_direct_child" ? "ses_root_tracked" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "ses_direct_child" ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -437,20 +438,20 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_child_after_compaction" setSessionAgent("ses_child_after_compaction", "atlas") - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "ses_child_after_compaction" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "ses_child_after_compaction" ? [ { info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }, { info: { agent: "compaction", providerID: "openai", modelID: "gpt-5.4" } }, ] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -472,13 +473,13 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_sqlite_descendant" - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "ses_sqlite_descendant" ? "root-session" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async ({ path }: { path: { id: string } }) => ({ + }))) + ctx.client.session.messages = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: path.id === "ses_sqlite_descendant" ? [ { id: "msg_0001", info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } }, @@ -486,7 +487,7 @@ describe("checkCompletionConditions continuation coverage", () => { { id: "msg_0002", info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } }, ] : [], - })) as unknown as RunContext["client"]["session"]["messages"] + }))) const { checkCompletionConditions } = await import("./completion") @@ -512,13 +513,13 @@ describe("checkCompletionConditions continuation coverage", () => { const ctx = createMockContext(directory) ctx.sessionID = "ses_appended_child" setSessionAgent("ses_appended_child", "atlas") - ctx.client.session.get = mock(async ({ path }: { path: { id: string } }) => ({ + ctx.client.session.get = unsafeTestValue(mock(async ({ path }: { path: { id: string } }) => ({ data: { id: path.id, parentID: path.id === "ses_appended_child" ? "ses_root_tracked" : undefined, }, - })) as unknown as RunContext["client"]["session"]["get"] - ctx.client.session.messages = mock(async () => ({ data: [] })) as unknown as RunContext["client"]["session"]["messages"] + }))) + ctx.client.session.messages = unsafeTestValue(mock(async () => ({ data: [] }))) const { checkCompletionConditions } = await import("./completion") diff --git a/src/cli/run/completion-verbose-logging.test.ts b/src/cli/run/completion-verbose-logging.test.ts index ff9adfcf4..32ec7b2ce 100644 --- a/src/cli/run/completion-verbose-logging.test.ts +++ b/src/cli/run/completion-verbose-logging.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, mock, spyOn } from "bun:test" import type { RunContext, ChildSession, SessionStatus } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const createMockContext = (overrides: { childrenBySession?: Record @@ -13,7 +14,7 @@ const createMockContext = (overrides: { } = overrides return { - client: { + client: unsafeTestValue({ session: { todo: mock(() => Promise.resolve({ data: [] })), children: mock((opts: { path: { id: string } }) => @@ -21,7 +22,7 @@ const createMockContext = (overrides: { ), status: mock(() => Promise.resolve({ data: statuses })), }, - } as unknown as RunContext["client"], + }), sessionID: "test-session", directory: "/test", abortController: new AbortController(), diff --git a/src/cli/run/completion.test.ts b/src/cli/run/completion.test.ts index 1537d318d..e5ad120fa 100644 --- a/src/cli/run/completion.test.ts +++ b/src/cli/run/completion.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, mock, spyOn } from "bun:test" import type { RunContext, Todo, ChildSession, SessionStatus } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const createMockContext = (overrides: { todo?: Todo[] @@ -13,7 +14,7 @@ const createMockContext = (overrides: { } = overrides return { - client: { + client: unsafeTestValue({ session: { todo: mock(() => Promise.resolve({ data: todo })), children: mock((opts: { path: { id: string } }) => @@ -21,7 +22,7 @@ const createMockContext = (overrides: { ), status: mock(() => Promise.resolve({ data: statuses })), }, - } as unknown as RunContext["client"], + }), sessionID: "test-session", directory: "/test", abortController: new AbortController(), diff --git a/src/cli/run/event-handlers.test.ts b/src/cli/run/event-handlers.test.ts index b6687cf7d..a86ffceaa 100644 --- a/src/cli/run/event-handlers.test.ts +++ b/src/cli/run/event-handlers.test.ts @@ -2,6 +2,7 @@ const { describe, it, expect, spyOn } = require("bun:test") import type { RunContext } from "./types" import { createEventState } from "./events" import { handleSessionStatus, handleMessagePartUpdated, handleMessageUpdated, handleTuiToast } from "./event-handlers" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const createMockContext = (sessionID: string = "test-session"): RunContext => ({ sessionID, @@ -23,7 +24,7 @@ describe("handleSessionStatus", () => { } //#when - handleSessionStatus called with idle status - handleSessionStatus(ctx, payload as any, state) + handleSessionStatus(ctx, unsafeTestValue(payload), state) //#then - state.mainSessionIdle === true expect(state.mainSessionIdle).toBe(true) @@ -44,7 +45,7 @@ describe("handleSessionStatus", () => { } //#when - handleSessionStatus called with busy status - handleSessionStatus(ctx, payload as any, state) + handleSessionStatus(ctx, unsafeTestValue(payload), state) //#then - state.mainSessionIdle === false expect(state.mainSessionIdle).toBe(false) @@ -65,7 +66,7 @@ describe("handleSessionStatus", () => { } //#when - handleSessionStatus called with different session ID - handleSessionStatus(ctx, payload as any, state) + handleSessionStatus(ctx, unsafeTestValue(payload), state) //#then - state.mainSessionIdle remains unchanged expect(state.mainSessionIdle).toBe(true) @@ -86,7 +87,7 @@ describe("handleSessionStatus", () => { } //#when - handleSessionStatus called with camelCase sessionId - handleSessionStatus(ctx, payload as any, state) + handleSessionStatus(ctx, unsafeTestValue(payload), state) //#then - state.mainSessionIdle === true expect(state.mainSessionIdle).toBe(true) @@ -114,7 +115,7 @@ describe("handleMessagePartUpdated", () => { } //#when - handleMessagePartUpdated(ctx, payload as any, state) + handleMessagePartUpdated(ctx, unsafeTestValue(payload), state) //#then expect(state.hasReceivedMeaningfulWork).toBe(true) @@ -142,7 +143,7 @@ describe("handleMessagePartUpdated", () => { } //#when - handleMessagePartUpdated(ctx, payload as any, state) + handleMessagePartUpdated(ctx, unsafeTestValue(payload), state) //#then expect(state.hasReceivedMeaningfulWork).toBe(false) @@ -170,7 +171,7 @@ describe("handleMessagePartUpdated", () => { } //#when - handleMessagePartUpdated(ctx, payload as any, state) + handleMessagePartUpdated(ctx, unsafeTestValue(payload), state) //#then expect(state.currentTool).toBe("read") @@ -200,7 +201,7 @@ describe("handleMessagePartUpdated", () => { } //#when - handleMessagePartUpdated(ctx, payload as any, state) + handleMessagePartUpdated(ctx, unsafeTestValue(payload), state) //#then expect(state.currentTool).toBeNull() @@ -225,7 +226,7 @@ describe("handleMessagePartUpdated", () => { } //#when - handleMessagePartUpdated(ctx, payload as any, state) + handleMessagePartUpdated(ctx, unsafeTestValue(payload), state) //#then expect(state.hasReceivedMeaningfulWork).toBe(true) @@ -243,7 +244,7 @@ describe("handleMessagePartUpdated", () => { handleMessageUpdated( ctx, - { + unsafeTestValue({ type: "message.updated", properties: { info: { @@ -254,7 +255,7 @@ describe("handleMessagePartUpdated", () => { modelID: "claude-sonnet-4-6", }, }, - } as any, + }), state, ) state.messageStartedAtById["msg_1"] = 1000 @@ -262,7 +263,7 @@ describe("handleMessagePartUpdated", () => { // when handleMessagePartUpdated( ctx, - { + unsafeTestValue({ type: "message.part.updated", properties: { part: { @@ -274,13 +275,13 @@ describe("handleMessagePartUpdated", () => { time: { end: 1 }, }, }, - } as any, + }), state, ) handleMessagePartUpdated( ctx, - { + unsafeTestValue({ type: "message.part.updated", properties: { part: { @@ -292,7 +293,7 @@ describe("handleMessagePartUpdated", () => { time: { end: 2 }, }, }, - } as any, + }), state, ) @@ -323,7 +324,7 @@ describe("handleTuiToast", () => { } //#when - handleTuiToast(ctx, payload as any, state) + handleTuiToast(ctx, unsafeTestValue(payload), state) //#then expect(state.mainSessionError).toBe(true) @@ -344,7 +345,7 @@ describe("handleTuiToast", () => { } //#when - handleTuiToast(ctx, payload as any, state) + handleTuiToast(ctx, unsafeTestValue(payload), state) //#then expect(state.mainSessionError).toBe(false) diff --git a/src/cli/run/integration.test.ts b/src/cli/run/integration.test.ts index c2b019e62..7f46b8dc3 100644 --- a/src/cli/run/integration.test.ts +++ b/src/cli/run/integration.test.ts @@ -7,6 +7,7 @@ import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hid import type { OpencodeClient } from "./types" import * as originalSdk from "@opencode-ai/sdk" import * as originalPortUtils from "../../shared/port-utils" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const mockServerClose = mock(() => {}) const mockCreateOpencode = mock(() => @@ -56,14 +57,14 @@ function createMockWriteStream(): MockWriteStream { const createMockClient = ( getResult?: { error?: unknown; data?: { id: string } } -): OpencodeClient => ({ +): OpencodeClient => (unsafeTestValue({ session: { get: mock((opts: { path: { id: string } }) => Promise.resolve(getResult ?? { data: { id: opts.path.id } }) ), create: mock(() => Promise.resolve({ data: { id: "new-session-id" } })), }, -} as unknown as OpencodeClient) +})) describe("integration: --json mode", () => { it("emits valid RunResult JSON to stdout", () => { @@ -78,8 +79,8 @@ describe("integration: --json mode", () => { summary: "Test summary", } const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) // when @@ -103,8 +104,8 @@ describe("integration: --json mode", () => { const mockStdout = createMockWriteStream() const mockStderr = createMockWriteStream() const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) manager.redirectToStderr() @@ -272,8 +273,8 @@ describe("integration: option combinations", () => { summary: "Test completed", } const jsonManager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) jsonManager.redirectToStderr() spawnSpy.mockClear() diff --git a/src/cli/run/json-output.test.ts b/src/cli/run/json-output.test.ts index d932af3c5..057a90fa4 100644 --- a/src/cli/run/json-output.test.ts +++ b/src/cli/run/json-output.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach } from "bun:test" import type { RunResult } from "./types" import { createJsonOutputManager } from "./json-output" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" interface MockWriteStream { write: (chunk: string) => boolean @@ -31,8 +32,8 @@ describe("createJsonOutputManager", () => { it("causes stdout writes to go to stderr", () => { // given const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) manager.redirectToStderr() @@ -49,8 +50,8 @@ describe("createJsonOutputManager", () => { it("reverses the redirect", () => { // given const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) manager.redirectToStderr() @@ -75,8 +76,8 @@ describe("createJsonOutputManager", () => { summary: "Test summary", } const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) // when @@ -98,8 +99,8 @@ describe("createJsonOutputManager", () => { summary: "Test summary", } const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) // when @@ -126,8 +127,8 @@ describe("createJsonOutputManager", () => { summary: "Test", } const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) manager.redirectToStderr() @@ -148,8 +149,8 @@ describe("createJsonOutputManager", () => { it("work correctly", () => { // given const manager = createJsonOutputManager({ - stdout: mockStdout as unknown as NodeJS.WriteStream, - stderr: mockStderr as unknown as NodeJS.WriteStream, + stdout: unsafeTestValue(mockStdout), + stderr: unsafeTestValue(mockStderr), }) // when diff --git a/src/cli/run/poll-for-completion.test.ts b/src/cli/run/poll-for-completion.test.ts index 670c6ba05..1d02dd7ee 100644 --- a/src/cli/run/poll-for-completion.test.ts +++ b/src/cli/run/poll-for-completion.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, it, expect, mock, spyOn } from "bun:te import type { RunContext, Todo, ChildSession, SessionStatus } from "./types" import { createEventState } from "./events" import { pollForCompletion } from "./poll-for-completion" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const createMockContext = (overrides: { todo?: Todo[] @@ -15,7 +16,7 @@ const createMockContext = (overrides: { } = overrides return { - client: { + client: unsafeTestValue({ session: { todo: mock(() => Promise.resolve({ data: todo })), children: mock((opts: { path: { id: string } }) => @@ -23,7 +24,7 @@ const createMockContext = (overrides: { ), status: mock(() => Promise.resolve({ data: statuses })), }, - } as unknown as RunContext["client"], + }), sessionID: "test-session", directory: "/test", abortController: new AbortController(), @@ -124,7 +125,7 @@ describe("pollForCompletion", () => { let todoCallCount = 0 let busyInserted = false - ;(ctx.client.session as any).todo = mock(async () => { + ;(unsafeTestValue(ctx.client.session)).todo = mock(async () => { todoCallCount++ if (todoCallCount === 1 && !busyInserted) { busyInserted = true @@ -133,10 +134,10 @@ describe("pollForCompletion", () => { } return { data: [] } }) - ;(ctx.client.session as any).children = mock(() => + ;(unsafeTestValue(ctx.client.session)).children = mock(() => Promise.resolve({ data: [] }) ) - ;(ctx.client.session as any).status = mock(() => + ;(unsafeTestValue(ctx.client.session)).status = mock(() => Promise.resolve({ data: {} }) ) @@ -322,17 +323,17 @@ describe("pollForCompletion", () => { const abortController = new AbortController() let pollTick = 0 - ;(ctx.client.session as any).todo = mock(async () => { + ;(unsafeTestValue(ctx.client.session)).todo = mock(async () => { pollTick++ if (pollTick === 2) { eventState.currentTool = "task" } return { data: [] } }) - ;(ctx.client.session as any).children = mock(() => + ;(unsafeTestValue(ctx.client.session)).children = mock(() => Promise.resolve({ data: [] }) ) - ;(ctx.client.session as any).status = mock(() => + ;(unsafeTestValue(ctx.client.session)).status = mock(() => Promise.resolve({ data: {} }) ) diff --git a/src/cli/run/session-resolver.test.ts b/src/cli/run/session-resolver.test.ts index 7b4338f11..4c1265a85 100644 --- a/src/cli/run/session-resolver.test.ts +++ b/src/cli/run/session-resolver.test.ts @@ -1,4 +1,5 @@ /// +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import { resolveSession } from "./session-resolver"; @@ -10,7 +11,7 @@ const createMockClient = (overrides: { } = {}): OpencodeClient => { const { getResult, createResults = [] } = overrides let createCallIndex = 0 - return { + return unsafeTestValue({ session: { get: mock((opts: { path: { id: string } }) => Promise.resolve(getResult ?? { data: { id: opts.path.id } }) @@ -22,7 +23,7 @@ const createMockClient = (overrides: { return Promise.resolve(result) }), }, - } as unknown as OpencodeClient + }) } describe("resolveSession", () => { diff --git a/src/cli/run/timestamp-output.test.ts b/src/cli/run/timestamp-output.test.ts index 48b8a02bb..f3b1144eb 100644 --- a/src/cli/run/timestamp-output.test.ts +++ b/src/cli/run/timestamp-output.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test" import { createTimestampTransformer, createTimestampedStdoutController } from "./timestamp-output" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createLocalDate(hours: number, minutes: number, seconds: number): Date { return new Date(2026, 1, 19, hours, minutes, seconds) @@ -87,7 +88,7 @@ describe("createTimestampedStdoutController", () => { it("prefixes stdout writes when enabled", () => { // given const stdout = createMockWriteStream() - const controller = createTimestampedStdoutController(stdout as unknown as NodeJS.WriteStream) + const controller = createTimestampedStdoutController(unsafeTestValue(stdout)) // when controller.enable() @@ -101,7 +102,7 @@ describe("createTimestampedStdoutController", () => { it("restores original write function", () => { // given const stdout = createMockWriteStream() - const controller = createTimestampedStdoutController(stdout as unknown as NodeJS.WriteStream) + const controller = createTimestampedStdoutController(unsafeTestValue(stdout)) controller.enable() // when @@ -118,7 +119,7 @@ describe("createTimestampedStdoutController", () => { it("supports Uint8Array chunks and encoding", () => { // given const stdout = createMockWriteStream() - const controller = createTimestampedStdoutController(stdout as unknown as NodeJS.WriteStream) + const controller = createTimestampedStdoutController(unsafeTestValue(stdout)) // when controller.enable() diff --git a/src/features/background-agent/background-task-notification-template.test.ts b/src/features/background-agent/background-task-notification-template.test.ts index 7001aef12..42c5371d0 100644 --- a/src/features/background-agent/background-task-notification-template.test.ts +++ b/src/features/background-agent/background-task-notification-template.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import { buildBackgroundTaskNotificationText } from "./background-task-notification-template" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("buildBackgroundTaskNotificationText", () => { describe("#given one task still running after a completed task notification", () => { @@ -134,7 +135,7 @@ Use \`background_output(task_id="")\` to retrieve each result. const notification = buildBackgroundTaskNotificationText({ task: { id: "bg_abc123", - description: undefined as unknown as string, + description: unsafeTestValue(undefined), status: "completed", }, duration: "5s", @@ -142,8 +143,8 @@ Use \`background_output(task_id="")\` to retrieve each result. allComplete: true, remainingCount: 0, completedTasks: [ - { id: "bg_abc123", description: undefined as unknown as string, status: "completed" }, - { id: "bg_def456", description: undefined as unknown as string, status: "completed" }, + { id: "bg_abc123", description: unsafeTestValue(undefined), status: "completed" }, + { id: "bg_def456", description: unsafeTestValue(undefined), status: "completed" }, ], }) @@ -230,7 +231,7 @@ Use \`background_output(task_id="")\` to retrieve each result. const notification = buildBackgroundTaskNotificationText({ task: { id: "bg_xyz789", - description: undefined as unknown as string, + description: unsafeTestValue(undefined), status: "completed", }, duration: "3s", diff --git a/src/features/background-agent/compaction-aware-message-resolver.test.ts b/src/features/background-agent/compaction-aware-message-resolver.test.ts index 4ad9a33cf..8c77654cf 100644 --- a/src/features/background-agent/compaction-aware-message-resolver.test.ts +++ b/src/features/background-agent/compaction-aware-message-resolver.test.ts @@ -12,6 +12,7 @@ import { setCompactionAgentConfigCheckpoint, } from "../../shared/compaction-agent-config-checkpoint" import { getCompactionPartStorageDir } from "../../shared/compaction-marker" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("isCompactionAgent", () => { describe("#given agent name variations", () => { @@ -49,7 +50,7 @@ describe("isCompactionAgent", () => { test("returns false for null", () => { // when - const result = isCompactionAgent(null as unknown as string) + const result = isCompactionAgent(unsafeTestValue(null)) // then expect(result).toBe(false) diff --git a/src/features/background-agent/manager-circuit-breaker.test.ts b/src/features/background-agent/manager-circuit-breaker.test.ts index 8adef0618..1df525814 100644 --- a/src/features/background-agent/manager-circuit-breaker.test.ts +++ b/src/features/background-agent/manager-circuit-breaker.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os" import type { BackgroundTaskConfig } from "../../config/schema" import { BackgroundManager } from "./manager" import type { BackgroundTask } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createManager(config?: BackgroundTaskConfig): BackgroundManager { const client = { @@ -16,12 +17,12 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager { }, } - const manager = new BackgroundManager({ pluginContext: { client, directory: tmpdir() } as unknown as PluginInput, config: config }) - const testManager = manager as unknown as { + const manager = new BackgroundManager({ pluginContext: unsafeTestValue({ client, directory: tmpdir() }), config: config }) + const testManager = unsafeTestValue<{ enqueueNotificationForParent: (sessionId: string, fn: () => Promise) => Promise notifyParentSession: (task: BackgroundTask) => Promise tasks: Map - } + }>(manager) testManager.enqueueNotificationForParent = async (_sessionId: string, fn) => { await fn() @@ -32,7 +33,7 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager { } function getTaskMap(manager: BackgroundManager): Map { - return (manager as unknown as { tasks: Map }).tasks + return (unsafeTestValue<{ tasks: Map }>(manager)).tasks } async function flushAsyncWork() { diff --git a/src/features/background-agent/manager-session-permission.test.ts b/src/features/background-agent/manager-session-permission.test.ts index a9f4a1756..fb34ee0aa 100644 --- a/src/features/background-agent/manager-session-permission.test.ts +++ b/src/features/background-agent/manager-session-permission.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os" import type { PluginInput } from "@opencode-ai/plugin" import { BackgroundManager } from "./manager" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("BackgroundManager session permission", () => { test("passes query directory when loading the parent session", async () => { @@ -21,7 +22,7 @@ describe("BackgroundManager session permission", () => { }, } const directory = tmpdir() - const manager = new BackgroundManager({ pluginContext: { client, directory } as unknown as PluginInput }) + const manager = new BackgroundManager({ pluginContext: unsafeTestValue({ client, directory }) }) // when await manager.launch({ @@ -62,7 +63,7 @@ describe("BackgroundManager session permission", () => { abort: async () => ({}), }, } - const manager = new BackgroundManager({ pluginContext: { client, directory: tmpdir() } as unknown as PluginInput }) + const manager = new BackgroundManager({ pluginContext: unsafeTestValue({ client, directory: tmpdir() }) }) // when await manager.launch({ diff --git a/src/features/background-agent/session-existence.test.ts b/src/features/background-agent/session-existence.test.ts index 9b59a4816..f0cdbed52 100644 --- a/src/features/background-agent/session-existence.test.ts +++ b/src/features/background-agent/session-existence.test.ts @@ -2,16 +2,17 @@ import { describe, expect, mock, test } from "bun:test" import type { OpencodeClient } from "./opencode-client" import { verifySessionExists } from "./session-existence" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("verifySessionExists", () => { test("passes query directory to session lookup when provided", async () => { // given const get = mock(async () => ({ data: { id: "session-123" } })) - const client = { + const client = unsafeTestValue({ session: { get, }, - } as unknown as OpencodeClient + }) // when const result = await verifySessionExists(client, "session-123", "/project/root") diff --git a/src/features/background-agent/subagent-spawn-limits.test.ts b/src/features/background-agent/subagent-spawn-limits.test.ts index e3094c551..8af9b6ff9 100644 --- a/src/features/background-agent/subagent-spawn-limits.test.ts +++ b/src/features/background-agent/subagent-spawn-limits.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_MAX_SUBAGENT_DEPTH, createSubagentDepthLimitError, } from "./subagent-spawn-limits" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createMockClient(sessionGet: OpencodeClient["session"]["get"]): OpencodeClient { return { @@ -20,14 +21,14 @@ describe("resolveSubagentSpawnContext", () => { test("passes query.directory to each session.get call", async () => { // given const sessionGetCalls: Array> = [] - const client = createMockClient((async (input) => { + const client = createMockClient(unsafeTestValue((async (input) => { sessionGetCalls.push(input as Record) if (input.path.id === "child-session") { return { data: { id: "child-session", parentID: "root-session" } } } return { data: { id: "root-session", parentID: undefined } } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when const result = await resolveSubagentSpawnContext(client, "child-session", "/project/root") @@ -50,10 +51,10 @@ describe("resolveSubagentSpawnContext", () => { describe("#given session.get returns an SDK error response", () => { test("throws a fail-closed spawn blocked error", async () => { // given - const client = createMockClient((async () => ({ + const client = createMockClient(unsafeTestValue((async () => ({ error: "lookup failed", data: undefined, - })) as unknown as OpencodeClient["session"]["get"]) + })))) // when const result = resolveSubagentSpawnContext(client, "parent-session") @@ -66,9 +67,9 @@ describe("resolveSubagentSpawnContext", () => { describe("#given session.get returns no session data", () => { test("throws a fail-closed spawn blocked error", async () => { // given - const client = createMockClient((async () => ({ + const client = createMockClient(unsafeTestValue((async () => ({ data: undefined, - })) as unknown as OpencodeClient["session"]["get"]) + })))) // when const result = resolveSubagentSpawnContext(client, "parent-session") @@ -81,12 +82,12 @@ describe("resolveSubagentSpawnContext", () => { describe("depth calculation smoke tests (regression guard)", () => { test("root session (no parentID) reports depth 0 and childDepth 1", async () => { // given - a root session with no parent - const client = createMockClient((async (opts) => { + const client = createMockClient(unsafeTestValue((async (opts) => { if (opts.path.id === "root-session") { return { data: { id: "root-session", parentID: undefined } } } return { error: "not found", data: undefined } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when const result = await resolveSubagentSpawnContext(client, "root-session") @@ -99,7 +100,7 @@ describe("resolveSubagentSpawnContext", () => { test("depth-1 child reports childDepth 2", async () => { // given - child -> root chain - const client = createMockClient((async (opts) => { + const client = createMockClient(unsafeTestValue((async (opts) => { if (opts.path.id === "child-1") { return { data: { id: "child-1", parentID: "root-session" } } } @@ -107,7 +108,7 @@ describe("resolveSubagentSpawnContext", () => { return { data: { id: "root-session", parentID: undefined } } } return { error: "not found", data: undefined } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when const result = await resolveSubagentSpawnContext(client, "child-1") @@ -120,7 +121,7 @@ describe("resolveSubagentSpawnContext", () => { test("depth-2 grandchild reports childDepth 3", async () => { // given - grandchild -> child -> root chain - const client = createMockClient((async (opts) => { + const client = createMockClient(unsafeTestValue((async (opts) => { const sessions: Record = { "grandchild": { id: "grandchild", parentID: "child" }, "child": { id: "child", parentID: "root" }, @@ -129,7 +130,7 @@ describe("resolveSubagentSpawnContext", () => { const session = sessions[opts.path.id] if (session) return { data: session } return { error: "not found", data: undefined } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when const result = await resolveSubagentSpawnContext(client, "grandchild") @@ -153,11 +154,11 @@ describe("resolveSubagentSpawnContext", () => { } } - const client = createMockClient((async (opts) => { + const client = createMockClient(unsafeTestValue((async (opts) => { const session = sessions[opts.path.id] if (session) return { data: session } return { error: "not found", data: undefined } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when - resolve from the deepest session const deepest = `session-${DEFAULT_MAX_SUBAGENT_DEPTH}` @@ -170,7 +171,7 @@ describe("resolveSubagentSpawnContext", () => { test("detects parent cycle and throws", async () => { // given - A -> B -> A (cycle) - const client = createMockClient((async (opts) => { + const client = createMockClient(unsafeTestValue((async (opts) => { const sessions: Record = { "session-a": { id: "session-a", parentID: "session-b" }, "session-b": { id: "session-b", parentID: "session-a" }, @@ -178,7 +179,7 @@ describe("resolveSubagentSpawnContext", () => { const session = sessions[opts.path.id] if (session) return { data: session } return { error: "not found", data: undefined } - }) as unknown as OpencodeClient["session"]["get"]) + }))) // when const result = resolveSubagentSpawnContext(client, "session-a") diff --git a/src/features/context-injector/injector.test.ts b/src/features/context-injector/injector.test.ts index 09de376fe..c9e98c330 100644 --- a/src/features/context-injector/injector.test.ts +++ b/src/features/context-injector/injector.test.ts @@ -3,6 +3,7 @@ import { ContextCollector } from "./collector" import { createContextInjectorMessagesTransformHook, } from "./injector" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("createContextInjectorMessagesTransformHook", () => { let collector: ContextCollector @@ -51,7 +52,7 @@ describe("createContextInjectorMessagesTransformHook", () => { createMockMessage("user", "Second message", sessionID), ] // eslint-disable-next-line @typescript-eslint/no-explicit-any - const output = { messages } as any + const output = unsafeTestValue({ messages }) // when await hook["experimental.chat.messages.transform"]!({}, output) @@ -115,7 +116,7 @@ describe("createContextInjectorMessagesTransformHook", () => { const sessionID = "ses_transform2" const messages = [createMockMessage("user", "Hello world", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any - const output = { messages } as any + const output = unsafeTestValue({ messages }) // when await hook["experimental.chat.messages.transform"]!({}, output) @@ -135,7 +136,7 @@ describe("createContextInjectorMessagesTransformHook", () => { }) const messages = [createMockMessage("assistant", "Response", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any - const output = { messages } as any + const output = unsafeTestValue({ messages }) // when await hook["experimental.chat.messages.transform"]!({}, output) @@ -156,7 +157,7 @@ describe("createContextInjectorMessagesTransformHook", () => { }) const messages = [createMockMessage("user", "Message", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any - const output = { messages } as any + const output = unsafeTestValue({ messages }) // when await hook["experimental.chat.messages.transform"]!({}, output) diff --git a/src/features/context-injector/injector.ts b/src/features/context-injector/injector.ts index 8a52de914..eff4faaec 100644 --- a/src/features/context-injector/injector.ts +++ b/src/features/context-injector/injector.ts @@ -79,6 +79,14 @@ type MessagesTransformHook = { ) => Promise } +function getSessionIDFromMessageInfo(info: Message): string | undefined { + return "sessionID" in info && typeof info.sessionID === "string" ? info.sessionID : undefined +} + +function hasText(part: Part): boolean { + return "text" in part && typeof part.text === "string" && part.text.length > 0 +} + export function createContextInjectorMessagesTransformHook( collector: ContextCollector ): MessagesTransformHook { @@ -106,8 +114,7 @@ export function createContextInjectorMessagesTransformHook( } const lastUserMessage = messages[lastUserMessageIndex] - // Try message.info.sessionID first, fallback to mainSessionID - const messageSessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID + const messageSessionID = getSessionIDFromMessageInfo(lastUserMessage.info) const sessionID = messageSessionID ?? getMainSessionID() log("[DEBUG] Extracted sessionID", { messageSessionID, @@ -135,7 +142,7 @@ export function createContextInjectorMessagesTransformHook( } const textPartIndex = lastUserMessage.parts.findIndex( - (p) => p.type === "text" && (p as { text?: string }).text + (p) => p.type === "text" && hasText(p) ) if (textPartIndex === -1) { @@ -150,7 +157,7 @@ export function createContextInjectorMessagesTransformHook( const syntheticPart = { id: `synthetic_hook_${sessionID}`, messageID: lastUserMessage.info.id, - sessionID: (lastUserMessage.info as { sessionID?: string }).sessionID ?? "", + sessionID: messageSessionID ?? "", type: "text" as const, text: pending.merged, synthetic: true, // hidden in UI diff --git a/src/features/hook-message-injector/injector.test.ts b/src/features/hook-message-injector/injector.test.ts index a50aefa95..0a5bb7387 100644 --- a/src/features/hook-message-injector/injector.test.ts +++ b/src/features/hook-message-injector/injector.test.ts @@ -11,6 +11,7 @@ import { injectHookMessage, } from "./injector" import { getCompactionPartStorageDir } from "../../shared/compaction-marker" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" //#region Mocks @@ -73,7 +74,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { info: { agent: "sisyphus", model: { providerID: "anthropic", modelID: "claude-opus-4" } } }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toEqual({ agent: "sisyphus", @@ -87,7 +88,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { info: { agent: "sisyphus", providerID: "openai", modelID: "gpt-5" } }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toEqual({ agent: "sisyphus", @@ -102,7 +103,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { id: "msg_new", info: { agent: "new-agent", model: { providerID: "new", modelID: "model" }, time: { created: 20 } } }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result?.agent).toBe("new-agent") }) @@ -112,7 +113,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { info: { agent: "partial-agent" } }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result?.agent).toBe("partial-agent") }) @@ -123,7 +124,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { info: {} }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBeNull() }) @@ -131,7 +132,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { it("returns null when messages array is empty", async () => { const mockClient = createMockClient([]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBeNull() }) @@ -145,7 +146,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { }, } - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBeNull() }) @@ -161,7 +162,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result?.tools).toEqual({ edit: true, write: false }) }) @@ -172,7 +173,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { { id: "msg_older", info: { agent: "newest-by-time", model: { providerID: "openai", modelID: "gpt-5" }, time: { created: 100 } } }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result?.agent).toBe("newest-by-time") }) @@ -190,7 +191,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => { }, ]) - const result = await findNearestMessageWithFieldsFromSDK(mockClient as any, "ses_123") + const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result?.agent).toBe("sisyphus") }) @@ -252,7 +253,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { { info: { agent: "second-agent" } }, ]) - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBe("first-agent") }) @@ -263,7 +264,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { { id: "msg_early", info: { agent: "earliest-agent", time: { created: 10 } } }, ]) - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBe("earliest-agent") }) @@ -274,7 +275,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { { id: "msg_real", info: { agent: "sisyphus", time: { created: 20 } } }, ]) - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBe("sisyphus") }) @@ -285,7 +286,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { { info: { agent: "first-real-agent" } }, ]) - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBe("first-real-agent") }) @@ -296,7 +297,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { { info: {} }, ]) - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBeNull() }) @@ -310,7 +311,7 @@ describe("findFirstMessageWithAgentFromSDK", () => { }, } - const result = await findFirstMessageWithAgentFromSDK(mockClient as any, "ses_123") + const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123") expect(result).toBeNull() }) diff --git a/src/features/skill-mcp-manager/manager.test.ts b/src/features/skill-mcp-manager/manager.test.ts index f3ef6f51e..accd4db20 100644 --- a/src/features/skill-mcp-manager/manager.test.ts +++ b/src/features/skill-mcp-manager/manager.test.ts @@ -6,6 +6,7 @@ import type { OAuthTokenData } from "../mcp-oauth/storage" import { setHttpClientDependenciesForTesting } from "./http-client" import { setStdioClientDependenciesForTesting } from "./stdio-client" import { SkillMcpManager } from "./manager" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure"))) const mockHttpClose = mock(() => Promise.resolve()) @@ -634,7 +635,7 @@ describe("SkillMcpManager", () => { close: mock(() => Promise.resolve()), } - const getOrCreateSpy = spyOn(manager as any, "getOrCreateClientWithRetry") + const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry") getOrCreateSpy.mockResolvedValue(mockClient) // when @@ -668,7 +669,7 @@ describe("SkillMcpManager", () => { close: mock(() => Promise.resolve()), } - const getOrCreateSpy = spyOn(manager as any, "getOrCreateClientWithRetry") + const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry") getOrCreateSpy.mockResolvedValue(mockClient) // when / #then @@ -700,7 +701,7 @@ describe("SkillMcpManager", () => { close: mock(() => Promise.resolve()), } - const getOrCreateSpy = spyOn(manager as any, "getOrCreateClientWithRetry") + const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry") getOrCreateSpy.mockResolvedValue(mockClient) // when / #then @@ -929,7 +930,7 @@ describe("SkillMcpManager", () => { close: mock(() => Promise.resolve()), } - const getOrCreateSpy = spyOn(manager as any, "getOrCreateClientWithRetry") + const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry") getOrCreateSpy.mockResolvedValue(mockClient) // when @@ -962,7 +963,7 @@ describe("SkillMcpManager", () => { close: mock(() => Promise.resolve()), } - const getOrCreateSpy = spyOn(manager as any, "getOrCreateClientWithRetry") + const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry") getOrCreateSpy.mockResolvedValue(mockClient) // when / #then diff --git a/src/features/task-toast-manager/manager.test.ts b/src/features/task-toast-manager/manager.test.ts index 92ab524a0..77dcd7c83 100644 --- a/src/features/task-toast-manager/manager.test.ts +++ b/src/features/task-toast-manager/manager.test.ts @@ -1,6 +1,7 @@ declare const require: (name: string) => any const { describe, test, expect, beforeEach, afterEach, mock } = require("bun:test") import type { ConcurrencyManager } from "../background-agent/concurrency" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type TaskToastManagerClass = typeof import("./manager").TaskToastManager @@ -20,15 +21,15 @@ describe("TaskToastManager", () => { showToast: mock(() => Promise.resolve()), }, } - mockConcurrencyManager = { + mockConcurrencyManager = unsafeTestValue({ getConcurrencyLimit: mock(() => 5), - } as unknown as ConcurrencyManager + }) const mod = await import("./manager") TaskToastManager = mod.TaskToastManager // eslint-disable-next-line @typescript-eslint/no-explicit-any - toastManager = new TaskToastManager(mockClient as any, mockConcurrencyManager) + toastManager = new TaskToastManager(unsafeTestValue(mockClient), mockConcurrencyManager) }) afterEach(() => { @@ -108,14 +109,14 @@ describe("TaskToastManager", () => { test("should display concurrency limit info when available", () => { // given - a concurrency manager with known limit - const mockConcurrencyWithCounts = { + const mockConcurrencyWithCounts = unsafeTestValue({ getConcurrencyLimit: mock(() => 5), getRunningCount: mock(() => 2), getQueuedCount: mock(() => 1), - } as unknown as ConcurrencyManager + }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const managerWithConcurrency = new TaskToastManager(mockClient as any, mockConcurrencyWithCounts) + const managerWithConcurrency = new TaskToastManager(unsafeTestValue(mockClient), mockConcurrencyWithCounts) // when - a task is added managerWithConcurrency.addTask({ @@ -357,11 +358,11 @@ describe("TaskToastManager", () => { test("should show model name in queued tasks too", () => { // given - a concurrency manager that limits to 1 - const limitedConcurrency = { + const limitedConcurrency = unsafeTestValue({ getConcurrencyLimit: mock(() => 1), - } as unknown as ConcurrencyManager + }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const limitedManager = new TaskToastManager(mockClient as any, limitedConcurrency) + const limitedManager = new TaskToastManager(unsafeTestValue(mockClient), limitedConcurrency) limitedManager.addTask({ id: "task_running", diff --git a/src/features/team-mode/team-runtime/cleanup-team-run-resources.test.ts b/src/features/team-mode/team-runtime/cleanup-team-run-resources.test.ts index 22ee7d53d..702380600 100644 --- a/src/features/team-mode/team-runtime/cleanup-team-run-resources.test.ts +++ b/src/features/team-mode/team-runtime/cleanup-team-run-resources.test.ts @@ -16,6 +16,7 @@ import { import { saveRuntimeState } from "../team-state-store/store" import type { RuntimeState } from "../types" import { cleanupTeamRunResources } from "./cleanup-team-run-resources" +import { unsafeTestValue } from "../../../../test-support/unsafe-test-value" const temporaryDirectories: string[] = [] @@ -41,9 +42,9 @@ function createRuntimeState(teamRunId: string): RuntimeState { } function createStubBgMgr(): BackgroundManager { - return { + return unsafeTestValue({ cancelTask: async () => undefined, - } as unknown as BackgroundManager + }) } describe("cleanupTeamRunResources", () => { diff --git a/src/features/tmux-subagent/polling-manager.test.ts b/src/features/tmux-subagent/polling-manager.test.ts index 38b32797f..7c0d3cd1d 100644 --- a/src/features/tmux-subagent/polling-manager.test.ts +++ b/src/features/tmux-subagent/polling-manager.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect } from "bun:test" import { TmuxPollingManager } from "./polling-manager" import type { TrackedSession } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("TmuxPollingManager overlap", () => { test("skips overlapping pollSessions executions", async () => { @@ -39,15 +40,15 @@ describe("TmuxPollingManager overlap", () => { } const manager = new TmuxPollingManager( - client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + unsafeTestValue(client), sessions, async () => {}, ) //#when - const firstPoll = (manager as unknown as { pollSessions: () => Promise }).pollSessions() + const firstPoll = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions() await Promise.resolve() - const secondPoll = (manager as unknown as { pollSessions: () => Promise }).pollSessions() + const secondPoll = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions() releaseStatus?.() await Promise.all([firstPoll, secondPoll]) @@ -85,7 +86,7 @@ describe("TmuxPollingManager overlap", () => { } const manager = new TmuxPollingManager( - client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + unsafeTestValue(client), sessions, async (sessionId) => { closedSessionIds.push(sessionId) @@ -98,7 +99,7 @@ describe("TmuxPollingManager overlap", () => { }) //#when - const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions await pollSessions.call(manager) await pollSessions.call(manager) await pollSessions.call(manager) @@ -132,7 +133,7 @@ describe("TmuxPollingManager overlap", () => { } const manager = new TmuxPollingManager( - client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + unsafeTestValue(client), sessions, async (sessionId) => { closedSessionIds.push(sessionId) @@ -140,7 +141,7 @@ describe("TmuxPollingManager overlap", () => { ) // when - const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions await pollSessions.call(manager) // then @@ -171,7 +172,7 @@ describe("TmuxPollingManager overlap", () => { } const manager = new TmuxPollingManager( - client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + unsafeTestValue(client), sessions, async (sessionId) => { closedSessionIds.push(sessionId) @@ -179,7 +180,7 @@ describe("TmuxPollingManager overlap", () => { ) // when - const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions await pollSessions.call(manager) // then @@ -222,13 +223,13 @@ describe("TmuxPollingManager overlap", () => { } manager = new TmuxPollingManager( - client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + unsafeTestValue(client), sessions, async (sessionId) => { closedSessionIds.push(sessionId) }, ) - const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise }>(manager)).pollSessions // when await pollSessions.call(manager) 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..9c529007d 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts @@ -5,6 +5,7 @@ import { executeCompact } from "./executor" import type { AutoCompactState } from "./types" import * as recoveryStrategy from "./recovery-strategy" import * as messagesReader from "../session-recovery/storage/messages-reader" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type TimerCallback = (...args: any[]) => void @@ -37,7 +38,7 @@ function createFakeTimeouts(): FakeTimeouts { callback, args, }) - return id as unknown as ReturnType + return unsafeTestValue>(id) }) as typeof setTimeout globalThis.clearTimeout = ((id?: number) => { @@ -243,7 +244,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 = (unsafeTestValue(mockClient.tui.showToast)).mock.calls const blockedToast = toastCalls.find( (call: any) => call[0]?.body?.title === "Compact In Progress", ) @@ -276,7 +277,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 = (unsafeTestValue(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..d6032a761 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 @@ -2,6 +2,7 @@ import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import type { ExperimentalConfig } from "../../config" import * as originalDeduplicationRecovery from "./deduplication-recovery" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const attemptDeduplicationRecoveryMock = mock(async () => {}) @@ -20,7 +21,7 @@ function createImmediateTimeouts(): () => void { globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number, ...args: unknown[]) => { callback(...args) - return 0 as unknown as ReturnType + return unsafeTestValue>(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..7d955909b 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 @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" import { runSummarizeRetryStrategy } from "./summarize-retry-strategy" import type { AutoCompactState, ParsedTokenLimitError, RetryState } from "./types" import type { OhMyOpenCodeConfig } from "../../config" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type TimeoutCall = { handle: ReturnType @@ -95,7 +96,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 = unsafeTestValue>(timeoutCalls.length + 1) timeoutCalls.push({ handle, delay: delay ?? 0 }) return handle }) as typeof setTimeout @@ -132,7 +133,7 @@ describe("runSummarizeRetryStrategy", () => { let scheduledCallback: (() => void) | undefined globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number) => { scheduledCallback = () => callback() - return 1 as unknown as ReturnType + return unsafeTestValue>(1) }) as typeof setTimeout autoCompactState.pendingCompact.add(sessionID) @@ -176,7 +177,7 @@ describe("runSummarizeRetryStrategy", () => { autoCompactState.emptyContentAttemptBySession.set(sessionID, 3) autoCompactState.retryTimerBySession.set( sessionID, - 1 as unknown as ReturnType, + unsafeTestValue>(1), ) //#when diff --git a/src/hooks/atlas/background-task-retry.test.ts b/src/hooks/atlas/background-task-retry.test.ts index e8a9cded6..8cab40ba2 100644 --- a/src/hooks/atlas/background-task-retry.test.ts +++ b/src/hooks/atlas/background-task-retry.test.ts @@ -7,6 +7,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { createAtlasHook } from "./atlas-hook" import { clearBoulderState, writeBoulderState } from "../../features/boulder-state" import { _resetForTesting, clearSessionAgent, registerAgentName, setSessionAgent } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" // Force process isolation in CI runner (globalThis.setTimeout override conflicts with other atlas tests) mock.module("../../shared/opencode-storage-detection", () => ({ @@ -79,7 +80,7 @@ describe("atlas background task retry", () => { callback: () => (callback as LongTimerCallback)(...args), cleared: false, }) - return id as unknown as ReturnType + return unsafeTestValue>(id) } return originalSetTimeout(callback, delay, ...args) @@ -120,7 +121,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -128,13 +129,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: unsafeTestValue[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -161,7 +162,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -169,13 +170,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: unsafeTestValue[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -204,7 +205,7 @@ describe("atlas background task retry", () => { let remainingRunningRetries = 2 const promptMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -212,9 +213,11 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: unsafeTestValue[1]>["backgroundManager"] & { + getTasksByParentSession: (sessionID: string) => Array<{ status: string }> + }>({ getTasksByParentSession: () => { if (remainingRunningRetries > 0) { remainingRunningRetries -= 1 @@ -223,9 +226,7 @@ describe("atlas background task retry", () => { return [] }, - } as unknown as NonNullable[1]>["backgroundManager"] & { - getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }), }) // when @@ -258,7 +259,7 @@ describe("atlas background task retry", () => { const promptAsyncMock = mock(async () => ({})) let backgroundCheckCount = 0 - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -266,9 +267,11 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: unsafeTestValue[1]>["backgroundManager"] & { + getTasksByParentSession: (sessionID: string) => Array<{ status: string }> + }>({ getTasksByParentSession: () => { backgroundCheckCount += 1 if (backgroundCheckCount === 1) { @@ -281,9 +284,7 @@ describe("atlas background task retry", () => { return [] }, - } as unknown as NonNullable[1]>["backgroundManager"] & { - getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }), }) // when @@ -313,7 +314,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true const promptAsyncMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -321,13 +322,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: unsafeTestValue[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], + }), }) // when @@ -366,7 +367,7 @@ describe("atlas background task retry", () => { let backgroundRunning = true let descendantAgent = "atlas" const promptAsyncMock = mock(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -384,18 +385,18 @@ describe("atlas background task retry", () => { }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { + backgroundManager: unsafeTestValue[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 +425,7 @@ describe("atlas background task retry", () => { const deferredPrompt = createDeferred<{}>() const promptAsyncMock = mock(() => deferredPrompt.promise) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -432,7 +433,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 +463,7 @@ describe("atlas background task retry", () => { promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise) promptAsyncMock.mockImplementationOnce(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -470,13 +471,13 @@ describe("atlas background task retry", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as PluginInput, { + }), { directory: testDir, - backgroundManager: { - getTasksByParentSession: () => [], - } as unknown as NonNullable[1]>["backgroundManager"] & { + backgroundManager: unsafeTestValue[1]>["backgroundManager"] & { getTasksByParentSession: (sessionID: string) => Array<{ status: string }> - }, + }>({ + getTasksByParentSession: () => [], + }), }) // when @@ -515,7 +516,7 @@ describe("atlas background task retry", () => { }) promptAsyncMock.mockImplementationOnce(async () => ({})) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue({ directory: testDir, client: { session: { @@ -523,13 +524,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: unsafeTestValue[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..902bfe78f 100644 --- a/src/hooks/atlas/boulder-continuation-injector.test.ts +++ b/src/hooks/atlas/boulder-continuation-injector.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import { registerAgentName, _resetForTesting } from "../../features/claude-code-session-state" import { injectBoulderContinuation } from "./boulder-continuation-injector" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("injectBoulderContinuation", () => { beforeEach(() => { @@ -20,7 +21,7 @@ describe("injectBoulderContinuation", () => { const promptAsyncMock = mock(async (_request: unknown) => undefined) const messagesMock = mock(async () => ({ data: [] })) - const ctx = { + const ctx = unsafeTestValue({ directory: "/tmp", client: { session: { @@ -28,7 +29,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -60,7 +61,7 @@ describe("injectBoulderContinuation", () => { const messagesMock = mock(async () => ({ data: [] })) const sessionState = { promptFailureCount: 2, lastContinuationInjectedAt: 123 } - const ctx = { + const ctx = unsafeTestValue({ directory: "/tmp", client: { session: { @@ -68,7 +69,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -78,9 +79,9 @@ describe("injectBoulderContinuation", () => { remaining: 1, total: 2, agent: "atlas", - backgroundManager: { + backgroundManager: unsafeTestValue[0]["backgroundManager"]>({ getTasksByParentSession: () => [{ status: "running" }], - } as unknown as Parameters[0]["backgroundManager"], + }), sessionState, }) @@ -98,7 +99,7 @@ describe("injectBoulderContinuation", () => { const messagesMock = mock(async () => ({ data: [] })) const sessionState = { promptFailureCount: 1, lastContinuationInjectedAt: 456 } - const ctx = { + const ctx = unsafeTestValue({ directory: "/tmp", client: { session: { @@ -106,7 +107,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -116,9 +117,9 @@ describe("injectBoulderContinuation", () => { remaining: 1, total: 2, agent: "atlas", - backgroundManager: { + backgroundManager: unsafeTestValue[0]["backgroundManager"]>({ getTasksByParentSession: () => [{ status: "pending" }], - } as unknown as Parameters[0]["backgroundManager"], + }), sessionState, }) @@ -134,7 +135,7 @@ describe("injectBoulderContinuation", () => { const promptAsyncMock = mock(async (_request: unknown) => undefined) const messagesMock = mock(async () => ({ data: [] })) - const ctx = { + const ctx = unsafeTestValue({ directory: "/tmp", client: { session: { @@ -142,7 +143,7 @@ describe("injectBoulderContinuation", () => { promptAsync: promptAsyncMock, }, }, - } as unknown as PluginInput + }) // when const result = await injectBoulderContinuation({ @@ -189,7 +190,7 @@ describe("injectBoulderContinuation", () => { }], })) - const ctx = { + const ctx = unsafeTestValue({ directory: "/tmp", client: { session: { @@ -197,7 +198,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..6d5d306ea 100644 --- a/src/hooks/atlas/idle-event-complete-boulder.test.ts +++ b/src/hooks/atlas/idle-event-complete-boulder.test.ts @@ -4,6 +4,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const { createAtlasHook } = await import("./index") @@ -49,7 +50,7 @@ describe("atlas hook idle-event complete boulder", () => { }, }) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue[0]>({ directory: testDirectory, client: { session: { @@ -59,7 +60,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..ff5e50f7b 100644 --- a/src/hooks/atlas/idle-event-lineage.test.ts +++ b/src/hooks/atlas/idle-event-lineage.test.ts @@ -7,6 +7,7 @@ import { join } from "node:path" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state" import { _resetForTesting, registerAgentName, setSessionAgent, subagentSessions } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const { createAtlasHook } = await import("./index") @@ -32,7 +33,7 @@ describe("atlas hook idle-event session lineage", () => { } function createHook(parentSessionIDs?: Record) { - return createAtlasHook({ + return createAtlasHook(unsafeTestValue[0]>({ directory: testDirectory, client: { session: { @@ -52,7 +53,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..af2c65575 100644 --- a/src/hooks/atlas/idle-event-persisted-lineage.test.ts +++ b/src/hooks/atlas/idle-event-persisted-lineage.test.ts @@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state" import type { BoulderState } from "../../features/boulder-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-persisted-lineage-storage-${randomUUID()}`) const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") @@ -58,7 +59,7 @@ describe("atlas hook idle-event persisted lineage", () => { parentSessionIDs?: Record, messagesBySession?: Record>, ) { - return createAtlasHook({ + return createAtlasHook(unsafeTestValue[0]>({ directory: testDirectory, client: { session: { @@ -79,7 +80,7 @@ describe("atlas hook idle-event persisted lineage", () => { }, }, }, - } as unknown as Parameters[0]) + })) } beforeEach(() => { @@ -173,7 +174,7 @@ describe("atlas hook idle-event persisted lineage", () => { }, }) - const hook = createAtlasHook({ + const hook = createAtlasHook(unsafeTestValue[0]>({ directory: testDirectory, client: { session: { @@ -193,7 +194,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..61f9204bf 100644 --- a/src/hooks/atlas/idle-event.test.ts +++ b/src/hooks/atlas/idle-event.test.ts @@ -8,6 +8,7 @@ import { createBoulderState, readBoulderState, writeBoulderState } from "../../f import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state" import { handleAtlasSessionIdle } from "./idle-event" import type { SessionState } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("handleAtlasSessionIdle completion nudge", () => { const SESSION_ID = "session-main-1" @@ -76,14 +77,14 @@ describe("handleAtlasSessionIdle completion nudge", () => { return { data: {} } }) - const ctx = { + const ctx = unsafeTestValue({ 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..e326e48fa 100644 --- a/src/hooks/atlas/recent-model-resolver.test.ts +++ b/src/hooks/atlas/recent-model-resolver.test.ts @@ -1,11 +1,12 @@ import { describe, expect, mock, test } from "bun:test" import type { PluginInput } from "@opencode-ai/plugin" import { resolveRecentPromptContextForSession } from "./recent-model-resolver" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("resolveRecentPromptContextForSession", () => { test("uses message time.created rather than SDK array order for recent prompt context", async () => { // given - const ctx = { + const ctx = unsafeTestValue({ client: { session: { messages: mock(async () => ({ @@ -32,7 +33,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..4bca3e302 100644 --- a/src/hooks/atlas/tool-execute-after-background-launch.test.ts +++ b/src/hooks/atlas/tool-execute-after-background-launch.test.ts @@ -8,6 +8,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { Project } from "@opencode-ai/sdk" import { readBoulderState, writeBoulderState } from "../../features/boulder-state" import { createToolExecuteBeforeHandler } from "./tool-execute-before" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const isCallerOrchestratorMock = mock(async () => true) const collectGitDiffStatsMock = mock(() => ({ @@ -80,11 +81,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => { function createHandler(parentSessionIDs?: Record) { const project = createProject() - const client = { + const client = unsafeTestValue({ 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 +142,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 = unsafeTestValue({ 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 +216,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 = unsafeTestValue({ session: { get: async () => createSessionGetResult(undefined), }, - } as unknown as PluginInput["client"] + }) spyOn(client.session, "get").mockImplementation((input) => { if (input?.path?.id === childSessionID) { @@ -288,11 +289,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 = unsafeTestValue({ 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 +359,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 = unsafeTestValue({ 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 +432,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 = unsafeTestValue({ 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..9d4c55806 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 = (unsafeTestValue>(current))[prop] if (typeof value === "function") { return (value as (...args: unknown[]) => unknown).bind(current) } @@ -29,6 +29,7 @@ mock.module("./package-json-locator", () => ({ })) import { getCachedVersion } from "./cached-version" +import { unsafeTestValue } from "../../../../test-support/unsafe-test-value" describe("getCachedVersion (GH-3257)", () => { let cacheRoot: string diff --git a/src/hooks/category-skill-reminder/index.test.ts b/src/hooks/category-skill-reminder/index.test.ts index 08d6118b8..2bbcf9052 100644 --- a/src/hooks/category-skill-reminder/index.test.ts +++ b/src/hooks/category-skill-reminder/index.test.ts @@ -3,6 +3,7 @@ import { createCategorySkillReminderHook } from "./index" import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state" import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" import * as sharedModule from "../../shared" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("category-skill-reminder hook", () => { let logCalls: Array<{ msg: string; data?: unknown }> @@ -21,13 +22,13 @@ describe("category-skill-reminder hook", () => { }) function createMockPluginInput() { - return { + return unsafeTestValue({ 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..db8a25099 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 @@ -3,6 +3,7 @@ import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" import type { HookHttp } from "./types" import * as sharedModule from "../../shared" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const mockFetch = mock(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) @@ -31,7 +32,7 @@ describe("executeHttpHook TLS security", () => { let logCalls: Array<{ message: string; data?: unknown }> beforeEach(() => { - globalThis.fetch = mockFetch as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(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..ad2e40fca 100644 --- a/src/hooks/claude-code-hooks/execute-http-hook.test.ts +++ b/src/hooks/claude-code-hooks/execute-http-hook.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" import type { HookHttp } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const mockFetch = mock(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) @@ -9,7 +10,7 @@ const originalFetch = globalThis.fetch describe("executeHttpHook", () => { beforeEach(() => { - globalThis.fetch = mockFetch as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(mockFetch) mockFetch.mockReset() mockFetch.mockImplementation(() => Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) @@ -33,7 +34,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] = unsafeTestValue<[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 +45,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, stdinData) - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Content-Type"]).toBe("application/json") }) @@ -72,7 +73,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer secret-123") }) @@ -88,7 +89,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer secret-123") }) @@ -104,7 +105,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0]) const headers = options.headers as Record expect(headers["Authorization"]).toBe("Bearer ") }) @@ -121,7 +122,7 @@ describe("executeHttpHook", () => { await executeHttpHook(hook, "{}") - const [, options] = mockFetch.mock.calls[0] as unknown as [string, RequestInit] + const [, options] = unsafeTestValue<[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..8141e0dd6 100644 --- a/src/hooks/claude-code-hooks/tool-input-cache.test.ts +++ b/src/hooks/claude-code-hooks/tool-input-cache.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("tool-input-cache", () => { const originalSetInterval = globalThis.setInterval @@ -33,11 +34,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 = unsafeTestValue>({ 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 = unsafeTestValue(setIntervalMock) + globalThis.clearInterval = unsafeTestValue(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..a540aa003 100644 --- a/src/hooks/comment-checker/cli.test.ts +++ b/src/hooks/comment-checker/cli.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os" import { processWithCli } from "./cli-runner" import type { PendingCall } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createMockInput() { return { @@ -74,7 +75,7 @@ done const originalSetTimeout = globalThis.setTimeout globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() - return 0 as unknown as ReturnType + return unsafeTestValue>(0) }) as typeof setTimeout try { @@ -102,7 +103,7 @@ done const originalSetTimeout = globalThis.setTimeout globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() - return 0 as unknown as ReturnType + return unsafeTestValue>(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..8c5c655d4 100644 --- a/src/hooks/comment-checker/pending-calls.test.ts +++ b/src/hooks/comment-checker/pending-calls.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect } from "bun:test" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("pending-calls cleanup interval", () => { test("starts cleanup once and unrefs timer", async () => { @@ -7,18 +8,18 @@ describe("pending-calls cleanup interval", () => { const setIntervalCalls: number[] = [] let unrefCalled = 0 - globalThis.setInterval = (( + globalThis.setInterval = unsafeTestValue((( _handler: TimerHandler, timeout?: number, - ..._args: any[] + ..._args: unknown[] ) => { setIntervalCalls.push(timeout as number) - return { + return unsafeTestValue>({ 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 +44,20 @@ describe("pending-calls cleanup interval", () => { let intervalHandle: ReturnType | undefined let clearCalls = 0 - globalThis.setInterval = (( + globalThis.setInterval = unsafeTestValue((( _handler: TimerHandler, _timeout?: number, - ..._args: any[] + ..._args: unknown[] ) => { - intervalHandle = { unref: () => {} } as unknown as ReturnType + intervalHandle = unsafeTestValue>({ unref: () => {} }) return intervalHandle - }) as unknown as typeof setInterval + })) - globalThis.clearInterval = ((handle?: ReturnType) => { + globalThis.clearInterval = unsafeTestValue(((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..dfee15e89 100644 --- a/src/hooks/edit-error-recovery/index.test.ts +++ b/src/hooks/edit-error-recovery/index.test.ts @@ -1,11 +1,12 @@ import { describe, it, expect, beforeEach } from "bun:test" import { createEditErrorRecoveryHook, EDIT_ERROR_REMINDER, EDIT_ERROR_PATTERNS } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("createEditErrorRecoveryHook", () => { let hook: ReturnType beforeEach(() => { - hook = createEditErrorRecoveryHook({} as any) + hook = createEditErrorRecoveryHook(unsafeTestValue({})) }) describe("tool.execute.after", () => { @@ -108,7 +109,7 @@ describe("createEditErrorRecoveryHook", () => { const input = createInput("Edit") const output = { title: "Edit", - output: undefined as unknown as string, + output: unsafeTestValue(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..8dcab9c50 100644 --- a/src/hooks/keyword-detector/hook-ralph-loop.test.ts +++ b/src/hooks/keyword-detector/hook-ralph-loop.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test, beforeEach, afterEach } from "bun:test" import { createKeywordDetectorHook } from "./index" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type StartLoopCall = { sessionID: string @@ -11,13 +12,13 @@ type StartLoopCall = { type CancelLoopCall = { sessionID: string } function createMockPluginInput() { - return { + return unsafeTestValue({ 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..8022eb539 100644 --- a/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts +++ b/src/hooks/keyword-detector/hyperplan-ultrawork.test.ts @@ -4,6 +4,7 @@ import { createKeywordDetectorHook } from "./index" import { setMainSession, _resetForTesting } from "../../features/claude-code-session-state" import * as sharedModule from "../../shared" import * as sessionState from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("keyword-detector hyperplan-ultrawork combo", () => { let logSpy: ReturnType @@ -22,7 +23,7 @@ describe("keyword-detector hyperplan-ultrawork combo", () => { function createMockPluginInput(options: { toastCalls?: string[] } = {}) { const toastCalls = options.toastCalls ?? [] - return { + return unsafeTestValue({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -30,7 +31,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..d1cf485b4 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -7,6 +7,7 @@ import { setMainSession, updateSessionAgent, clearSessionAgent, _resetForTesting import { ContextCollector } from "../../features/context-injector" import * as sharedModule from "../../shared" import * as sessionState from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type ToastOptions = { body: { title: string } } @@ -881,13 +882,13 @@ describe("keyword-detector team mode", () => { }) function createMockPluginInput() { - return { + return unsafeTestValue({ client: { tui: { showToast: async () => {}, }, }, - } as unknown as PluginInput + }) } test("should inject team-mode message when user types 'team mode'", async () => { @@ -1063,7 +1064,7 @@ describe("keyword-detector disabled_keywords config", () => { function createMockPluginInput(options: { toastCalls?: string[] } = {}) { const toastCalls = options.toastCalls ?? [] - return { + return unsafeTestValue({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -1071,7 +1072,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..8faaa52b8 100644 --- a/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts +++ b/src/hooks/keyword-detector/ultrawork-edge-trigger.test.ts @@ -3,6 +3,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { createKeywordDetectorHook } from "./index" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type StartLoopCall = { sessionID: string @@ -11,7 +12,7 @@ type StartLoopCall = { } function createMockPluginInput(toastCalls: string[] = []) { - return { + return unsafeTestValue({ client: { tui: { showToast: async (opts: { body: { title: string } }) => { @@ -19,7 +20,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..ec900124c 100644 --- a/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts +++ b/src/hooks/keyword-detector/ultrawork-runtime-variant.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from "bun:test" import { createKeywordDetectorHook } from "./index" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createMockPluginInput(toastMessages: string[]) { - return { + return unsafeTestValue({ client: { tui: { showToast: async (opts: { body: { message: string } }) => { @@ -11,7 +12,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..550c8cdbd 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -1,3 +1,4 @@ +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" declare const require: (name: string) => any const { beforeEach, describe, expect, mock, test, afterAll } = require("bun:test") @@ -86,12 +87,12 @@ describe("model fallback hook", () => { }) test("applies pending fallback on chat.message by overriding model", async () => { - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "chat.message"?: ( input: { sessionID: string }, output: { message: Record; parts: Array<{ type: string; text?: string }> }, ) => Promise - } + }>(modelFallback) const set = setPendingModelFallback( modelFallback, @@ -122,12 +123,12 @@ describe("model fallback hook", () => { }) test("preserves fallback progression across repeated session.error retries", async () => { - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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 +213,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_noop_skip" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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 +255,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_noop_variant_skip" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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 +300,12 @@ describe("model fallback hook", () => { clearPendingModelFallback(modelFallback, sessionID) readConnectedProvidersCacheMock.mockReturnValue(["provider-x"]) - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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 +356,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 = unsafeTestValue<{ "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 +394,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_ghcp" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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 +435,12 @@ describe("model fallback hook", () => { const sessionID = "ses_model_fallback_google" clearPendingModelFallback(modelFallback, sessionID) - const hook = modelFallback as unknown as { + const hook = unsafeTestValue<{ "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..c28f65bf4 100644 --- a/src/hooks/no-hephaestus-non-gpt/index.test.ts +++ b/src/hooks/no-hephaestus-non-gpt/index.test.ts @@ -4,6 +4,7 @@ import { describe, expect, spyOn, test } from "bun:test" import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state" import { getAgentDisplayName } from "../../shared/agent-display-names" import { createNoHephaestusNonGptHook } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus") const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus") @@ -19,9 +20,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(unsafeTestValue({ client: { tui: { showToast } }, - } as any) + })) const output1 = createOutput() const output2 = createOutput() @@ -54,9 +55,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(unsafeTestValue({ client: { tui: { showToast } }, - } as any, { + }), { allowNonGptModel: true, }) @@ -83,9 +84,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(unsafeTestValue({ client: { tui: { showToast } }, - } as any) + })) const output = createOutput() @@ -104,9 +105,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(unsafeTestValue({ client: { tui: { showToast } }, - } as any) + })) const output = createOutput() @@ -127,9 +128,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(unsafeTestValue({ 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..81565fe44 100644 --- a/src/hooks/no-sisyphus-gpt/index.test.ts +++ b/src/hooks/no-sisyphus-gpt/index.test.ts @@ -5,6 +5,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state" import { getAgentDisplayName } from "../../shared/agent-display-names" import { createNoSisyphusGptHook } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus") const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus") @@ -22,9 +23,9 @@ function createOutput(): HookOutput { } function createHookContext(showToast: (input: unknown) => Promise): PluginInput { - return { + return unsafeTestValue({ 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..bbb666caf 100644 --- a/src/hooks/question-label-truncator/index.test.ts +++ b/src/hooks/question-label-truncator/index.test.ts @@ -1,3 +1,4 @@ +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" import { describe, it, expect } from "bun:test"; import { createQuestionLabelTruncatorHook } from "./index"; @@ -23,10 +24,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output)); // then - const truncatedLabel = (output.args as any).questions[0].options[0].label; + const truncatedLabel = (unsafeTestValue(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 +51,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output)); // then - const resultLabel = (output.args as any).questions[0].options[0].label; + const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label; expect(resultLabel).toBe(shortLabel); }); @@ -74,10 +75,10 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output)); // then - const resultLabel = (output.args as any).questions[0].options[0].label; + const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label; expect(resultLabel).toBe(exactLabel); }); @@ -90,7 +91,7 @@ describe("createQuestionLabelTruncatorHook", () => { const originalArgs = { ...output.args }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output)); // then expect(output.args).toEqual(originalArgs); @@ -120,11 +121,11 @@ describe("createQuestionLabelTruncatorHook", () => { }; // when - await hook["tool.execute.before"]?.(input as any, output as any); + await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output)); // then - const q1opts = (output.args as any).questions[0].options; - const q2opts = (output.args as any).questions[1].options; + const q1opts = (unsafeTestValue(output.args)).questions[0].options; + const q2opts = (unsafeTestValue(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/ralph-loop/completion-promise-detector-test-input.ts b/src/hooks/ralph-loop/completion-promise-detector-test-input.test.ts similarity index 68% rename from src/hooks/ralph-loop/completion-promise-detector-test-input.ts rename to src/hooks/ralph-loop/completion-promise-detector-test-input.test.ts index 4a1e6c259..ca4e4b8a1 100644 --- a/src/hooks/ralph-loop/completion-promise-detector-test-input.ts +++ b/src/hooks/ralph-loop/completion-promise-detector-test-input.test.ts @@ -1,5 +1,6 @@ /// import type { PluginInput } from "@opencode-ai/plugin" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" export type SessionMessage = { info?: { role?: string } @@ -16,8 +17,8 @@ export function createPluginInput(messages: SessionMessage[]): PluginInput { $: {} as PluginInput["$"], } as PluginInput - pluginInput.client.session.messages = - (async () => ({ data: messages })) as unknown as PluginInput["client"]["session"]["messages"] + const messagesFunction = unsafeTestValue(async () => ({ data: messages })) + pluginInput.client.session.messages = messagesFunction return pluginInput } diff --git a/src/hooks/ralph-loop/completion-promise-detector.test.ts b/src/hooks/ralph-loop/completion-promise-detector.test.ts index d59151a7b..2241ddcd0 100644 --- a/src/hooks/ralph-loop/completion-promise-detector.test.ts +++ b/src/hooks/ralph-loop/completion-promise-detector.test.ts @@ -1,7 +1,7 @@ /// import { describe, expect, test } from "bun:test" import { detectCompletionInSessionMessages } from "./completion-promise-detector" -import { createPluginInput } from "./completion-promise-detector-test-input" +import { createPluginInput } from "./completion-promise-detector-test-input.test" describe("detectCompletionInSessionMessages", () => { describe("#given session with prior DONE and new messages", () => { diff --git a/src/hooks/ralph-loop/completion-promise-session-negative.test.ts b/src/hooks/ralph-loop/completion-promise-session-negative.test.ts index 7acd7f879..d33059e05 100644 --- a/src/hooks/ralph-loop/completion-promise-session-negative.test.ts +++ b/src/hooks/ralph-loop/completion-promise-session-negative.test.ts @@ -1,7 +1,7 @@ /// import { describe, expect, test } from "bun:test" import { detectCompletionInSessionMessages } from "./completion-promise-detector" -import { createPluginInput } from "./completion-promise-detector-test-input" +import { createPluginInput } from "./completion-promise-detector-test-input.test" describe("detectCompletionInSessionMessages negative cases", () => { describe("#given natural language completion text without explicit promise", () => { diff --git a/src/hooks/ralph-loop/reset-strategy-race-condition.test.ts b/src/hooks/ralph-loop/reset-strategy-race-condition.test.ts index 15de66084..99e09ab51 100644 --- a/src/hooks/ralph-loop/reset-strategy-race-condition.test.ts +++ b/src/hooks/ralph-loop/reset-strategy-race-condition.test.ts @@ -1,6 +1,7 @@ /// import { describe, expect, test } from "bun:test" import { createRalphLoopHook } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createDeferred(): { promise: Promise @@ -44,7 +45,7 @@ describe("ralph-loop reset strategy race condition", () => { const selectSessionDeferred = createDeferred() const hook = createRalphLoopHook( - { + unsafeTestValue[0]>({ directory: process.cwd(), client: { session: { @@ -86,7 +87,7 @@ describe("ralph-loop reset strategy race condition", () => { }, }, }, - } as unknown as Parameters[0], + }), { idleSettleMs: 0 }, ) diff --git a/src/hooks/ralph-loop/ulw-loop-verification.test.ts b/src/hooks/ralph-loop/ulw-loop-verification.test.ts index 54041f452..2c7499b96 100644 --- a/src/hooks/ralph-loop/ulw-loop-verification.test.ts +++ b/src/hooks/ralph-loop/ulw-loop-verification.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path" import { createRalphLoopHook } from "./index" import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants" import { clearState, writeState } from "./storage" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("ulw-loop verification", () => { const testDir = join(tmpdir(), `ulw-loop-verification-${Date.now()}`) @@ -15,7 +16,7 @@ describe("ulw-loop verification", () => { let oracleTranscriptPath: string function createMockPluginInput() { - return { + return unsafeTestValue[0]>({ client: { session: { promptAsync: async (opts: { path: { id: string }; body: { parts: Array<{ type: string; text: string }> } }) => { @@ -39,7 +40,7 @@ describe("ulw-loop verification", () => { }, }, directory: testDir, - } as unknown as Parameters[0] + }) } beforeEach(() => { diff --git a/src/hooks/runtime-fallback/fallback-models.test.ts b/src/hooks/runtime-fallback/fallback-models.test.ts index ebfa8fbc9..0946a2452 100644 --- a/src/hooks/runtime-fallback/fallback-models.test.ts +++ b/src/hooks/runtime-fallback/fallback-models.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { getFallbackModelsForSession } from "./fallback-models" import { SessionCategoryRegistry } from "../../shared/session-category-registry" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("runtime-fallback fallback-models", () => { afterEach(() => { @@ -12,13 +13,13 @@ describe("runtime-fallback fallback-models", () => { //#given const sessionID = "ses_runtime_fallback_category" SessionCategoryRegistry.register(sessionID, "quick") - const pluginConfig = { + const pluginConfig = unsafeTestValue({ categories: { quick: { fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], }, }, - } as any + }) //#when const result = getFallbackModelsForSession(sessionID, undefined, pluginConfig) @@ -29,13 +30,13 @@ describe("runtime-fallback fallback-models", () => { test("uses agent-specific fallback_models when agent is resolved", () => { //#given - const pluginConfig = { + const pluginConfig = unsafeTestValue({ 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 +47,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 = unsafeTestValue({ agents: { sisyphus: { fallback_models: ["quotio/gpt-5.2", "quotio/glm-5", "quotio/kimi-k2.5"], @@ -55,7 +56,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..45c52bcbe 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config" import * as loggerModule from "../../shared/logger" import { SessionCategoryRegistry } from "../../shared/session-category-registry" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type RuntimeFallbackModule = typeof import("./hook") @@ -41,7 +42,7 @@ describe("runtime-fallback", () => { abort?: (args: unknown) => Promise } }) { - return { + return unsafeTestValue({ client: { tui: { showToast: async (opts: { body: { title: string; message: string; variant: string; duration: number } }) => { @@ -59,7 +60,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..99b08672a 100644 --- a/src/hooks/session-notification-sender.test.ts +++ b/src/hooks/session-notification-sender.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, jest, spyOn, test } from "bun: import * as sender from "./session-notification-sender" import * as utils from "./session-notification-utils" import type { PluginInput } from "@opencode-ai/plugin" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" @@ -80,7 +81,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 = unsafeTestValue({ $: (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 +96,7 @@ describe("session-notification-sender", () => { promise.nothrow = () => promise return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -107,7 +108,7 @@ describe("session-notification-sender", () => { spyOn(utils, "getTerminalNotifierPath").mockResolvedValue(null) const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +131,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -142,9 +143,9 @@ describe("session-notification-sender", () => { spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") const calls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: createShellPromise((cmdStr) => { calls.push(cmdStr) }), - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -157,9 +158,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 = unsafeTestValue({ $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")), - } as unknown as PluginInput + }) const originalFactory = mockCtx.$ const trackingCalls: string[] = [] @@ -180,9 +181,9 @@ describe("session-notification-sender", () => { spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") const trackingCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")), - } as unknown as PluginInput + }) const originalFactory = mockCtx.$ mockCtx.$ = ((cmd: TemplateStringsArray, ...values: unknown[]) => { @@ -200,9 +201,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 = unsafeTestValue({ $: createShellPromise((cmdStr) => { calls.push(cmdStr) }), - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message") @@ -213,7 +214,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on linux notify-send", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +237,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "linux", "Test", "Message") @@ -246,7 +247,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on win32 powershell", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +270,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.sendSessionNotification(mockCtx, "win32", "Test", "Message") @@ -283,7 +284,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 = unsafeTestValue({ $: (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 +307,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "darwin", "/sound.aiff") @@ -316,7 +317,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on linux paplay", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +340,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga") @@ -351,7 +352,7 @@ describe("session-notification-sender", () => { spyOn(utils, "getPaplayPath").mockResolvedValue(null) const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +375,7 @@ describe("session-notification-sender", () => { } return promise }, - } as unknown as PluginInput + }) await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga") @@ -384,7 +385,7 @@ describe("session-notification-sender", () => { test("#then should call .quiet() on win32 powershell sound", async () => { const quietCalls: string[] = [] - const mockCtx = { + const mockCtx = unsafeTestValue({ $: (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 +408,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..6c7fa74b0 100644 --- a/src/hooks/session-recovery/storage/readers-from-sdk.test.ts +++ b/src/hooks/session-recovery/storage/readers-from-sdk.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "bun:test" +import { unsafeTestValue } from "../../../../test-support/unsafe-test-value" async function importFreshReaders() { const token = `${Date.now()}-${Math.random()}` const [{ readMessagesFromSDK, readMessages }, { readPartsFromSDK, readParts }] = await Promise.all([ @@ -13,7 +14,7 @@ function createMockClient(handlers: { messages?: (sessionID: string) => unknown[] message?: (sessionID: string, messageID: string) => unknown }) { - return { + return unsafeTestValue({ session: { messages: async (opts: { path: { id: string } }) => { if (handlers.messages) { @@ -28,7 +29,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..e4a981218 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -16,6 +16,7 @@ import { import type { BoulderState } from "../../features/boulder-state" import * as sessionState from "../../features/claude-code-session-state" import * as worktreeDetector from "./worktree-detector" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("start-work hook", () => { let testDir: string @@ -738,7 +739,7 @@ You are starting a Sisyphus work session. const promptAsyncMock = spyOn({ promptAsync: async (_request: unknown) => undefined, }, "promptAsync") - const ctx = { + const ctx = unsafeTestValue[0]>({ directory: testDir, client: { session: { @@ -747,7 +748,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 +785,18 @@ You are starting a Sisyphus work session. promptAsync: async (_request: unknown) => undefined, }, "promptAsync") - globalThis.setTimeout = ((callback: Function, delay?: number, ...args: unknown[]) => { + globalThis.setTimeout = unsafeTestValue(((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 unsafeTestValue>(id) } return originalSetTimeout(callback as Parameters[0], delay) - }) as unknown as typeof setTimeout + })) - globalThis.clearTimeout = ((id?: number | ReturnType) => { + globalThis.clearTimeout = unsafeTestValue(((id?: number | ReturnType) => { if (typeof id === "number" && capturedTimers.has(id)) { capturedTimers.get(id)!.cleared = true capturedTimers.delete(id) @@ -803,11 +804,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 = unsafeTestValue[0]>({ directory: testDir, client: { session: { @@ -816,13 +817,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: unsafeTestValue[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..7ecf0001a 100644 --- a/src/hooks/stop-continuation-guard/index.test.ts +++ b/src/hooks/stop-continuation-guard/index.test.ts @@ -6,6 +6,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundManager, BackgroundTask } from "../../features/background-agent" import { readContinuationMarker } from "../../features/run-continuation-state" import { createStopContinuationGuardHook } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type CancelCall = { taskId: string @@ -31,14 +32,14 @@ describe("stop-continuation-guard", () => { }) function createMockPluginInput() { - return { + return unsafeTestValue({ 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..3ffb01718 100644 --- a/src/hooks/task-resume-info/index.test.ts +++ b/src/hooks/task-resume-info/index.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "bun:test" import { createTaskResumeInfoHook } from "./index" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("createTaskResumeInfoHook", () => { const hook = createTaskResumeInfoHook() @@ -19,7 +20,7 @@ describe("createTaskResumeInfoHook", () => { const input = createInput("task") const output = { title: "delegate_task", - output: undefined as unknown as string, + output: unsafeTestValue(undefined), metadata: {}, } diff --git a/src/openclaw/__tests__/config.test.ts b/src/openclaw/__tests__/config.test.ts index 62972f45a..60d3effaf 100644 --- a/src/openclaw/__tests__/config.test.ts +++ b/src/openclaw/__tests__/config.test.ts @@ -2,10 +2,11 @@ import { describe, expect, test } from "bun:test" import { resolveGateway, validateGatewayUrl, normalizeReplyListenerConfig } from "../config" import type { OpenClawConfig } from "../types" import { OpenClawConfigSchema } from "../../config/schema/openclaw" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("OpenClaw Config", () => { test("resolveGateway resolves HTTP gateway", () => { - const config: OpenClawConfig = { + const config: OpenClawConfig = unsafeTestValue({ enabled: true, gateways: { discord: { @@ -20,7 +21,7 @@ describe("OpenClaw Config", () => { instruction: "Started session {{sessionId}}", }, }, - } as any + }) const resolved = resolveGateway(config, "session-start") expect(resolved).not.toBeNull() @@ -30,31 +31,31 @@ describe("OpenClaw Config", () => { }) test("resolveGateway returns null for disabled config", () => { - const config: OpenClawConfig = { + const config: OpenClawConfig = unsafeTestValue({ enabled: false, gateways: {}, hooks: {}, - } as any + }) expect(resolveGateway(config, "session-start")).toBeNull() }) test("resolveGateway returns null for unknown hook", () => { - const config: OpenClawConfig = { + const config: OpenClawConfig = unsafeTestValue({ enabled: true, gateways: {}, hooks: {}, - } as any + }) expect(resolveGateway(config, "unknown")).toBeNull() }) test("resolveGateway returns null for disabled hook", () => { - const config: OpenClawConfig = { + const config: OpenClawConfig = unsafeTestValue({ enabled: true, gateways: { g: { type: "http", url: "https://example.com" } }, hooks: { event: { enabled: false, gateway: "g", instruction: "i" }, }, - } as any + }) expect(resolveGateway(config, "event")).toBeNull() }) diff --git a/src/openclaw/__tests__/reply-listener-discord.test.ts b/src/openclaw/__tests__/reply-listener-discord.test.ts index 8fcc77c03..4d01220ca 100644 --- a/src/openclaw/__tests__/reply-listener-discord.test.ts +++ b/src/openclaw/__tests__/reply-listener-discord.test.ts @@ -8,6 +8,7 @@ import * as injectionModule from "../reply-listener-injection" import * as sessionRegistryModule from "../session-registry" import type { ReplyListenerDaemonState } from "../reply-listener-state" import type { OpenClawConfig } from "../types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const originalFetch = globalThis.fetch @@ -75,7 +76,7 @@ describe("pollDiscordReplies", () => { status: 401, }), )) - globalThis.fetch = fetchMock as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(fetchMock) const state = createState() @@ -109,7 +110,7 @@ describe("pollDiscordReplies", () => { ), ) .mockResolvedValueOnce(new Response(null, { status: 204 })) - globalThis.fetch = fetchMock as unknown as typeof fetch + globalThis.fetch = unsafeTestValue(fetchMock) const lookupSpy = spyOn(sessionRegistryModule, "lookupByMessageId").mockReturnValue({ sessionId: "ses-1", tmuxSession: "session-1", diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index d2b0b9ea9..bfc189341 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -22,6 +22,7 @@ import * as modelResolver from "../shared/model-resolver" import * as configErrors from "../shared/config-errors" import * as agentPriorityOrder from "./agent-priority-order" import * as prometheusAgentConfigBuilder from "./prometheus-agent-config-builder" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" let createConfigHandler: (typeof import("./config-handler"))["createConfigHandler"] @@ -46,36 +47,36 @@ beforeEach(async () => { mock.restore() configErrors.clearConfigLoadErrors() - spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({ + spyOn(agents, unsafeTestValue("createBuiltinAgents")).mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, }) - spyOn(commandLoader, "loadUserCommands" as any).mockResolvedValue({}) - spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({}) - spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({}) - spyOn(commandLoader, "loadOpencodeProjectCommands" as any).mockResolvedValue({}) + spyOn(commandLoader, unsafeTestValue("loadUserCommands")).mockResolvedValue({}) + spyOn(commandLoader, unsafeTestValue("loadProjectCommands")).mockResolvedValue({}) + spyOn(commandLoader, unsafeTestValue("loadOpencodeGlobalCommands")).mockResolvedValue({}) + spyOn(commandLoader, unsafeTestValue("loadOpencodeProjectCommands")).mockResolvedValue({}) - spyOn(builtinCommands, "loadBuiltinCommands" as any).mockReturnValue({}) + spyOn(builtinCommands, unsafeTestValue("loadBuiltinCommands")).mockReturnValue({}) - spyOn(skillLoader, "loadUserSkills" as any).mockResolvedValue({}) - spyOn(skillLoader, "loadProjectSkills" as any).mockResolvedValue({}) - spyOn(skillLoader, "loadOpencodeGlobalSkills" as any).mockResolvedValue({}) - spyOn(skillLoader, "loadOpencodeProjectSkills" as any).mockResolvedValue({}) - spyOn(skillLoader, "discoverUserClaudeSkills" as any).mockResolvedValue([]) - spyOn(skillLoader, "discoverProjectClaudeSkills" as any).mockResolvedValue([]) - spyOn(skillLoader, "discoverOpencodeGlobalSkills" as any).mockResolvedValue([]) - spyOn(skillLoader, "discoverOpencodeProjectSkills" as any).mockResolvedValue([]) + spyOn(skillLoader, unsafeTestValue("loadUserSkills")).mockResolvedValue({}) + spyOn(skillLoader, unsafeTestValue("loadProjectSkills")).mockResolvedValue({}) + spyOn(skillLoader, unsafeTestValue("loadOpencodeGlobalSkills")).mockResolvedValue({}) + spyOn(skillLoader, unsafeTestValue("loadOpencodeProjectSkills")).mockResolvedValue({}) + spyOn(skillLoader, unsafeTestValue("discoverUserClaudeSkills")).mockResolvedValue([]) + spyOn(skillLoader, unsafeTestValue("discoverProjectClaudeSkills")).mockResolvedValue([]) + spyOn(skillLoader, unsafeTestValue("discoverOpencodeGlobalSkills")).mockResolvedValue([]) + spyOn(skillLoader, unsafeTestValue("discoverOpencodeProjectSkills")).mockResolvedValue([]) - spyOn(agentLoader, "loadUserAgents" as any).mockReturnValue({}) - spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({}) - spyOn(agentLoader, "loadOpencodeGlobalAgents" as any).mockReturnValue({}) - spyOn(agentLoader, "loadOpencodeProjectAgents" as any).mockReturnValue({}) + spyOn(agentLoader, unsafeTestValue("loadUserAgents")).mockReturnValue({}) + spyOn(agentLoader, unsafeTestValue("loadProjectAgents")).mockReturnValue({}) + spyOn(agentLoader, unsafeTestValue("loadOpencodeGlobalAgents")).mockReturnValue({}) + spyOn(agentLoader, unsafeTestValue("loadOpencodeProjectAgents")).mockReturnValue({}) - spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} }) + spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({ servers: {}, loadedServers: [] }) setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {}) - spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({ + spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockResolvedValue({ commands: {}, skills: {}, agents: {}, @@ -85,54 +86,57 @@ beforeEach(async () => { errors: [], }) - spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({}) + spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({}) - spyOn(shared, "log" as any).mockImplementation(() => {}) - spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"])) - spyOn(shared, "readConnectedProvidersCache" as any).mockReturnValue(null) + spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {}) + spyOn(shared, unsafeTestValue("fetchAvailableModels")).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"])) + spyOn(shared, unsafeTestValue("readConnectedProvidersCache")).mockReturnValue(null) - spyOn(configDir, "getOpenCodeConfigPaths" as any).mockReturnValue({ - global: "/tmp/.config/opencode", - project: "/tmp/.opencode", + spyOn(configDir, unsafeTestValue("getOpenCodeConfigPaths")).mockReturnValue({ + configDir: "/tmp/.config/opencode", + configJson: "/tmp/.config/opencode/opencode.json", + configJsonc: "/tmp/.config/opencode/opencode.jsonc", + packageJson: "/tmp/.config/opencode/package.json", + omoConfig: "/tmp/.config/opencode/oh-my-opencode.jsonc", }) - spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record) => config) + spyOn(permissionCompat, unsafeTestValue("migrateAgentConfig")).mockImplementation((config: Record) => config) - spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-7" }) + spyOn(modelResolver, unsafeTestValue("resolveModelWithFallback")).mockReturnValue({ model: "anthropic/claude-opus-4-7", source: "provider-fallback" }) ;({ createConfigHandler } = await importFreshConfigHandlerModule()) }) afterEach(() => { - (agents.createBuiltinAgents as any)?.mockRestore?.() - ;(sisyphusJunior.createSisyphusJuniorAgentWithOverrides as any)?.mockRestore?.() - ;(commandLoader.loadUserCommands as any)?.mockRestore?.() - ;(commandLoader.loadProjectCommands as any)?.mockRestore?.() - ;(commandLoader.loadOpencodeGlobalCommands as any)?.mockRestore?.() - ;(commandLoader.loadOpencodeProjectCommands as any)?.mockRestore?.() - ;(builtinCommands.loadBuiltinCommands as any)?.mockRestore?.() - ;(skillLoader.loadUserSkills as any)?.mockRestore?.() - ;(skillLoader.loadProjectSkills as any)?.mockRestore?.() - ;(skillLoader.loadOpencodeGlobalSkills as any)?.mockRestore?.() - ;(skillLoader.loadOpencodeProjectSkills as any)?.mockRestore?.() - ;(skillLoader.discoverUserClaudeSkills as any)?.mockRestore?.() - ;(skillLoader.discoverProjectClaudeSkills as any)?.mockRestore?.() - ;(skillLoader.discoverOpencodeGlobalSkills as any)?.mockRestore?.() - ;(skillLoader.discoverOpencodeProjectSkills as any)?.mockRestore?.() - ;(agentLoader.loadUserAgents as any)?.mockRestore?.() - ;(agentLoader.loadProjectAgents as any)?.mockRestore?.() - ;(agentLoader.loadOpencodeGlobalAgents as any)?.mockRestore?.() - ;(agentLoader.loadOpencodeProjectAgents as any)?.mockRestore?.() - ;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.() + (unsafeTestValue(agents.createBuiltinAgents))?.mockRestore?.() + ;(unsafeTestValue(sisyphusJunior.createSisyphusJuniorAgentWithOverrides))?.mockRestore?.() + ;(unsafeTestValue(commandLoader.loadUserCommands))?.mockRestore?.() + ;(unsafeTestValue(commandLoader.loadProjectCommands))?.mockRestore?.() + ;(unsafeTestValue(commandLoader.loadOpencodeGlobalCommands))?.mockRestore?.() + ;(unsafeTestValue(commandLoader.loadOpencodeProjectCommands))?.mockRestore?.() + ;(unsafeTestValue(builtinCommands.loadBuiltinCommands))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.loadUserSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.loadProjectSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.loadOpencodeGlobalSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.loadOpencodeProjectSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.discoverUserClaudeSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.discoverProjectClaudeSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.discoverOpencodeGlobalSkills))?.mockRestore?.() + ;(unsafeTestValue(skillLoader.discoverOpencodeProjectSkills))?.mockRestore?.() + ;(unsafeTestValue(agentLoader.loadUserAgents))?.mockRestore?.() + ;(unsafeTestValue(agentLoader.loadProjectAgents))?.mockRestore?.() + ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents))?.mockRestore?.() + ;(unsafeTestValue(agentLoader.loadOpencodeProjectAgents))?.mockRestore?.() + ;(unsafeTestValue(mcpLoader.loadMcpConfigs))?.mockRestore?.() setAdditionalAllowedMcpEnvVarsSpy?.mockRestore() - ;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.() - ;(mcpModule.createBuiltinMcps as any)?.mockRestore?.() - ;(shared.log as any)?.mockRestore?.() - ;(shared.fetchAvailableModels as any)?.mockRestore?.() - ;(shared.readConnectedProvidersCache as any)?.mockRestore?.() - ;(configDir.getOpenCodeConfigPaths as any)?.mockRestore?.() - ;(permissionCompat.migrateAgentConfig as any)?.mockRestore?.() - ;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.() - ;(agentPriorityOrder.reorderAgentsByPriority as any)?.mockRestore?.() + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents))?.mockRestore?.() + ;(unsafeTestValue(mcpModule.createBuiltinMcps))?.mockRestore?.() + ;(unsafeTestValue(shared.log))?.mockRestore?.() + ;(unsafeTestValue(shared.fetchAvailableModels))?.mockRestore?.() + ;(unsafeTestValue(shared.readConnectedProvidersCache))?.mockRestore?.() + ;(unsafeTestValue(configDir.getOpenCodeConfigPaths))?.mockRestore?.() + ;(unsafeTestValue(permissionCompat.migrateAgentConfig))?.mockRestore?.() + ;(unsafeTestValue(modelResolver.resolveModelWithFallback))?.mockRestore?.() + ;(unsafeTestValue(agentPriorityOrder.reorderAgentsByPriority))?.mockRestore?.() configErrors.clearConfigLoadErrors() mock.restore() }) @@ -230,10 +234,10 @@ describe("MCP env allowlist initialization", () => { describe("Plan agent demote behavior", () => { test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => { // #given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, @@ -275,17 +279,17 @@ describe("Plan agent demote behavior", () => { test("assembles core agents first before priority reorder runs", async () => { // #given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, atlas: { name: "atlas", prompt: "test", mode: "primary" }, }) - const reorderSpy = spyOn(agentPriorityOrder, "reorderAgentsByPriority") as any + const reorderSpy = unsafeTestValue(spyOn(agentPriorityOrder, "reorderAgentsByPriority")) const pluginConfig = createPluginConfig({ sisyphus_agent: { planner_enabled: true, @@ -321,9 +325,9 @@ describe("Plan agent demote behavior", () => { test("backfills runtime core agent names when builtin configs omit name", async () => { // #given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { prompt: "test", mode: "primary" }, hephaestus: { prompt: "test", mode: "primary" }, @@ -485,9 +489,9 @@ describe("Plan agent demote behavior", () => { describe("Agent permission defaults", () => { test("hephaestus should allow task", async () => { // #given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, @@ -1054,7 +1058,7 @@ describe("Plan agent model inheritance from prometheus", () => { test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => { //#given - prometheus configured with category that has temperature and reasoningEffort - spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({ model: "openai/gpt-5.4", provenance: "override", variant: "high", @@ -1109,7 +1113,7 @@ describe("Plan agent model inheritance from prometheus", () => { test("plan agent user override takes priority over prometheus inherited settings", async () => { //#given - prometheus resolves to opus, but user has plan override for gpt-5.4 - spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({ model: "anthropic/claude-opus-4-7", provenance: "provider-fallback", variant: "max", @@ -1152,7 +1156,7 @@ describe("Plan agent model inheritance from prometheus", () => { test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => { //#given - spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ + spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({ model: "anthropic/claude-opus-4-7", provenance: "provider-fallback", variant: "max", @@ -1229,8 +1233,10 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", ( describe("config-handler plugin loading error boundary (#1559)", () => { test("returns empty defaults when loadAllPluginComponents throws", async () => { //#given - ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() - spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash")) + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.() + spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation(async () => { + throw new Error("crash") + }) const pluginConfig = createPluginConfig({}) const config: Record = { model: "anthropic/claude-opus-4-7", @@ -1255,8 +1261,8 @@ describe("config-handler plugin loading error boundary (#1559)", () => { test("returns empty defaults when loadAllPluginComponents times out", async () => { //#given - ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() - spyOn(pluginLoader, "loadAllPluginComponents" as any).mockImplementation( + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.() + spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation( () => new Promise(() => {}) ) const pluginConfig = createPluginConfig({ @@ -1285,8 +1291,10 @@ describe("config-handler plugin loading error boundary (#1559)", () => { test("records a config load error when loadAllPluginComponents fails", async () => { //#given - ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() - spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash")) + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.() + spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation(async () => { + throw new Error("crash") + }) const pluginConfig = createPluginConfig({}) const config: Record = { model: "anthropic/claude-opus-4-7", @@ -1314,14 +1322,14 @@ describe("config-handler plugin loading error boundary (#1559)", () => { test("passes through plugin data on successful load (identity test)", async () => { //#given - ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() - spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({ - commands: { "test-cmd": { description: "test", template: "test" } }, + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.() + spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockResolvedValue({ + commands: { "test-cmd": { name: "test-cmd", description: "test", template: "test" } }, skills: {}, agents: {}, mcpServers: {}, hooksConfigs: [], - plugins: [{ name: "test-plugin", version: "1.0.0" }], + plugins: [{ name: "test-plugin", version: "1.0.0", scope: "project", installPath: "/tmp/test-plugin", pluginKey: "test-plugin" }], errors: [], }) const pluginConfig = createPluginConfig({}) @@ -1351,16 +1359,16 @@ describe("config-handler plugin loading error boundary (#1559)", () => { describe("command agent routing coherence", () => { test("keeps start-work aligned with the exported Atlas list key opencode matches exactly", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, atlas: { name: "atlas", prompt: "test", mode: "primary" }, }) - ;(builtinCommands.loadBuiltinCommands as unknown as { + ;(unsafeTestValue<{ mockReturnValue: (value: Record) => void - }).mockReturnValue({ + }>(builtinCommands.loadBuiltinCommands)).mockReturnValue({ "start-work": { name: "start-work", description: "(builtin) Start work", @@ -1404,9 +1412,9 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => { test("denies todowrite and todoread for primary agents when task_system is enabled", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, @@ -1445,10 +1453,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => { test("does not deny todowrite/todoread when task_system is disabled", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, @@ -1487,10 +1495,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => { test("does not deny todowrite/todoread when task_system is undefined", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, }) @@ -1526,10 +1534,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => { describe("disable_omo_env pass-through", () => { test("passes disable_omo_env=true to createBuiltinAgents", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "without-env", mode: "primary" }, }) @@ -1563,10 +1571,10 @@ describe("disable_omo_env pass-through", () => { test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => { //#given - const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { + const createBuiltinAgentsMock = unsafeTestValue<{ mockResolvedValue: (value: Record) => void mock: { calls: unknown[][] } - } + }>(agents.createBuiltinAgents) createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "with-env", mode: "primary" }, }) @@ -1600,14 +1608,14 @@ describe("disable_omo_env pass-through", () => { describe("Agent merge priority — project-local overrides global", () => { test("project-local Claude agent overrides global Claude agent with same name", async () => { // #given — same agent name in both global (user) and project scopes - ;(agentLoader.loadUserAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadUserAgents)).mockReturnValue({ "my-custom-agent": { description: "(user) global version", mode: "subagent", prompt: "I am the global agent", }, }) - ;(agentLoader.loadProjectAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadProjectAgents)).mockReturnValue({ "my-custom-agent": { description: "(project) project version", mode: "subagent", @@ -1615,7 +1623,7 @@ describe("Agent merge priority — project-local overrides global", () => { }, }) - const pluginConfig: OhMyOpenCodeConfig = {} + const pluginConfig = createPluginConfig() const config: Record = { model: "anthropic/claude-opus-4-7", agent: {}, @@ -1640,14 +1648,14 @@ describe("Agent merge priority — project-local overrides global", () => { test("opencode project agent overrides opencode global agent with same name", async () => { // #given — same agent name in opencode global vs opencode project - ;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({ "my-custom-agent": { description: "(opencode) global version", mode: "subagent", prompt: "I am the opencode global agent", }, }) - ;(agentLoader.loadOpencodeProjectAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadOpencodeProjectAgents)).mockReturnValue({ "my-custom-agent": { description: "(opencode-project) project version", mode: "subagent", @@ -1655,7 +1663,7 @@ describe("Agent merge priority — project-local overrides global", () => { }, }) - const pluginConfig: OhMyOpenCodeConfig = {} + const pluginConfig = createPluginConfig() const config: Record = { model: "anthropic/claude-opus-4-7", agent: {}, @@ -1680,14 +1688,14 @@ describe("Agent merge priority — project-local overrides global", () => { test("project Claude agent overrides opencode global agent with same name", async () => { // #given — project-scope Claude agent vs global-scope opencode agent - ;(agentLoader.loadOpencodeGlobalAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({ "my-custom-agent": { description: "(opencode) global version", mode: "subagent", prompt: "I am the opencode global agent", }, }) - ;(agentLoader.loadProjectAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadProjectAgents)).mockReturnValue({ "my-custom-agent": { description: "(project) project version", mode: "subagent", @@ -1695,7 +1703,7 @@ describe("Agent merge priority — project-local overrides global", () => { }, }) - const pluginConfig: OhMyOpenCodeConfig = {} + const pluginConfig = createPluginConfig() const config: Record = { model: "anthropic/claude-opus-4-7", agent: {}, @@ -1720,7 +1728,7 @@ describe("Agent merge priority — project-local overrides global", () => { test("plugin agents have lowest priority — overridden by all other sources", async () => { // #given — same agent in plugin, global, and project scopes - ;(pluginLoader.loadAllPluginComponents as any).mockResolvedValue({ + ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockResolvedValue({ commands: {}, skills: {}, agents: { @@ -1735,7 +1743,7 @@ describe("Agent merge priority — project-local overrides global", () => { plugins: [], errors: [], }) - ;(agentLoader.loadUserAgents as any).mockReturnValue({ + ;(unsafeTestValue(agentLoader.loadUserAgents)).mockReturnValue({ "my-custom-agent": { description: "(user) global version", mode: "subagent", @@ -1743,7 +1751,7 @@ describe("Agent merge priority — project-local overrides global", () => { }, }) - const pluginConfig: OhMyOpenCodeConfig = {} + const pluginConfig = createPluginConfig() const config: Record = { model: "anthropic/claude-opus-4-7", agent: {}, diff --git a/src/plugin-handlers/mcp-config-handler.test.ts b/src/plugin-handlers/mcp-config-handler.test.ts index f9fc6472f..217ca303e 100644 --- a/src/plugin-handlers/mcp-config-handler.test.ts +++ b/src/plugin-handlers/mcp-config-handler.test.ts @@ -6,22 +6,23 @@ import type { OhMyOpenCodeConfig } from "../config" import * as mcpLoader from "../features/claude-code-mcp-loader" import * as mcpModule from "../mcp" import * as shared from "../shared" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" let loadMcpConfigsSpy: ReturnType let createBuiltinMcpsSpy: ReturnType beforeEach(() => { - loadMcpConfigsSpy = spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ + loadMcpConfigsSpy = spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({ servers: {}, }) - createBuiltinMcpsSpy = spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({}) - spyOn(shared, "log" as any).mockImplementation(() => {}) + createBuiltinMcpsSpy = spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({}) + spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {}) }) afterEach(() => { loadMcpConfigsSpy.mockRestore() createBuiltinMcpsSpy.mockRestore() - ;(shared.log as any)?.mockRestore?.() + ;(unsafeTestValue(shared.log))?.mockRestore?.() }) function createPluginConfig(overrides: Partial = {}): OhMyOpenCodeConfig { @@ -82,7 +83,7 @@ describe("applyMcpConfig", () => { }) const config: Record = { mcp: {} } - const pluginConfig = createPluginConfig({ disabled_mcps: ["playwright"] as any }) + const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["playwright"]) }) //#when const { applyMcpConfig } = await import("./mcp-config-handler") @@ -107,7 +108,7 @@ describe("applyMcpConfig", () => { test("passes disabled_mcps to loadMcpConfigs", async () => { //#given const config: Record = { mcp: {} } - const pluginConfig = createPluginConfig({ disabled_mcps: ["firecrawl", "exa"] as any }) + const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["firecrawl", "exa"]) }) //#when const { applyMcpConfig } = await import("./mcp-config-handler") @@ -145,7 +146,7 @@ describe("applyMcpConfig", () => { test("deletes plugin MCPs that are in disabled_mcps", async () => { //#given const config: Record = { mcp: {} } - const pluginConfig = createPluginConfig({ disabled_mcps: ["plugin:custom"] as any }) + const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["plugin:custom"]) }) //#when const { applyMcpConfig } = await import("./mcp-config-handler") diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts index ab6725bec..1957700f8 100644 --- a/src/plugin/chat-message.test.ts +++ b/src/plugin/chat-message.test.ts @@ -13,6 +13,7 @@ import { _resetForTesting, setMainSession, subagentSessions, registerAgentName, import { getAgentListDisplayName } from "../shared/agent-display-names" import { getOmoOpenCodeCacheDir, getOpenCodeCacheDir } from "../shared/data-path" import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } type ChatMessageHandlerOutput = { message: Record; parts: ChatMessagePart[] } @@ -56,13 +57,13 @@ function createMockHandlerArgs(overrides?: { }) { const appliedSessions: string[] = [] return { - ctx: { client: { tui: { showToast: async () => {} } } } as any, - pluginConfig: (overrides?.pluginConfig ?? {}) as any, + ctx: unsafeTestValue({ client: { tui: { showToast: async () => {} } } }), + pluginConfig: unsafeTestValue((overrides?.pluginConfig ?? {})), firstMessageVariantGate: { shouldOverride: () => overrides?.shouldOverride ?? false, markApplied: (sessionID: string) => { appliedSessions.push(sessionID) }, }, - hooks: { + hooks: unsafeTestValue({ stopContinuationGuard: null, backgroundNotificationHook: null, keywordDetector: null, @@ -70,7 +71,7 @@ function createMockHandlerArgs(overrides?: { autoSlashCommand: null, startWork: null, ralphLoop: null, - } as any, + }), _appliedSessions: appliedSessions, } } diff --git a/src/plugin/event.model-fallback-2941.test.ts b/src/plugin/event.model-fallback-2941.test.ts index 2b97d2cb7..143c5e09e 100644 --- a/src/plugin/event.model-fallback-2941.test.ts +++ b/src/plugin/event.model-fallback-2941.test.ts @@ -6,6 +6,7 @@ import { createChatMessageHandler } from "./chat-message" import { _resetForTesting, setSessionAgent } from "../features/claude-code-session-state" import { clearPendingModelFallback, createModelFallbackHook, setSessionFallbackChain } from "../hooks/model-fallback/hook" import * as connectedProvidersCache from "../shared/connected-providers-cache" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" type EventInput = { event: { type: string; properties?: unknown } } type EventHandlerArgs = Parameters[0] @@ -13,27 +14,27 @@ type EventHandlerInput = Parameters>[0] type ChatMessageHandlerArgs = Parameters[0] function asEventHandlerInput(input: EventInput): EventHandlerInput { - return input as unknown as EventHandlerInput + return unsafeTestValue(input) } function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] { - return ctx as unknown as EventHandlerArgs["ctx"] + return unsafeTestValue(ctx) } function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] { - return config as unknown as EventHandlerArgs["pluginConfig"] + return unsafeTestValue(config) } function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] { - return ctx as unknown as ChatMessageHandlerArgs["ctx"] + return unsafeTestValue(ctx) } function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] { - return config as unknown as ChatMessageHandlerArgs["pluginConfig"] + return unsafeTestValue(config) } function createEventHandlerManagers(): EventHandlerArgs["managers"] { - return { + return unsafeTestValue({ tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, @@ -41,17 +42,17 @@ function createEventHandlerManagers(): EventHandlerArgs["managers"] { skillMcpManager: { disconnectSession: async () => {}, }, - } as unknown as EventHandlerArgs["managers"] + }) } function createEventHandlerHooks(modelFallback: ReturnType): EventHandlerArgs["hooks"] { - return { + return unsafeTestValue({ modelFallback, - } as unknown as EventHandlerArgs["hooks"] + }) } function createChatMessageHandlerHooks(modelFallback: ReturnType): ChatMessageHandlerArgs["hooks"] { - return { + return unsafeTestValue({ modelFallback, stopContinuationGuard: null, keywordDetector: null, @@ -59,7 +60,7 @@ function createChatMessageHandlerHooks(modelFallback: ReturnType void } | undefined diff --git a/src/plugin/event.model-fallback-pin-agent.test.ts b/src/plugin/event.model-fallback-pin-agent.test.ts index db29bcb0e..b9adbdfee 100644 --- a/src/plugin/event.model-fallback-pin-agent.test.ts +++ b/src/plugin/event.model-fallback-pin-agent.test.ts @@ -5,6 +5,7 @@ import { createEventHandler } from "./event" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import * as connectedProvidersCache from "../shared/connected-providers-cache" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined @@ -50,16 +51,16 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model } const handler = createEventHandler({ - ctx: { + ctx: unsafeTestValue({ directory: "/tmp", client: { session: sessionClient }, - } as any, - pluginConfig: (args?.pluginConfig ?? {}) as any, + }), + pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { + managers: unsafeTestValue({ tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, @@ -67,8 +68,8 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model skillMcpManager: { disconnectSession: async () => {}, }, - } as any, - hooks: args?.hooks ?? ({} as any), + }), + hooks: args?.hooks ?? (unsafeTestValue({})), }) return { handler, promptAsyncBodies, promptBodies } diff --git a/src/plugin/event.model-fallback.test.ts b/src/plugin/event.model-fallback.test.ts index 3ff82ae20..654bffbcc 100644 --- a/src/plugin/event.model-fallback.test.ts +++ b/src/plugin/event.model-fallback.test.ts @@ -6,6 +6,7 @@ import { createChatMessageHandler } from "./chat-message" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import * as connectedProvidersCache from "../shared/connected-providers-cache" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined @@ -22,7 +23,7 @@ describe("createEventHandler - model fallback", () => { const promptCalls: string[] = [] const handler = createEventHandler({ - ctx: { + ctx: unsafeTestValue({ directory: "/tmp", client: { session: { @@ -36,13 +37,13 @@ describe("createEventHandler - model fallback", () => { }, }, }, - } as any, - pluginConfig: (args?.pluginConfig ?? {}) as any, + }), + pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { + managers: unsafeTestValue({ tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, @@ -50,8 +51,8 @@ describe("createEventHandler - model fallback", () => { skillMcpManager: { disconnectSession: async () => {}, }, - } as any, - hooks: args?.hooks ?? ({} as any), + }), + hooks: args?.hooks ?? (unsafeTestValue({})), }) return { handler, abortCalls, promptCalls } @@ -148,19 +149,19 @@ describe("createEventHandler - model fallback", () => { const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback } }) const chatMessageHandler = createChatMessageHandler({ - ctx: { + ctx: unsafeTestValue({ client: { tui: { showToast: async () => ({}), }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: unsafeTestValue({}), firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, }, - hooks: { + hooks: unsafeTestValue({ modelFallback, stopContinuationGuard: null, keywordDetector: null, @@ -168,7 +169,7 @@ describe("createEventHandler - model fallback", () => { autoSlashCommand: null, startWork: null, ralphLoop: null, - } as any, + }), }) await handler({ @@ -358,19 +359,19 @@ describe("createEventHandler - model fallback", () => { const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback }, pluginConfig }) const chatMessageHandler = createChatMessageHandler({ - ctx: { + ctx: unsafeTestValue({ client: { tui: { showToast: async () => ({}), }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: unsafeTestValue({}), firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, }, - hooks: { + hooks: unsafeTestValue({ modelFallback, stopContinuationGuard: null, keywordDetector: null, @@ -378,7 +379,7 @@ describe("createEventHandler - model fallback", () => { autoSlashCommand: null, startWork: null, ralphLoop: null, - } as any, + }), }) await handler({ @@ -449,7 +450,7 @@ describe("createEventHandler - model fallback", () => { setupConnectedProviderCacheMocks() const eventHandler = createEventHandler({ - ctx: { + ctx: unsafeTestValue({ directory: "/tmp", client: { session: { @@ -463,13 +464,13 @@ describe("createEventHandler - model fallback", () => { }, }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: unsafeTestValue({}), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, }, - managers: { + managers: unsafeTestValue({ tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, @@ -477,14 +478,14 @@ describe("createEventHandler - model fallback", () => { skillMcpManager: { disconnectSession: async () => {}, }, - } as any, - hooks: { + }), + hooks: unsafeTestValue({ modelFallback, - } as any, + }), }) const chatMessageHandler = createChatMessageHandler({ - ctx: { + ctx: unsafeTestValue({ client: { tui: { showToast: async ({ body }: { body: { title?: string } }) => { @@ -493,13 +494,13 @@ describe("createEventHandler - model fallback", () => { }, }, }, - } as any, - pluginConfig: {} as any, + }), + pluginConfig: unsafeTestValue({}), firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, }, - hooks: { + hooks: unsafeTestValue({ modelFallback, stopContinuationGuard: null, keywordDetector: null, @@ -507,7 +508,7 @@ describe("createEventHandler - model fallback", () => { autoSlashCommand: null, startWork: null, ralphLoop: null, - } as any, + }), }) const triggerRetryCycle = async () => { diff --git a/src/plugin/fallback.cliproxyapi-matrix.test.ts b/src/plugin/fallback.cliproxyapi-matrix.test.ts index 3d1b5fd4c..089a2680c 100644 --- a/src/plugin/fallback.cliproxyapi-matrix.test.ts +++ b/src/plugin/fallback.cliproxyapi-matrix.test.ts @@ -11,6 +11,7 @@ import type { RuntimeFallbackPluginInput } from "../hooks/runtime-fallback/types import { _resetForTesting } from "../features/claude-code-session-state" import { SessionCategoryRegistry } from "../shared/session-category-registry" import * as connectedProvidersCache from "../shared/connected-providers-cache" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" type EventHandlerArgs = Parameters[0] type ChatMessageHandlerArgs = Parameters[0] @@ -18,42 +19,42 @@ type HarnessContext = EventHandlerArgs["ctx"] & RuntimeFallbackPluginInput type HarnessEventInput = Parameters["eventHandler"]>[0] function asHarnessEventInput(input: unknown): HarnessEventInput { - return input as unknown as HarnessEventInput + return unsafeTestValue(input) } function asHarnessContext(ctx: unknown): HarnessContext { - return ctx as unknown as HarnessContext + return unsafeTestValue(ctx) } function createEventHandlerManagers( overrides: Record = {}, ): EventHandlerArgs["managers"] { - return { + return unsafeTestValue({ ...({} as EventHandlerArgs["managers"]), tmuxSessionManager: { onSessionCreated: async () => {}, onSessionDeleted: async () => {}, }, ...overrides, - } as unknown as EventHandlerArgs["managers"] + }) } function createEventHandlerHooks( overrides: Record, ): EventHandlerArgs["hooks"] { - return { + return unsafeTestValue({ ...({} as EventHandlerArgs["hooks"]), ...overrides, - } as unknown as EventHandlerArgs["hooks"] + }) } function createChatMessageHandlerHooks( overrides: Record, ): ChatMessageHandlerArgs["hooks"] { - return { + return unsafeTestValue({ ...({} as ChatMessageHandlerArgs["hooks"]), ...overrides, - } as unknown as ChatMessageHandlerArgs["hooks"] + }) } const PRIMARY_MODEL = { @@ -87,7 +88,7 @@ let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined function createPluginConfig(mode: HarnessMode) { - return { + return unsafeTestValue({ agents: { sisyphus: { fallback_models: CLIPROXYAPI_FALLBACKS, @@ -100,7 +101,7 @@ function createPluginConfig(mode: HarnessMode) { }, } : {}), - } as unknown as EventHandlerArgs["pluginConfig"] + }) } function createHarness(args: { @@ -187,14 +188,14 @@ function createHarness(args: { timeout_seconds: args.sessionTimeoutMs ? 30 : 0, notify_on_fallback: false, }, - pluginConfig: pluginConfig as unknown as EventHandlerArgs["pluginConfig"], + pluginConfig: unsafeTestValue(pluginConfig), ...(args.sessionTimeoutMs ? { session_timeout_ms: args.sessionTimeoutMs } : {}), }) } const eventHandler = createEventHandler({ ctx, - pluginConfig: pluginConfig as unknown as EventHandlerArgs["pluginConfig"], + pluginConfig: unsafeTestValue(pluginConfig), firstMessageVariantGate: { markSessionCreated: () => {}, clear: () => {}, @@ -209,7 +210,7 @@ function createHarness(args: { const chatMessageHandler = createChatMessageHandler({ ctx, - pluginConfig: pluginConfig as unknown as ChatMessageHandlerArgs["pluginConfig"], + pluginConfig: unsafeTestValue(pluginConfig), firstMessageVariantGate: { shouldOverride: () => false, markApplied: () => {}, diff --git a/src/plugin/hooks/create-session-hooks.test.ts b/src/plugin/hooks/create-session-hooks.test.ts index ab6b5ad3b..42a091f4f 100644 --- a/src/plugin/hooks/create-session-hooks.test.ts +++ b/src/plugin/hooks/create-session-hooks.test.ts @@ -3,8 +3,9 @@ import type { OhMyOpenCodeConfig } from "../../config" import type { ModelCacheState } from "../../plugin-state" import type { PluginContext } from "../types" import { createSessionHooks } from "./create-session-hooks" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" -const mockContext = { +const mockContext = unsafeTestValue({ directory: "/tmp", client: { tui: { @@ -15,7 +16,7 @@ const mockContext = { update: async () => ({}), }, }, -} as unknown as PluginContext +}) const mockModelCacheState = {} as ModelCacheState diff --git a/src/plugin/tool-execute-before.ulw-loop.test.ts b/src/plugin/tool-execute-before.ulw-loop.test.ts index d4283c044..8a9994d1b 100644 --- a/src/plugin/tool-execute-before.ulw-loop.test.ts +++ b/src/plugin/tool-execute-before.ulw-loop.test.ts @@ -6,6 +6,7 @@ import { createToolExecuteAfterHandler } from "./tool-execute-after" import { createToolExecuteBeforeHandler } from "./tool-execute-before" import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants" import { clearState, readState, writeState } from "../hooks/ralph-loop/storage" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" describe("tool.execute.before ultrawork oracle verification", () => { function createCtx(directory: string) { @@ -56,7 +57,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { }) const handler = createToolExecuteBeforeHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) const output = { args: createOracleTaskArgs("Check it") } @@ -78,7 +79,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { const directory = join(tmpdir(), `tool-before-ulw-${Date.now()}-plain`) mkdirSync(directory, { recursive: true }) const handler = createToolExecuteBeforeHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) const output = { args: createOracleTaskArgs("Check it") } @@ -96,8 +97,8 @@ describe("tool.execute.before ultrawork oracle verification", () => { mkdirSync(directory, { recursive: true }) const startLoopCalls: Array<{ sessionID: string; prompt: string; options: Record }> = [] const handler = createToolExecuteBeforeHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], - hooks: { + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), + hooks: unsafeTestValue[0]["hooks"]>({ ralphLoop: { startLoop: (sessionID: string, prompt: string, options?: Record) => { startLoopCalls.push({ sessionID, prompt, options: options ?? {} }) @@ -106,7 +107,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { cancelLoop: () => true, getState: () => null, }, - } as unknown as Parameters[0]["hooks"], + }), }) const output = { args: { @@ -148,7 +149,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { }) const beforeHandler = createToolExecuteBeforeHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) const beforeOutput = { args: createOracleTaskArgs("Check it") } @@ -156,7 +157,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { const metadataFromSyncTask = createSyncTaskMetadata(beforeOutput.args, "ses-oracle") const handler = createToolExecuteAfterHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) @@ -191,7 +192,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { }) const handler = createToolExecuteAfterHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) @@ -230,7 +231,7 @@ describe("tool.execute.before ultrawork oracle verification", () => { }) const handler = createToolExecuteAfterHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) @@ -269,11 +270,11 @@ describe("tool.execute.before ultrawork oracle verification", () => { }) const beforeHandler = createToolExecuteBeforeHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) const afterHandler = createToolExecuteAfterHandler({ - ctx: createCtx(directory) as unknown as Parameters[0]["ctx"], + ctx: unsafeTestValue[0]["ctx"]>(createCtx(directory)), hooks: {} as Parameters[0]["hooks"], }) diff --git a/src/plugin/ultrawork-model-override.test.ts b/src/plugin/ultrawork-model-override.test.ts index b37dc1285..1ec45978e 100644 --- a/src/plugin/ultrawork-model-override.test.ts +++ b/src/plugin/ultrawork-model-override.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" import * as sharedModule from "../shared" import * as dbOverrideModule from "./ultrawork-db-model-override" import * as sessionStateModule from "../features/claude-code-session-state" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" let resolveUltraworkOverride: (typeof import("./ultrawork-model-override"))["resolveUltraworkOverride"] let detectUltrawork: (typeof import("./ultrawork-model-override"))["detectUltrawork"] @@ -70,11 +71,11 @@ describe("resolveUltraworkOverride", () => { } function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { - return { + return unsafeTestValue[0]>({ agents: { [agentName]: { ultrawork }, }, - } as unknown as Parameters[0] + }) } test("should resolve override when ultrawork keyword detected", () => { @@ -139,9 +140,9 @@ describe("resolveUltraworkOverride", () => { test("should return null when agent has no ultrawork config", () => { //#given - const config = { + const config = unsafeTestValue[0]>({ agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } }, - } as unknown as Parameters[0] + }) const output = createOutput("ultrawork do something") //#when @@ -278,11 +279,11 @@ describe("applyUltraworkModelOverrideOnMessage", () => { } function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { - return { + return unsafeTestValue[0]>({ agents: { [agentName]: { ultrawork }, }, - } as unknown as Parameters[0] + }) } test("should schedule deferred DB override without variant when SDK unavailable", () => { diff --git a/src/shared/frontmatter.test.ts b/src/shared/frontmatter.test.ts index a4e7e4750..19225086b 100644 --- a/src/shared/frontmatter.test.ts +++ b/src/shared/frontmatter.test.ts @@ -216,20 +216,23 @@ Body content` agent: string } + interface FrontmatterWithExtras extends MinimalMeta { + extra_field: string + another_extra: { nested: string; array: string[] } + custom_boolean: boolean + custom_number: number + } + // when - const result = parseFrontmatter(content) + const result = parseFrontmatter(content) // then expect(result.data.description).toBe("Test command") expect(result.data.agent).toBe("build") expect(result.body).toBe("Body content") - // @ts-expect-error - accessing extra field not in MinimalMeta expect(result.data.extra_field).toBe("should not fail") - // @ts-expect-error - accessing extra field not in MinimalMeta expect(result.data.another_extra).toEqual({ nested: "value", array: ["item1", "item2"] }) - // @ts-expect-error - accessing extra field not in MinimalMeta expect(result.data.custom_boolean).toBe(true) - // @ts-expect-error - accessing extra field not in MinimalMeta expect(result.data.custom_number).toBe(42) }) diff --git a/src/shared/git-worktree/collect-git-diff-stats.test.ts b/src/shared/git-worktree/collect-git-diff-stats.test.ts index e74148bf2..f16a28219 100644 --- a/src/shared/git-worktree/collect-git-diff-stats.test.ts +++ b/src/shared/git-worktree/collect-git-diff-stats.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" import * as childProcess from "node:child_process" import * as fs from "node:fs" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("collectGitDiffStats", () => { let execFileSyncSpy: ReturnType @@ -52,7 +53,7 @@ describe("collectGitDiffStats", () => { expect(execSyncSpy).not.toHaveBeenCalled() expect(execFileSyncSpy.mock.calls.length).toBeGreaterThanOrEqual(3) - const calls = execFileSyncSpy.mock.calls as unknown as Array<[string, string[], { cwd?: string }]> + const calls = unsafeTestValue>(execFileSyncSpy.mock.calls) const diffCall = calls.find(([, args]) => args[0] === "diff") const statusCall = calls.find(([, args]) => args[0] === "status") const untrackedCall = calls.find(([, args]) => args[0] === "ls-files") diff --git a/src/shared/migration/migrations-sidecar.ts b/src/shared/migration/migrations-sidecar.ts index 0cbac7db1..9491b3f8a 100644 --- a/src/shared/migration/migrations-sidecar.ts +++ b/src/shared/migration/migrations-sidecar.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs" import * as path from "node:path" import { log } from "../logger" +import { isRecord } from "../record-type-guard" import { writeFileAtomically } from "../write-file-atomically" /** @@ -48,14 +49,9 @@ export function readAppliedMigrations(configPath: string): Set { return new Set() } const content = fs.readFileSync(sidecarPath, "utf-8") - const parsed = JSON.parse(content) as unknown - if ( - parsed && - typeof parsed === "object" && - !Array.isArray(parsed) && - Array.isArray((parsed as MigrationsSidecar).appliedMigrations) - ) { - return new Set((parsed as MigrationsSidecar).appliedMigrations.filter((m): m is string => typeof m === "string")) + const parsed: unknown = JSON.parse(content) + if (isRecord(parsed) && Array.isArray(parsed.appliedMigrations)) { + return new Set(parsed.appliedMigrations.filter((migration): migration is string => typeof migration === "string")) } return new Set() } catch (err) { diff --git a/src/shared/model-suggestion-retry.test.ts b/src/shared/model-suggestion-retry.test.ts index 698419a67..e594ad6a3 100644 --- a/src/shared/model-suggestion-retry.test.ts +++ b/src/shared/model-suggestion-retry.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, mock } from "bun:test" import { parseModelSuggestion, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "./model-suggestion-retry" +import { unsafeTestValue } from "../../test-support/unsafe-test-value" describe("parseModelSuggestion", () => { describe("structured NamedError format", () => { @@ -217,7 +218,7 @@ describe("promptWithModelSuggestionRetry", () => { const client = { session: { promptAsync: promptMock } } // when calling promptWithModelSuggestionRetry - await promptWithModelSuggestionRetry(client as any, { + await promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -244,7 +245,7 @@ describe("promptWithModelSuggestionRetry", () => { // when calling promptWithModelSuggestionRetry // then should throw the error without retrying await expect( - promptWithModelSuggestionRetry(client as any, { + promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { agent: "explore", @@ -267,7 +268,7 @@ describe("promptWithModelSuggestionRetry", () => { // when calling promptWithModelSuggestionRetry // then should throw the original error await expect( - promptWithModelSuggestionRetry(client as any, { + promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -288,7 +289,7 @@ describe("promptWithModelSuggestionRetry", () => { // when calling promptWithModelSuggestionRetry // then should throw the error await expect( - promptWithModelSuggestionRetry(client as any, { + promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -307,7 +308,7 @@ describe("promptWithModelSuggestionRetry", () => { const client = { session: { promptAsync: promptMock } } // when calling with additional body fields - await promptWithModelSuggestionRetry(client as any, { + await promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { agent: "explore", @@ -341,7 +342,7 @@ describe("promptWithModelSuggestionRetry", () => { // when calling promptWithModelSuggestionRetry // then should throw the error await expect( - promptWithModelSuggestionRetry(client as any, { + promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -365,7 +366,7 @@ describe("promptWithModelSuggestionRetry", () => { // when calling without model in body // then should throw the error await expect( - promptWithModelSuggestionRetry(client as any, { + promptWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -386,7 +387,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { const client = { session: { prompt: promptMock, promptAsync: promptAsyncMock } } // when calling promptSyncWithModelSuggestionRetry - await promptSyncWithModelSuggestionRetry(client as any, { + await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -424,7 +425,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { // when calling with short timeout // then should abort the request and throw timeout error await expect( - promptSyncWithModelSuggestionRetry(client as any, { + promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -451,7 +452,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { const client = { session: { prompt: promptMock } } // when calling promptSyncWithModelSuggestionRetry - await promptSyncWithModelSuggestionRetry(client as any, { + await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -477,7 +478,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { // when calling promptSyncWithModelSuggestionRetry // then should throw the original error await expect( - promptSyncWithModelSuggestionRetry(client as any, { + promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -504,7 +505,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { // when calling without model in body // then should throw (cannot retry without original model) await expect( - promptSyncWithModelSuggestionRetry(client as any, { + promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { parts: [{ type: "text", text: "hello" }], @@ -521,7 +522,7 @@ describe("promptSyncWithModelSuggestionRetry", () => { const client = { session: { prompt: promptMock } } // when calling with additional body fields - await promptSyncWithModelSuggestionRetry(client as any, { + await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), { path: { id: "session-1" }, body: { agent: "multimodal-looker", diff --git a/src/tools/background-task/create-background-task.metadata.test.ts b/src/tools/background-task/create-background-task.metadata.test.ts index d21e69c09..ae3142f68 100644 --- a/src/tools/background-task/create-background-task.metadata.test.ts +++ b/src/tools/background-task/create-background-task.metadata.test.ts @@ -6,6 +6,7 @@ import { describe, expect, mock, test } from "bun:test" import type { BackgroundManager } from "../../features/background-agent" import { clearPendingStore, consumeToolMetadata } from "../../features/tool-metadata-store" import { createBackgroundTask } from "./create-background-task" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode" @@ -18,7 +19,7 @@ describe("createBackgroundTask metadata", () => { // #given clearPendingStore() - const manager = { + const manager = unsafeTestValue({ launch: mock(() => Promise.resolve({ id: "task-1", sessionID: null, @@ -27,12 +28,12 @@ describe("createBackgroundTask metadata", () => { status: "pending", })), getTask: mock(() => undefined), - } as unknown as BackgroundManager - const client = { + }) + const client = unsafeTestValue({ session: { messages: mock(() => Promise.resolve({ data: [] })), }, - } as unknown as PluginInput["client"] + }) let capturedMetadata: { title?: string; metadata?: Record } | undefined const tool = createBackgroundTask(manager, client) diff --git a/src/tools/background-task/create-background-task.test.ts b/src/tools/background-task/create-background-task.test.ts index a7b588ff6..4dd36efe7 100644 --- a/src/tools/background-task/create-background-task.test.ts +++ b/src/tools/background-task/create-background-task.test.ts @@ -4,6 +4,7 @@ import { describe, test, expect, mock } from "bun:test" import type { BackgroundManager } from "../../features/background-agent" import type { PluginInput } from "@opencode-ai/plugin" import { createBackgroundTask } from "./create-background-task" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("createBackgroundTask", () => { const launchMock = mock(async (): Promise<{ @@ -21,16 +22,16 @@ describe("createBackgroundTask", () => { })) const getTaskMock = mock() - const mockManager = { + const mockManager = unsafeTestValue({ launch: launchMock, getTask: getTaskMock, - } as unknown as BackgroundManager + }) - const mockClient = { + const mockClient = unsafeTestValue({ session: { messages: mock(() => Promise.resolve({ data: [] })), }, - } as unknown as PluginInput["client"] + }) const tool = createBackgroundTask(mockManager, mockClient) diff --git a/src/tools/background-task/tools.test.ts b/src/tools/background-task/tools.test.ts index 12404bf72..81969b431 100644 --- a/src/tools/background-task/tools.test.ts +++ b/src/tools/background-task/tools.test.ts @@ -6,6 +6,7 @@ import type { BackgroundManager, BackgroundTask } from "../../features/backgroun import type { ToolContext } from "@opencode-ai/plugin/tool" import type { BackgroundCancelClient, BackgroundOutputManager, BackgroundOutputClient } from "./tools" import { consumeToolMetadata, clearPendingStore } from "../../features/tool-metadata-store" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode" @@ -66,10 +67,10 @@ describe("background_output full_session", () => { const manager = createMockManager(task) const client = createMockClient({}) const tool = createBackgroundOutput(manager, client) - const ctxWithCallId = { + const ctxWithCallId = unsafeTestValue({ ...mockContext, callID: "call-1", - } as unknown as ToolContext + }) // #when await tool.execute({ task_id: "task-1" }, ctxWithCallId) @@ -93,10 +94,10 @@ describe("background_output full_session", () => { const manager = createMockManager(task) const client = createMockClient({}) const tool = createBackgroundOutput(manager, client) - const ctxWithCallId = { + const ctxWithCallId = unsafeTestValue({ ...mockContext, callID: "call-1", - } as unknown as ToolContext + }) // #when await tool.execute({ task_id: "task-1" }, ctxWithCallId) @@ -387,7 +388,7 @@ describe("background_cancel", () => { // #given const task = createTask({ status: "running" }) const cancelled: string[] = [] - const manager = { + const manager = unsafeTestValue({ getTask: (id: string) => (id === task.id ? task : undefined), getAllDescendantTasks: () => [task], cancelTask: async (taskId: string) => { @@ -395,7 +396,7 @@ describe("background_cancel", () => { task.status = "cancelled" return true }, - } as unknown as BackgroundManager + }) const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient const tool = createBackgroundCancel(manager, client) @@ -412,7 +413,7 @@ describe("background_cancel", () => { const taskA = createTask({ id: "task-a", status: "running" }) const taskB = createTask({ id: "task-b", status: "pending" }) const cancelled: string[] = [] - const manager = { + const manager = unsafeTestValue({ getTask: () => undefined, getAllDescendantTasks: () => [taskA, taskB], cancelTask: async (taskId: string) => { @@ -421,7 +422,7 @@ describe("background_cancel", () => { task.status = "cancelled" return true }, - } as unknown as BackgroundManager + }) const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient const tool = createBackgroundCancel(manager, client) @@ -437,7 +438,7 @@ describe("background_cancel", () => { // #given const taskA = createTask({ id: "task-a", status: "running", sessionId: "ses-a", description: "running task" }) const taskB = createTask({ id: "task-b", status: "pending", sessionId: undefined, description: "pending task" }) - const manager = { + const manager = unsafeTestValue({ getTask: () => undefined, getAllDescendantTasks: () => [taskA, taskB], cancelTask: async (taskId: string) => { @@ -445,7 +446,7 @@ describe("background_cancel", () => { task.status = "cancelled" return true }, - } as unknown as BackgroundManager + }) const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient const tool = createBackgroundCancel(manager, client) @@ -461,7 +462,7 @@ describe("background_cancel", () => { // #given const task = createTask({ id: "task-1", status: "running" }) const cancelOptions: Array<{ taskId: string; options: unknown }> = [] - const manager = { + const manager = unsafeTestValue({ getTask: (id: string) => (id === task.id ? task : undefined), getAllDescendantTasks: () => [task], cancelTask: async (taskId: string, options?: unknown) => { @@ -469,7 +470,7 @@ describe("background_cancel", () => { task.status = "cancelled" return true }, - } as unknown as BackgroundManager + }) const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient const tool = createBackgroundCancel(manager, client) @@ -487,7 +488,7 @@ describe("background_cancel", () => { // #given const task = createTask({ id: "task-1", status: "running" }) const cancelOptions: Array<{ taskId: string; options: unknown }> = [] - const manager = { + const manager = unsafeTestValue({ getTask: (id: string) => (id === task.id ? task : undefined), getAllDescendantTasks: () => [task], cancelTask: async (taskId: string, options?: unknown) => { @@ -495,7 +496,7 @@ describe("background_cancel", () => { task.status = "cancelled" return true }, - } as unknown as BackgroundManager + }) const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient const tool = createBackgroundCancel(manager, client) diff --git a/src/tools/call-omo-agent/agent-resolver.test.ts b/src/tools/call-omo-agent/agent-resolver.test.ts index 773cd27aa..f2afa1f18 100644 --- a/src/tools/call-omo-agent/agent-resolver.test.ts +++ b/src/tools/call-omo-agent/agent-resolver.test.ts @@ -2,7 +2,7 @@ const { describe, test, expect, mock, beforeEach } = require("bun:test") const { resolveCallableAgents, clearCallableAgentsCache } = require("./agent-resolver") const { ALLOWED_AGENTS } = require("./constants") -function createMockClient(agents = []) { +function createMockClient(agents: Array> = []) { return { app: { agents: mock(() => Promise.resolve({ data: agents })), diff --git a/src/tools/call-omo-agent/session-creator.test.ts b/src/tools/call-omo-agent/session-creator.test.ts index db231651d..222975c68 100644 --- a/src/tools/call-omo-agent/session-creator.test.ts +++ b/src/tools/call-omo-agent/session-creator.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import { createOrGetSession } from "./session-creator" import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("call-omo-agent createOrGetSession", () => { test("creates child session without overriding permission and tracks it as subagent session", async () => { @@ -37,12 +38,12 @@ describe("call-omo-agent createOrGetSession", () => { } // when - const result = await createOrGetSession(args as any, toolContext as any, ctx as any) + const result = await createOrGetSession(unsafeTestValue(args), unsafeTestValue(toolContext), unsafeTestValue(ctx)) // then expect(result).toEqual({ sessionID: "ses_child", isNew: true }) expect(createCalls).toHaveLength(1) - const createBody = (createCalls[0] as any)?.body + const createBody = (unsafeTestValue(createCalls[0]))?.body expect(createBody?.parentID).toBe("ses_parent") expect(createBody?.permission).toBeUndefined() expect(subagentSessions.has("ses_child")).toBe(true) diff --git a/src/tools/call-omo-agent/subagent-session-creator.test.ts b/src/tools/call-omo-agent/subagent-session-creator.test.ts index dea60d524..6e6f65e05 100644 --- a/src/tools/call-omo-agent/subagent-session-creator.test.ts +++ b/src/tools/call-omo-agent/subagent-session-creator.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import { resolveOrCreateSessionId } from "./subagent-session-creator" import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("call-omo-agent resolveOrCreateSessionId", () => { const originalPlatform = process.platform @@ -19,7 +20,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => { const { parentDirectory, contextDirectory } = options const parentSessionData = parentDirectory ? { data: { directory: parentDirectory } } : { data: {} } - const ctx = { + const ctx = unsafeTestValue[0]>({ directory: contextDirectory, client: { session: { @@ -31,7 +32,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => { }, }, }, - } as unknown as Parameters[0] + }) const args = { description: "sync test", diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index aab485706..9b8392b93 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -1,3 +1,4 @@ +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const { describe, test, expect, mock } = require("bun:test") type ExecuteSync = typeof import("./sync-executor").executeSync @@ -389,7 +390,7 @@ describe("executeSync", () => { } //#when - await executeSync(args, toolContext, ctx as any, deps, undefined, spawnReservation) + await executeSync(args, toolContext, unsafeTestValue(ctx), deps, undefined, spawnReservation) //#then expect(spawnReservation.commit).toHaveBeenCalledTimes(1) diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index 56e22a80a..31ae8beb8 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -14,6 +14,10 @@ type SessionWithPromptAsync = { promptAsync: (opts: { path: { id: string }; body: Record }) => Promise } +function hasPromptAsync(session: PluginInput["client"]["session"]): session is PluginInput["client"]["session"] & SessionWithPromptAsync { + return "promptAsync" in session && typeof session.promptAsync === "function" +} + type ExecuteSyncDeps = { createOrGetSession: typeof createOrGetSession waitForCompletion: typeof waitForCompletion @@ -102,7 +106,11 @@ export async function executeSync( const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type) try { - await (ctx.client.session as unknown as SessionWithPromptAsync).promptAsync({ + if (!hasPromptAsync(ctx.client.session)) { + return `Error: Failed to send prompt: promptAsync is not available on this OpenCode client.\n\n\nsession_id: ${sessionID}\n` + } + + await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { agent: normalizedSubagentType, diff --git a/src/tools/delegate-task/available-models.ts b/src/tools/delegate-task/available-models.ts index 711ac1920..9ec078cf7 100644 --- a/src/tools/delegate-task/available-models.ts +++ b/src/tools/delegate-task/available-models.ts @@ -1,7 +1,25 @@ import type { OpencodeClient } from "./types" import { log } from "../../shared/logger" +import { isRecord } from "../../shared/record-type-guard" import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache" +type ModelListClient = OpencodeClient & { + model: { list: () => Promise } +} + +function hasModelList(client: OpencodeClient): client is ModelListClient { + return "model" in client && isRecord(client.model) && typeof client.model.list === "function" +} + +function isModelRow(value: unknown): value is { provider: string; id: string } { + return isRecord(value) && typeof value.provider === "string" && typeof value.id === "string" +} + +function extractModelRows(result: unknown): Array<{ provider: string; id: string }> { + const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.data) ? result.data : [] + return rows.filter(isModelRow) +} + function addFromProviderModels( out: Set, providerID: string, @@ -35,24 +53,17 @@ export async function getAvailableModelsForDelegateTask(client: OpencodeClient): return new Set() } - const modelList = (client as unknown as { model?: { list?: () => Promise } }) - ?.model - ?.list - - if (!modelList) { + if (!hasModelList(client)) { return new Set() } try { - const result = await modelList() - const rows = Array.isArray(result) - ? result - : ((result as { data?: unknown }).data as Array<{ provider?: string; id?: string }> | undefined) ?? [] + const result = await client.model.list() + const rows = extractModelRows(result) const connected = new Set(connectedProviders) const out = new Set() for (const row of rows) { - if (!row?.provider || !row?.id) continue if (!connected.has(row.provider)) continue out.add(`${row.provider}/${row.id}`) } diff --git a/src/tools/delegate-task/category-resolver.test.ts b/src/tools/delegate-task/category-resolver.test.ts index 8077054d0..7fac923e2 100644 --- a/src/tools/delegate-task/category-resolver.test.ts +++ b/src/tools/delegate-task/category-resolver.test.ts @@ -3,6 +3,7 @@ const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require(" import { resolveCategoryExecution } from "./category-resolver" import type { ExecutorContext } from "./executor-types" import * as connectedProvidersCache from "../../shared/connected-providers-cache" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("resolveCategoryExecution", () => { let connectedProvidersSpy: ReturnType | undefined @@ -26,8 +27,8 @@ describe("resolveCategoryExecution", () => { }) const createMockExecutorContext = (): ExecutorContext => ({ - client: {} as any, - manager: {} as any, + client: unsafeTestValue({}), + manager: unsafeTestValue({}), directory: "/tmp/test", userCategories: {}, sisyphusJuniorModel: undefined, diff --git a/src/tools/delegate-task/metadata-await.test.ts b/src/tools/delegate-task/metadata-await.test.ts index 6592457c5..c9073889f 100644 --- a/src/tools/delegate-task/metadata-await.test.ts +++ b/src/tools/delegate-task/metadata-await.test.ts @@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test") import { executeBackgroundTask } from "./executor" import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("task tool metadata awaiting", () => { test("executeBackgroundTask awaits ctx.metadata before returning", async () => { @@ -28,7 +29,7 @@ describe("task tool metadata awaiting", () => { subagent_type: "explore", } - const executorCtx = { + const executorCtx = unsafeTestValue({ manager: { launch: async () => ({ id: "task_1", @@ -40,7 +41,7 @@ describe("task tool metadata awaiting", () => { }), getTask: () => undefined, }, - } as any + }) const parentContext = { sessionID: "ses_parent", diff --git a/src/tools/delegate-task/metadata-model-unification.test.ts b/src/tools/delegate-task/metadata-model-unification.test.ts index 3a64022ab..0a23afa0d 100644 --- a/src/tools/delegate-task/metadata-model-unification.test.ts +++ b/src/tools/delegate-task/metadata-model-unification.test.ts @@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test") import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { ParentContext } from "./executor-types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" } const MODEL_WITH_VARIANT = { providerID: "google", modelID: "gemini-3.1-pro", variant: "high" } @@ -63,7 +64,7 @@ describe("metadata model unification", () => { load_skills: [], run_in_background: true, subagent_type: "explore", } - await executeBackgroundTask(args, ctx, { + await executeBackgroundTask(args, ctx, unsafeTestValue({ manager: { launch: async () => ({ id: "bg_1", description: "test", agent: "explore", @@ -71,7 +72,7 @@ describe("metadata model unification", () => { }), getTask: () => undefined, }, - } as any, parentContext, "explore", MODEL, undefined) + }), parentContext, "explore", MODEL, undefined) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -92,7 +93,7 @@ describe("metadata model unification", () => { } await executeUnstableAgentTask( args, ctx, - { + unsafeTestValue({ manager: { launch: async () => launchedTask, getTask: () => launchedTask, @@ -109,7 +110,7 @@ describe("metadata model unification", () => { }, }, syncPollTimeoutMs: 100, - } as any, + }), parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6", ) @@ -126,14 +127,14 @@ describe("metadata model unification", () => { load_skills: [], run_in_background: true, task_id: "ses_resumed", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_2", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed", model: MODEL, }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -153,7 +154,7 @@ describe("metadata model unification", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -162,7 +163,7 @@ describe("metadata model unification", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -206,7 +207,7 @@ describe("metadata model unification", () => { load_skills: [], run_in_background: true, subagent_type: "explore", } - await executeBackgroundTask(args, ctx, { + await executeBackgroundTask(args, ctx, unsafeTestValue({ manager: { launch: async () => ({ id: "bg_1", description: "test", agent: "explore", @@ -214,7 +215,7 @@ describe("metadata model unification", () => { }), getTask: () => undefined, }, - } as any, parentContext, "explore", undefined, undefined) + }), parentContext, "explore", undefined, undefined) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -236,7 +237,7 @@ describe("metadata model unification", () => { await executeUnstableAgentTask( args, ctx, - { + unsafeTestValue({ manager: { launch: async () => launchedTask, getTask: () => launchedTask, @@ -253,7 +254,7 @@ describe("metadata model unification", () => { }, }, syncPollTimeoutMs: 100, - } as any, + }), parentContext, "explore", undefined, undefined, "anthropic/claude-sonnet-4-6", ) @@ -270,14 +271,14 @@ describe("metadata model unification", () => { load_skills: [], run_in_background: true, task_id: "ses_resumed", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_2", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed", }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -297,14 +298,14 @@ describe("metadata model unification", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ data: [] }), prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -381,7 +382,7 @@ describe("metadata model unification", () => { category: "visual-engineering", load_skills: [], run_in_background: true, subagent_type: "explore", } - await executeBackgroundTask(args, ctx, { + await executeBackgroundTask(args, ctx, unsafeTestValue({ manager: { launch: async () => ({ id: "bg_variant", description: "test", agent: "explore", @@ -389,7 +390,7 @@ describe("metadata model unification", () => { }), getTask: () => undefined, }, - } as any, parentContext, "explore", MODEL_WITH_VARIANT, undefined) + }), parentContext, "explore", MODEL_WITH_VARIANT, undefined) const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId) expect(meta).toBeDefined() @@ -411,7 +412,7 @@ describe("metadata model unification", () => { await executeUnstableAgentTask( args, ctx, - { + unsafeTestValue({ manager: { launch: async () => launchedTask, getTask: () => launchedTask, @@ -428,7 +429,7 @@ describe("metadata model unification", () => { }, }, syncPollTimeoutMs: 100, - } as any, + }), parentContext, "explore", MODEL_WITH_VARIANT, undefined, "google/gemini-3.1-pro high", ) @@ -445,14 +446,14 @@ describe("metadata model unification", () => { load_skills: [], run_in_background: true, task_id: "ses_resumed_variant", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_resume_variant", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed_variant", model: MODEL_WITH_VARIANT, }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId) expect(meta).toBeDefined() @@ -472,7 +473,7 @@ describe("metadata model unification", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -481,7 +482,7 @@ describe("metadata model unification", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId) expect(meta).toBeDefined() diff --git a/src/tools/delegate-task/metadata-task-id-consistency.test.ts b/src/tools/delegate-task/metadata-task-id-consistency.test.ts index 23ce7d64a..3f466e6cb 100644 --- a/src/tools/delegate-task/metadata-task-id-consistency.test.ts +++ b/src/tools/delegate-task/metadata-task-id-consistency.test.ts @@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test") import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { ParentContext } from "./executor-types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" } @@ -64,7 +65,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { load_skills: [], run_in_background: true, subagent_type: "explore", } - await executeBackgroundTask(args, ctx, { + await executeBackgroundTask(args, ctx, unsafeTestValue({ manager: { launch: async () => ({ id: "bg_abc123", description: "test", agent: "explore", @@ -72,7 +73,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { }), getTask: () => undefined, }, - } as any, parentContext, "explore", MODEL, undefined) + }), parentContext, "explore", MODEL, undefined) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -98,7 +99,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { await executeUnstableAgentTask( args, ctx, - { + unsafeTestValue({ manager: { launch: async () => launchedTask, getTask: () => launchedTask, @@ -115,7 +116,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { }, }, syncPollTimeoutMs: 100, - } as any, + }), parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6", ) @@ -136,14 +137,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => { load_skills: [], run_in_background: true, task_id: "ses_resumed_x", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_resumed_y", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed_x", model: MODEL, }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -160,14 +161,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => { load_skills: [], run_in_background: true, task_id: "ses_resumed_x", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_resumed_y", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed_x", model: MODEL, category: "deep", }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -187,14 +188,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => { task_id: "ses_resumed_x", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_resumed_y", description: "continue", agent: "explore", status: "running", sessionId: "ses_resumed_x", model: MODEL, }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -216,7 +217,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -225,7 +226,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((m: any) => m.metadata?.sessionId) expect(meta).toBeDefined() @@ -246,7 +247,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -255,7 +256,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -275,7 +276,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -284,7 +285,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -309,7 +310,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -318,7 +319,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { prompt: async () => ({}), }, }, - } as any, parentContext, deps) + }), parentContext, deps) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -368,7 +369,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { run_in_background: true, } - await executeBackgroundTask(args, ctx, { + await executeBackgroundTask(args, ctx, unsafeTestValue({ manager: { launch: async () => ({ id: "bg_abc123", description: "test", agent: "Sisyphus-Junior", @@ -376,7 +377,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { }), getTask: () => undefined, }, - } as any, parentContext, "Sisyphus-Junior", MODEL, undefined) + }), parentContext, "Sisyphus-Junior", MODEL, undefined) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -402,7 +403,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { await executeUnstableAgentTask( args, ctx, - { + unsafeTestValue({ manager: { launch: async () => launchedTask, getTask: () => launchedTask, @@ -419,7 +420,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { }, }, syncPollTimeoutMs: 100, - } as any, + }), parentContext, "Sisyphus-Junior", MODEL, undefined, "anthropic/claude-sonnet-4-6", ) @@ -438,14 +439,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => { load_skills: [], run_in_background: true, task_id: "ses_resume_title", } - await executeBackgroundContinuation(args, ctx, { + await executeBackgroundContinuation(args, ctx, unsafeTestValue({ manager: { resume: async () => ({ id: "bg_resume_title", description: "continue work", agent: "explore", status: "running", sessionId: "ses_resume_title", model: MODEL, }), }, - } as any, parentContext) + }), parentContext) const meta = ctx.captured.find((item: any) => item.metadata?.sessionId) expect(meta).toBeDefined() @@ -460,7 +461,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { load_skills: [], run_in_background: false, task_id: "ses_sync_title", } - await executeSyncContinuation(args, ctx, { + await executeSyncContinuation(args, ctx, unsafeTestValue({ client: { session: { messages: async () => ({ @@ -469,7 +470,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => { prompt: async () => ({}), }, }, - } as any, parentContext, { + }), parentContext, { pollSyncSession: async () => null, fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), }) @@ -500,8 +501,8 @@ describe("taskId and backgroundTaskId metadata consistency", () => { }, } - const bgOutput = createBackgroundOutput(manager as any, client as any) - await bgOutput.execute({ task_id: "bg_output_xyz" } as any, ctx as any) + const bgOutput = createBackgroundOutput(unsafeTestValue(manager), unsafeTestValue(client)) + await bgOutput.execute(unsafeTestValue({ task_id: "bg_output_xyz" }), unsafeTestValue(ctx)) const meta = ctx.captured.find((m: any) => m.metadata?.backgroundTaskId) expect(meta).toBeDefined() diff --git a/src/tools/delegate-task/task-schema.test.ts b/src/tools/delegate-task/task-schema.test.ts index c50d175bc..fcad9cd4a 100644 --- a/src/tools/delegate-task/task-schema.test.ts +++ b/src/tools/delegate-task/task-schema.test.ts @@ -1,3 +1,4 @@ +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const { describe, expect, test } = require("bun:test") function requireFresh(modulePath: string): T { @@ -18,14 +19,14 @@ function createDelegateTask(...args: Parameters(toolDefinition.args.category) //#then expect(categorySchema.def.type).toBe("optional") diff --git a/src/tools/delegate-task/unstable-agent-permission.test.ts b/src/tools/delegate-task/unstable-agent-permission.test.ts index 50b96bad6..21defd5e0 100644 --- a/src/tools/delegate-task/unstable-agent-permission.test.ts +++ b/src/tools/delegate-task/unstable-agent-permission.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { executeUnstableAgentTask } from "./unstable-agent-task" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("executeUnstableAgentTask session permission", () => { test("passes question-deny session permission into background launch", async () => { @@ -33,7 +34,7 @@ describe("executeUnstableAgentTask session permission", () => { metadata: () => {}, abort: new AbortController().signal, } satisfies Parameters[1] - const executorContext = { + const executorContext = unsafeTestValue[2]>({ manager: mockManager, client: { session: { @@ -41,7 +42,7 @@ describe("executeUnstableAgentTask session permission", () => { messages: async () => ({ data: [] }), }, }, - } as unknown as Parameters[2] + }) const parentContext = { sessionID: "parent-session", messageID: "msg_parent", diff --git a/src/tools/hashline-edit/normalize-edits.test.ts b/src/tools/hashline-edit/normalize-edits.test.ts index 45cf6f253..a76b9f8fb 100644 --- a/src/tools/hashline-edit/normalize-edits.test.ts +++ b/src/tools/hashline-edit/normalize-edits.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test" import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("normalizeHashlineEdits", () => { it("maps replace with pos to replace", () => { @@ -51,9 +52,9 @@ describe("normalizeHashlineEdits", () => { it("rejects legacy payload without op", () => { //#given - const input = [{ type: "set_line", line: "2#VK", text: "updated" }] as unknown as Parameters< + const input = unsafeTestValue[0] + >[0]>([{ type: "set_line", line: "2#VK", text: "updated" }]) //#when / #then expect(() => normalizeHashlineEdits(input)).toThrow(/legacy format was removed/i) diff --git a/src/tools/hashline-edit/tools.test.ts b/src/tools/hashline-edit/tools.test.ts index 1158ca3d2..686cc839f 100644 --- a/src/tools/hashline-edit/tools.test.ts +++ b/src/tools/hashline-edit/tools.test.ts @@ -6,16 +6,17 @@ import { canonicalizeFileText } from "./file-text-canonicalization" import * as fs from "node:fs" import * as os from "node:os" import * as path from "node:path" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createMockContext(): ToolContext { - return { + return unsafeTestValue({ sessionID: "test", messageID: "test", agent: "test", abort: new AbortController().signal, metadata: mock(() => {}), ask: async () => {}, - } as unknown as ToolContext + }) } describe("createHashlineEditTool", () => { diff --git a/src/tools/look-at/multimodal-agent-metadata.test.ts b/src/tools/look-at/multimodal-agent-metadata.test.ts index aa057eb34..b7b5730ba 100644 --- a/src/tools/look-at/multimodal-agent-metadata.test.ts +++ b/src/tools/look-at/multimodal-agent-metadata.test.ts @@ -6,6 +6,7 @@ import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadat import { setVisionCapableModelsCache, clearVisionCapableModelsCache } from "../../shared/vision-capable-models-cache" import * as connectedProvidersCache from "../../shared/connected-providers-cache" import * as modelAvailability from "../../shared/model-availability" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" function createPluginInput(agentData: Array>): PluginInput { const client = {} as PluginInput["client"] @@ -32,8 +33,8 @@ describe("resolveMultimodalLookerAgentMetadata", () => { afterEach(() => { clearVisionCapableModelsCache() - ;(modelAvailability.fetchAvailableModels as unknown as { mockRestore?: () => void }).mockRestore?.() - ;(connectedProvidersCache.readConnectedProvidersCache as unknown as { mockRestore?: () => void }).mockRestore?.() + ;(unsafeTestValue<{ mockRestore?: () => void }>(modelAvailability.fetchAvailableModels)).mockRestore?.() + ;(unsafeTestValue<{ mockRestore?: () => void }>(connectedProvidersCache.readConnectedProvidersCache)).mockRestore?.() }) test("returns configured multimodal-looker model when it already matches a vision-capable override", async () => { diff --git a/src/tools/look-at/session-poller.test.ts b/src/tools/look-at/session-poller.test.ts index 757327a3d..cec05175e 100644 --- a/src/tools/look-at/session-poller.test.ts +++ b/src/tools/look-at/session-poller.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test, mock } from "bun:test" import { pollSessionUntilIdle } from "./session-poller" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" type SessionStatusResult = { data?: Record @@ -30,7 +31,7 @@ describe("pollSessionUntilIdle", () => { { data: { ses_test: { type: "idle" } } }, ]) - await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) + await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) expect(client.session.status).toHaveBeenCalledTimes(3) }) @@ -43,7 +44,7 @@ describe("pollSessionUntilIdle", () => { { data: {} }, ]) - await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) + await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) expect(client.session.status).toHaveBeenCalledTimes(1) }) @@ -57,7 +58,7 @@ describe("pollSessionUntilIdle", () => { ]) await expect( - pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 50 }) + pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 50 }) ).rejects.toThrow("timed out") }) @@ -69,7 +70,7 @@ describe("pollSessionUntilIdle", () => { { error: new Error("API error") }, ]) - await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) + await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) expect(client.session.status).toHaveBeenCalledTimes(1) }) @@ -85,7 +86,7 @@ describe("pollSessionUntilIdle", () => { { data: {} }, ]) - await pollSessionUntilIdle(client as any, "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) + await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) expect(client.session.status).toHaveBeenCalledTimes(4) }) @@ -98,7 +99,7 @@ describe("pollSessionUntilIdle", () => { { data: {} }, ]) - await pollSessionUntilIdle(client as any, "ses_test") + await pollSessionUntilIdle(unsafeTestValue(client), "ses_test") expect(client.session.status).toHaveBeenCalledTimes(1) }) diff --git a/src/tools/look-at/tools.test.ts b/src/tools/look-at/tools.test.ts index 9067032de..56eda17f8 100644 --- a/src/tools/look-at/tools.test.ts +++ b/src/tools/look-at/tools.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, test, mock } from "bun:test" import type { ToolContext } from "@opencode-ai/plugin/tool" import { clearVisionCapableModelsCache, setVisionCapableModelsCache } from "../../shared/vision-capable-models-cache" import { normalizeArgs, validateArgs, createLookAt } from "./tools" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("look-at tool", () => { afterEach(() => { @@ -14,7 +15,7 @@ describe("look-at tool", () => { // then should normalize to file_path test("normalizes path to file_path for LLM compatibility", () => { const args = { path: "/some/file.png", goal: "analyze" } - const normalized = normalizeArgs(args as any) + const normalized = normalizeArgs(unsafeTestValue(args)) expect(normalized.file_path).toBe("/some/file.png") expect(normalized.goal).toBe("analyze") }) @@ -33,7 +34,7 @@ describe("look-at tool", () => { // then prefer file_path test("prefers file_path over path when both provided", () => { const args = { file_path: "/preferred.png", path: "/fallback.png", goal: "test" } - const normalized = normalizeArgs(args as any) + const normalized = normalizeArgs(unsafeTestValue(args)) expect(normalized.file_path).toBe("/preferred.png") }) @@ -42,7 +43,7 @@ describe("look-at tool", () => { // then preserve image_data in normalized args test("preserves image_data when provided", () => { const args = { image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "analyze" } - const normalized = normalizeArgs(args as any) + const normalized = normalizeArgs(unsafeTestValue(args)) expect(normalized.image_data).toBe("data:image/png;base64,iVBORw0KGgo=") expect(normalized.file_path).toBeUndefined() }) @@ -69,7 +70,7 @@ describe("look-at tool", () => { // when validated // then clear error message test("returns error when neither file_path nor image_data provided", () => { - const args = { goal: "analyze" } as any + const args = unsafeTestValue({ goal: "analyze" }) const error = validateArgs(args) expect(error).toContain("file_path") expect(error).toContain("image_data") @@ -88,7 +89,7 @@ describe("look-at tool", () => { // when validated // then clear error message test("returns error when goal is missing", () => { - const args = { file_path: "/some/path.png" } as any + const args = unsafeTestValue({ file_path: "/some/path.png" }) const error = validateArgs(args) expect(error).toContain("goal") expect(error).toContain("required") @@ -156,10 +157,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -193,10 +194,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -230,10 +231,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -291,10 +292,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -346,10 +347,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -395,10 +396,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -437,10 +438,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -486,10 +487,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const result = await tool.execute( { file_path: "/test/file.png", goal: "analyze" }, @@ -515,10 +516,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const result = await tool.execute( { file_path: "/test/file.png", goal: "analyze" }, @@ -539,10 +540,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const result = await tool.execute( { file_path: "/test/file.png", goal: "analyze" }, @@ -579,10 +580,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -632,10 +633,10 @@ describe("look-at tool", () => { }, } - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) const toolContext: ToolContext = { sessionID: "parent-session", @@ -701,10 +702,10 @@ describe("look-at tool", () => { test("instructs agent to analyze attached file when Read is disabled (file_path mode)", async () => { const { mockClient, captured } = captureLastPromptBody() - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) await tool.execute( { file_path: "/test/file.png", goal: "describe contents" }, @@ -726,10 +727,10 @@ describe("look-at tool", () => { test("instructs agent to analyze attached image when image_data is provided", async () => { const { mockClient, captured } = captureLastPromptBody() - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) await tool.execute( { image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "describe image" }, @@ -751,10 +752,10 @@ describe("look-at tool", () => { test("explicitly warns the agent not to attempt Read when Read is disabled", async () => { const { mockClient, captured } = captureLastPromptBody() - const tool = createLookAt({ + const tool = createLookAt(unsafeTestValue({ client: mockClient, directory: "/project", - } as any) + })) await tool.execute( { file_path: "/test/file.pdf", goal: "extract text" }, diff --git a/src/tools/lsp/client.test.ts b/src/tools/lsp/client.test.ts index f89de579f..d7d4f7c73 100644 --- a/src/tools/lsp/client.test.ts +++ b/src/tools/lsp/client.test.ts @@ -16,6 +16,7 @@ afterAll(() => { mock.restore() }) import { LSPClient, lspManager, validateCwd } from "./client" import type { ResolvedServer } from "./types" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" describe("LSPClient", () => { beforeEach(async () => { @@ -36,7 +37,7 @@ describe("LSPClient", () => { const originalSetTimeout = globalThis.setTimeout globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { fn() - return 0 as unknown as ReturnType + return unsafeTestValue>(0) }) as typeof setTimeout const server: ResolvedServer = { @@ -50,7 +51,7 @@ describe("LSPClient", () => { // Stub protocol output: we only want to assert notifications. const sendNotificationSpy = spyOn( - client as unknown as { sendNotification: (m: string, p?: unknown) => void }, + unsafeTestValue<{ sendNotification: (m: string, p?: unknown) => void }>(client), "sendNotification" ) diff --git a/src/tools/lsp/lsp-process.ts b/src/tools/lsp/lsp-process.ts index 91d940b94..634e66b2b 100644 --- a/src/tools/lsp/lsp-process.ts +++ b/src/tools/lsp/lsp-process.ts @@ -1,4 +1,4 @@ -import { spawn as bunSpawn } from "../../shared/bun-spawn-shim" +import { spawn as bunSpawn, type SpawnedProcess } from "../../shared/bun-spawn-shim" import { spawn as nodeSpawn, type ChildProcess } from "node:child_process" import { existsSync, statSync } from "fs" import { log } from "../../shared/logger" @@ -127,6 +127,30 @@ function wrapNodeProcess(proc: ChildProcess): UnifiedProcess { }, } } + +function wrapBunProcess(proc: SpawnedProcess): UnifiedProcess { + return { + stdin: { + write(chunk: Uint8Array | string) { + proc.stdin.write(chunk) + }, + }, + stdout: { + getReader: () => proc.stdout.getReader(), + }, + stderr: { + getReader: () => proc.stderr.getReader(), + }, + get exitCode() { + return proc.exitCode + }, + exited: proc.exited, + kill(signal?: string) { + proc.kill(signal === "SIGKILL" ? "SIGKILL" : undefined) + }, + } +} + export function spawnProcess( command: string[], options: { cwd: string; env: Record } @@ -154,5 +178,5 @@ export function spawnProcess( cwd: options.cwd, env: options.env, }) - return proc as unknown as UnifiedProcess + return wrapBunProcess(proc) } diff --git a/src/tools/session-manager/storage.test.ts b/src/tools/session-manager/storage.test.ts index 1fbdb4e37..5a4ede7c2 100644 --- a/src/tools/session-manager/storage.test.ts +++ b/src/tools/session-manager/storage.test.ts @@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node: import { join } from "node:path" import { tmpdir } from "node:os" import { randomUUID } from "node:crypto" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`) const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message") @@ -448,7 +449,7 @@ describe("session-manager storage - SDK path (beta mode)", () => { // Re-import to get fresh module with mocked isSqliteBackend const { setStorageClient, getMainSessions } = await import("./storage") - setStorageClient(mockClient as unknown as Parameters[0]) + setStorageClient(unsafeTestValue[0]>(mockClient)) // when const sessions = await getMainSessions({ directory: "/test" }) @@ -473,7 +474,7 @@ describe("session-manager storage - SDK path (beta mode)", () => { })) const { setStorageClient, getAllSessions } = await import("./storage") - setStorageClient(mockClient as unknown as Parameters[0]) + setStorageClient(unsafeTestValue[0]>(mockClient)) // when const sessionIDs = await getAllSessions() @@ -503,7 +504,7 @@ describe("session-manager storage - SDK path (beta mode)", () => { })) const { setStorageClient, readSessionMessages } = await import("./storage") - setStorageClient(mockClient as unknown as Parameters[0]) + setStorageClient(unsafeTestValue[0]>(mockClient)) // when const messages = await readSessionMessages("ses_test") @@ -531,7 +532,7 @@ describe("session-manager storage - SDK path (beta mode)", () => { })) const { setStorageClient, readSessionTodos } = await import("./storage") - setStorageClient(mockClient as unknown as Parameters[0]) + setStorageClient(unsafeTestValue[0]>(mockClient)) // when const todos = await readSessionTodos("ses_test") @@ -555,7 +556,7 @@ describe("session-manager storage - SDK path (beta mode)", () => { })) const { setStorageClient, readSessionMessages } = await import("./storage") - setStorageClient(mockClient as unknown as Parameters[0]) + setStorageClient(unsafeTestValue[0]>(mockClient)) await expect(readSessionMessages("ses_test")).rejects.toThrow("API error") }) diff --git a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts index 32dd83bde..ccdfba648 100644 --- a/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts +++ b/src/tools/skill/zauc-mocks-skill-tools/tools.test.ts @@ -12,6 +12,7 @@ import { clearSkillCache } from "../../../features/opencode-skill-loader/skill-c import type { LoadedSkill } from "../../../features/opencode-skill-loader/types" import type { CommandInfo } from "../../slashcommand/types" import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js" +import { unsafeTestValue } from "../../../../test-support/unsafe-test-value" const originalReadFileSync = fs.readFileSync.bind(fs) @@ -205,7 +206,7 @@ describe("skill tool - agent restriction", () => { // given const loadedSkills = [createMockSkill("sisyphus-only-skill", { agent: "sisyphus" })] const tool = createSkillTool({ skills: loadedSkills }) - const contextWithoutAgent = { ...mockContext, agent: undefined as unknown as string } + const contextWithoutAgent = { ...mockContext, agent: unsafeTestValue(undefined) } // when / #then return expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow( diff --git a/test-setup.ts b/test-setup.ts index e66350edb..ccdfb0807 100644 --- a/test-setup.ts +++ b/test-setup.ts @@ -1,3 +1,4 @@ +/// import { afterEach, beforeEach, mock } from "bun:test" import { rmSync } from "node:fs" import { _resetForTesting as resetClaudeSessionState } from "./src/features/claude-code-session-state/state" diff --git a/test-support/unsafe-test-value.ts b/test-support/unsafe-test-value.ts new file mode 100644 index 000000000..faccd312e --- /dev/null +++ b/test-support/unsafe-test-value.ts @@ -0,0 +1,5 @@ +export function unsafeTestValue(value: TValue): TValue +export function unsafeTestValue(value: unknown): TValue +export function unsafeTestValue(value: unknown): TValue { + return value as TValue +} diff --git a/tests/hashline/test-edge-cases.ts b/tests/hashline/test-edge-cases.ts index b00b0302d..57a438491 100644 --- a/tests/hashline/test-edge-cases.ts +++ b/tests/hashline/test-edge-cases.ts @@ -12,7 +12,8 @@ import { spawn } from "node:child_process"; import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; // ── CLI arg passthrough ─────────────────────────────────────── const extraArgs: string[] = []; @@ -459,7 +460,6 @@ const TEST_CASES: TestCase[] = [ "Expected line 2 to be exactly 180 characters.", ].join(" "), validate: (content) => { - const expected = "L".repeat(180); const lines = content.replace(/\r/g, "").trimEnd().split("\n"); if (!lines[1]) { return { passed: false, reason: "line 2 is missing" }; @@ -880,6 +880,7 @@ const TEST_CASES: TestCase[] = [ // ── JSONL event types ───────────────────────────────────────── interface ToolCallEvent { + [key: string]: unknown; tool_call_id: string; tool_input: Record; tool_name: string; @@ -887,6 +888,7 @@ interface ToolCallEvent { } interface ToolResultEvent { + [key: string]: unknown; error?: string; output: string; tool_call_id: string; @@ -898,6 +900,28 @@ interface AnyEvent { [key: string]: unknown; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isToolCallEvent(event: AnyEvent): event is ToolCallEvent { + return ( + event.type === "tool_call" && + typeof event.tool_call_id === "string" && + typeof event.tool_name === "string" && + isRecord(event.tool_input) + ); +} + +function isToolResultEvent(event: AnyEvent): event is ToolResultEvent { + return ( + event.type === "tool_result" && + typeof event.tool_call_id === "string" && + typeof event.output === "string" && + (event.error === undefined || typeof event.error === "string") + ); +} + // ── Run single test case ───────────────────────────────────── async function runTestCase( tc: TestCase, @@ -913,7 +937,8 @@ async function runTestCase( writeFileSync(testFile, tc.fileContent, "utf-8"); } - const headlessScript = resolve(import.meta.dir, "headless.ts"); + const currentDirectory = dirname(fileURLToPath(import.meta.url)); + const headlessScript = resolve(currentDirectory, "headless.ts"); const headlessArgs = [ "run", headlessScript, @@ -976,12 +1001,8 @@ async function runTestCase( } } - const toolCalls = events.filter( - (e) => e.type === "tool_call" - ) as unknown as ToolCallEvent[]; - const toolResults = events.filter( - (e) => e.type === "tool_result" - ) as unknown as ToolResultEvent[]; + const toolCalls = events.filter(isToolCallEvent); + const toolResults = events.filter(isToolResultEvent); const editCalls = toolCalls.filter((e) => e.tool_name === "edit_file"); const editCallIds = new Set(editCalls.map((e) => e.tool_call_id)); diff --git a/tests/hashline/test-edit-ops.ts b/tests/hashline/test-edit-ops.ts index 05d63b4d2..357add621 100644 --- a/tests/hashline/test-edit-ops.ts +++ b/tests/hashline/test-edit-ops.ts @@ -12,7 +12,8 @@ import { spawn } from "node:child_process"; import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; // ── CLI arg passthrough ─────────────────────────────────────── const extraArgs: string[] = []; @@ -37,7 +38,6 @@ for (let i = 0; i < rawArgs.length; i++) { const BOLD = "\x1b[1m"; const GREEN = "\x1b[32m"; const RED = "\x1b[31m"; -const YELLOW = "\x1b[33m"; const DIM = "\x1b[2m"; const CYAN = "\x1b[36m"; const RESET = "\x1b[0m"; @@ -45,7 +45,6 @@ const RESET = "\x1b[0m"; const pass = (msg: string) => console.log(` ${GREEN}✓${RESET} ${msg}`); const fail = (msg: string) => console.log(` ${RED}✗${RESET} ${msg}`); const info = (msg: string) => console.log(` ${DIM}${msg}${RESET}`); -const warn = (msg: string) => console.log(` ${YELLOW}⚠${RESET} ${msg}`); // ── Test case definition ───────────────────────────────────── interface TestCase { @@ -575,6 +574,7 @@ const TEST_CASES: TestCase[] = [ // ── JSONL event types ───────────────────────────────────────── interface ToolCallEvent { + [key: string]: unknown; tool_call_id: string; tool_input: Record; tool_name: string; @@ -582,6 +582,7 @@ interface ToolCallEvent { } interface ToolResultEvent { + [key: string]: unknown; error?: string; output: string; tool_call_id: string; @@ -593,6 +594,28 @@ interface AnyEvent { [key: string]: unknown; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isToolCallEvent(event: AnyEvent): event is ToolCallEvent { + return ( + event.type === "tool_call" && + typeof event.tool_call_id === "string" && + typeof event.tool_name === "string" && + isRecord(event.tool_input) + ); +} + +function isToolResultEvent(event: AnyEvent): event is ToolResultEvent { + return ( + event.type === "tool_result" && + typeof event.tool_call_id === "string" && + typeof event.output === "string" && + (event.error === undefined || typeof event.error === "string") + ); +} + // ── Run single test case ───────────────────────────────────── async function runTestCase( tc: TestCase, @@ -606,7 +629,8 @@ async function runTestCase( const testFile = join(testDir, tc.fileName); writeFileSync(testFile, tc.fileContent, "utf-8"); - const headlessScript = resolve(import.meta.dir, "headless.ts"); + const currentDirectory = dirname(fileURLToPath(import.meta.url)); + const headlessScript = resolve(currentDirectory, "headless.ts"); const headlessArgs = [ "run", headlessScript, @@ -669,12 +693,8 @@ async function runTestCase( } } - const toolCalls = events.filter( - (e) => e.type === "tool_call" - ) as unknown as ToolCallEvent[]; - const toolResults = events.filter( - (e) => e.type === "tool_result" - ) as unknown as ToolResultEvent[]; + const toolCalls = events.filter(isToolCallEvent); + const toolResults = events.filter(isToolResultEvent); const editCalls = toolCalls.filter((e) => e.tool_name === "edit_file"); const editCallIds = new Set(editCalls.map((e) => e.tool_call_id));