test: make unsafe test coercion explicit

Move test coercion out of a hidden global and require each test to import the helper so review tools and runtime scripts can see the unsafe boundary.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-12 15:38:31 +09:00
parent ce5da13fc5
commit d5fbada13d
103 changed files with 700 additions and 554 deletions
@@ -4,6 +4,7 @@ import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { describe, expect, it } from "bun:test" import { describe, expect, it } from "bun:test"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const HUNG_LEAD_SESSION_ID = "ses_999999999fffeeRegrTestHang0" const HUNG_LEAD_SESSION_ID = "ses_999999999fffeeRegrTestHang0"
@@ -16,7 +17,7 @@ function makeHangingClient(): {
hangCount.value += 1 hangCount.value += 1
return new Promise<never>(() => {}) return new Promise<never>(() => {})
} }
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: sessionGet, get: sessionGet,
}, },
+4 -3
View File
@@ -3,6 +3,7 @@
import { afterEach, describe, expect, mock, test } from "bun:test" import { afterEach, describe, expect, mock, test } from "bun:test"
import { fetchNpmDistTags } from "../config-manager" import { fetchNpmDistTags } from "../config-manager"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("fetchNpmDistTags", () => { describe("fetchNpmDistTags", () => {
const originalFetch = globalThis.fetch const originalFetch = globalThis.fetch
@@ -13,7 +14,7 @@ describe("fetchNpmDistTags", () => {
test("returns dist-tags on success", async () => { test("returns dist-tags on success", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }), json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }),
@@ -29,7 +30,7 @@ describe("fetchNpmDistTags", () => {
test("returns null on network failure", async () => { test("returns null on network failure", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => Promise.reject(new Error("Network error")))) globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() => Promise.reject(new Error("Network error"))))
//#when //#when
const result = await fetchNpmDistTags("oh-my-openagent") const result = await fetchNpmDistTags("oh-my-openagent")
@@ -40,7 +41,7 @@ describe("fetchNpmDistTags", () => {
test("returns null on non-ok response", async () => { test("returns null on non-ok response", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: false, ok: false,
status: 404, status: 404,
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
import * as configContext from "./config-context" import * as configContext from "./config-context"
import * as spawnHelpers from "../../shared/spawn-with-windows-hide" import * as spawnHelpers from "../../shared/spawn-with-windows-hide"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type OpenCodeBinaryModule = typeof import("./opencode-binary") type OpenCodeBinaryModule = typeof import("./opencode-binary")
@@ -92,11 +93,11 @@ describe("getOpenCodeVersion (installer)", () => {
}), }),
) )
const immediateSetTimeout = testCoerce<typeof globalThis.setTimeout>(((handler: TimerHandler) => { const immediateSetTimeout = unsafeTestValue<typeof globalThis.setTimeout>(((handler: TimerHandler) => {
if (typeof handler === "function") { if (typeof handler === "function") {
handler() handler()
} }
return testCoerce<ReturnType<typeof setTimeout>>(1) return unsafeTestValue<ReturnType<typeof setTimeout>>(1)
})) }))
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout) const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
@@ -124,11 +125,11 @@ describe("getOpenCodeVersion (installer)", () => {
}), }),
) )
const immediateSetTimeout = testCoerce<typeof globalThis.setTimeout>(((handler: TimerHandler) => { const immediateSetTimeout = unsafeTestValue<typeof globalThis.setTimeout>(((handler: TimerHandler) => {
if (typeof handler === "function") { if (typeof handler === "function") {
handler() handler()
} }
return testCoerce<ReturnType<typeof setTimeout>>(1) return unsafeTestValue<ReturnType<typeof setTimeout>>(1)
})) }))
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout) const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
@@ -3,6 +3,7 @@
import { afterEach, describe, expect, mock, test } from "bun:test" import { afterEach, describe, expect, mock, test } from "bun:test"
import { getPluginNameWithVersion } from "../config-manager" import { getPluginNameWithVersion } from "../config-manager"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("getPluginNameWithVersion", () => { describe("getPluginNameWithVersion", () => {
const originalFetch = globalThis.fetch const originalFetch = globalThis.fetch
@@ -13,7 +14,7 @@ describe("getPluginNameWithVersion", () => {
test("returns the canonical latest tag when current version matches latest", async () => { test("returns the canonical latest tag when current version matches latest", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }), json: () => Promise.resolve({ latest: "3.13.1", beta: "3.14.0-beta.1" }),
@@ -29,7 +30,7 @@ describe("getPluginNameWithVersion", () => {
test("preserves the canonical prerelease channel when fetch fails", async () => { test("preserves the canonical prerelease channel when fetch fails", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => Promise.reject(new Error("Network error")))) globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() => Promise.reject(new Error("Network error"))))
//#when //#when
const result = await getPluginNameWithVersion("3.14.0-beta.1") const result = await getPluginNameWithVersion("3.14.0-beta.1")
@@ -40,7 +41,7 @@ describe("getPluginNameWithVersion", () => {
test("returns the canonical bare package name for stable fallback", async () => { test("returns the canonical bare package name for stable fallback", async () => {
//#given //#given
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: false, ok: false,
status: 404, status: 404,
+4 -3
View File
@@ -5,6 +5,7 @@ import { join } from "node:path"
import { install } from "./install" import { install } from "./install"
import * as configManager from "./config-manager" import * as configManager from "./config-manager"
import type { InstallArgs } from "./types" import type { InstallArgs } from "./types"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
// Mock console methods to capture output // Mock console methods to capture output
const mockConsoleLog = mock(() => {}) const mockConsoleLog = mock(() => {})
@@ -57,7 +58,7 @@ describe("install CLI - binary check behavior", () => {
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// given mock npm fetch // given mock npm fetch
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }), json: () => Promise.resolve({ latest: "3.0.0" }),
@@ -92,7 +93,7 @@ describe("install CLI - binary check behavior", () => {
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// given mock npm fetch // given mock npm fetch
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }), json: () => Promise.resolve({ latest: "3.0.0" }),
@@ -131,7 +132,7 @@ describe("install CLI - binary check behavior", () => {
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0") getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0")
// given mock npm fetch // given mock npm fetch
globalThis.fetch = testCoerce<typeof fetch>(mock(() => globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
Promise.resolve({ Promise.resolve({
ok: true, ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }), json: () => Promise.resolve({ latest: "3.0.0" }),
+23 -22
View File
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os"
import type { RunContext } from "./types" import type { RunContext } from "./types"
import { _resetForTesting, setSessionAgent } from "../../features/claude-code-session-state" import { _resetForTesting, setSessionAgent } from "../../features/claude-code-session-state"
import { writeState as writeRalphLoopState } from "../../hooks/ralph-loop/storage" import { writeState as writeRalphLoopState } from "../../hooks/ralph-loop/storage"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const testDirs: string[] = [] const testDirs: string[] = []
@@ -26,7 +27,7 @@ function createTempDir(): string {
function createMockContext(directory: string): RunContext { function createMockContext(directory: string): RunContext {
return { return {
client: testCoerce<RunContext["client"]>({ client: unsafeTestValue<RunContext["client"]>({
session: { session: {
todo: mock(() => Promise.resolve({ data: [] })), todo: mock(() => Promise.resolve({ data: [] })),
children: mock(() => Promise.resolve({ data: [] })), children: mock(() => Promise.resolve({ data: [] })),
@@ -155,13 +156,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "child-session" ctx.sessionID = "child-session"
setSessionAgent("child-session", "atlas") setSessionAgent("child-session", "atlas")
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "child-session" ? "root-session" : undefined, parentID: path.id === "child-session" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "child-session" data: path.id === "child-session"
? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }] ? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }]
: [], : [],
@@ -187,13 +188,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "lineage-only-session" ctx.sessionID = "lineage-only-session"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "lineage-only-session" ? "root-session" : undefined, parentID: path.id === "lineage-only-session" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async () => ({ data: [] }))) ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async () => ({ data: [] })))
const { checkCompletionConditions } = await import("./completion") const { checkCompletionConditions } = await import("./completion")
@@ -218,13 +219,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "mismatch-subagent-session" ctx.sessionID = "mismatch-subagent-session"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "mismatch-subagent-session" ? "root-session" : undefined, parentID: path.id === "mismatch-subagent-session" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "mismatch-subagent-session" data: path.id === "mismatch-subagent-session"
? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }]
: [], : [],
@@ -253,13 +254,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "appended-mismatch-session" ctx.sessionID = "appended-mismatch-session"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "appended-mismatch-session" ? "root-session" : undefined, parentID: path.id === "appended-mismatch-session" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "appended-mismatch-session" data: path.id === "appended-mismatch-session"
? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }]
: [], : [],
@@ -288,10 +289,10 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_appended_descendant" ctx.sessionID = "ses_appended_descendant"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async () => { ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async () => {
throw new Error("session lookup failed") throw new Error("session lookup failed")
})) }))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "ses_appended_descendant" data: path.id === "ses_appended_descendant"
? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }] ? [{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }]
: [], : [],
@@ -317,7 +318,7 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_direct_child" ctx.sessionID = "ses_direct_child"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "ses_direct_child" ? "ses_parent" : undefined, parentID: path.id === "ses_direct_child" ? "ses_parent" : undefined,
@@ -347,7 +348,7 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_direct_tracked" ctx.sessionID = "ses_direct_tracked"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: undefined, parentID: undefined,
@@ -374,7 +375,7 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_unknown_child" ctx.sessionID = "ses_unknown_child"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async () => { ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async () => {
throw new Error("lineage unavailable") throw new Error("lineage unavailable")
})) }))
@@ -401,13 +402,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_direct_child" ctx.sessionID = "ses_direct_child"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "ses_direct_child" ? "ses_root_tracked" : undefined, parentID: path.id === "ses_direct_child" ? "ses_root_tracked" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "ses_direct_child" data: path.id === "ses_direct_child"
? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }] ? [{ info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4" } }]
: [], : [],
@@ -437,13 +438,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_child_after_compaction" ctx.sessionID = "ses_child_after_compaction"
setSessionAgent("ses_child_after_compaction", "atlas") setSessionAgent("ses_child_after_compaction", "atlas")
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "ses_child_after_compaction" ? "root-session" : undefined, parentID: path.id === "ses_child_after_compaction" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "ses_child_after_compaction" data: path.id === "ses_child_after_compaction"
? [ ? [
{ info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } }, { info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4" } },
@@ -472,13 +473,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_sqlite_descendant" ctx.sessionID = "ses_sqlite_descendant"
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "ses_sqlite_descendant" ? "root-session" : undefined, parentID: path.id === "ses_sqlite_descendant" ? "root-session" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: path.id === "ses_sqlite_descendant" data: path.id === "ses_sqlite_descendant"
? [ ? [
{ id: "msg_0001", info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } }, { id: "msg_0001", info: { agent: "atlas", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } },
@@ -512,13 +513,13 @@ describe("checkCompletionConditions continuation coverage", () => {
const ctx = createMockContext(directory) const ctx = createMockContext(directory)
ctx.sessionID = "ses_appended_child" ctx.sessionID = "ses_appended_child"
setSessionAgent("ses_appended_child", "atlas") setSessionAgent("ses_appended_child", "atlas")
ctx.client.session.get = testCoerce<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({ ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async ({ path }: { path: { id: string } }) => ({
data: { data: {
id: path.id, id: path.id,
parentID: path.id === "ses_appended_child" ? "ses_root_tracked" : undefined, parentID: path.id === "ses_appended_child" ? "ses_root_tracked" : undefined,
}, },
}))) })))
ctx.client.session.messages = testCoerce<RunContext["client"]["session"]["messages"]>(mock(async () => ({ data: [] }))) ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async () => ({ data: [] })))
const { checkCompletionConditions } = await import("./completion") const { checkCompletionConditions } = await import("./completion")
@@ -1,5 +1,6 @@
import { describe, it, expect, mock, spyOn } from "bun:test" import { describe, it, expect, mock, spyOn } from "bun:test"
import type { RunContext, ChildSession, SessionStatus } from "./types" import type { RunContext, ChildSession, SessionStatus } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const createMockContext = (overrides: { const createMockContext = (overrides: {
childrenBySession?: Record<string, ChildSession[]> childrenBySession?: Record<string, ChildSession[]>
@@ -13,7 +14,7 @@ const createMockContext = (overrides: {
} = overrides } = overrides
return { return {
client: testCoerce<RunContext["client"]>({ client: unsafeTestValue<RunContext["client"]>({
session: { session: {
todo: mock(() => Promise.resolve({ data: [] })), todo: mock(() => Promise.resolve({ data: [] })),
children: mock((opts: { path: { id: string } }) => children: mock((opts: { path: { id: string } }) =>
+2 -1
View File
@@ -1,5 +1,6 @@
import { describe, it, expect, mock, spyOn } from "bun:test" import { describe, it, expect, mock, spyOn } from "bun:test"
import type { RunContext, Todo, ChildSession, SessionStatus } from "./types" import type { RunContext, Todo, ChildSession, SessionStatus } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const createMockContext = (overrides: { const createMockContext = (overrides: {
todo?: Todo[] todo?: Todo[]
@@ -13,7 +14,7 @@ const createMockContext = (overrides: {
} = overrides } = overrides
return { return {
client: testCoerce<RunContext["client"]>({ client: unsafeTestValue<RunContext["client"]>({
session: { session: {
todo: mock(() => Promise.resolve({ data: todo })), todo: mock(() => Promise.resolve({ data: todo })),
children: mock((opts: { path: { id: string } }) => children: mock((opts: { path: { id: string } }) =>
+15 -14
View File
@@ -2,6 +2,7 @@ const { describe, it, expect, spyOn } = require("bun:test")
import type { RunContext } from "./types" import type { RunContext } from "./types"
import { createEventState } from "./events" import { createEventState } from "./events"
import { handleSessionStatus, handleMessagePartUpdated, handleMessageUpdated, handleTuiToast } from "./event-handlers" import { handleSessionStatus, handleMessagePartUpdated, handleMessageUpdated, handleTuiToast } from "./event-handlers"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const createMockContext = (sessionID: string = "test-session"): RunContext => ({ const createMockContext = (sessionID: string = "test-session"): RunContext => ({
sessionID, sessionID,
@@ -23,7 +24,7 @@ describe("handleSessionStatus", () => {
} }
//#when - handleSessionStatus called with idle status //#when - handleSessionStatus called with idle status
handleSessionStatus(ctx, testCoerce(payload), state) handleSessionStatus(ctx, unsafeTestValue(payload), state)
//#then - state.mainSessionIdle === true //#then - state.mainSessionIdle === true
expect(state.mainSessionIdle).toBe(true) expect(state.mainSessionIdle).toBe(true)
@@ -44,7 +45,7 @@ describe("handleSessionStatus", () => {
} }
//#when - handleSessionStatus called with busy status //#when - handleSessionStatus called with busy status
handleSessionStatus(ctx, testCoerce(payload), state) handleSessionStatus(ctx, unsafeTestValue(payload), state)
//#then - state.mainSessionIdle === false //#then - state.mainSessionIdle === false
expect(state.mainSessionIdle).toBe(false) expect(state.mainSessionIdle).toBe(false)
@@ -65,7 +66,7 @@ describe("handleSessionStatus", () => {
} }
//#when - handleSessionStatus called with different session ID //#when - handleSessionStatus called with different session ID
handleSessionStatus(ctx, testCoerce(payload), state) handleSessionStatus(ctx, unsafeTestValue(payload), state)
//#then - state.mainSessionIdle remains unchanged //#then - state.mainSessionIdle remains unchanged
expect(state.mainSessionIdle).toBe(true) expect(state.mainSessionIdle).toBe(true)
@@ -86,7 +87,7 @@ describe("handleSessionStatus", () => {
} }
//#when - handleSessionStatus called with camelCase sessionId //#when - handleSessionStatus called with camelCase sessionId
handleSessionStatus(ctx, testCoerce(payload), state) handleSessionStatus(ctx, unsafeTestValue(payload), state)
//#then - state.mainSessionIdle === true //#then - state.mainSessionIdle === true
expect(state.mainSessionIdle).toBe(true) expect(state.mainSessionIdle).toBe(true)
@@ -114,7 +115,7 @@ describe("handleMessagePartUpdated", () => {
} }
//#when //#when
handleMessagePartUpdated(ctx, testCoerce(payload), state) handleMessagePartUpdated(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.hasReceivedMeaningfulWork).toBe(true) expect(state.hasReceivedMeaningfulWork).toBe(true)
@@ -142,7 +143,7 @@ describe("handleMessagePartUpdated", () => {
} }
//#when //#when
handleMessagePartUpdated(ctx, testCoerce(payload), state) handleMessagePartUpdated(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.hasReceivedMeaningfulWork).toBe(false) expect(state.hasReceivedMeaningfulWork).toBe(false)
@@ -170,7 +171,7 @@ describe("handleMessagePartUpdated", () => {
} }
//#when //#when
handleMessagePartUpdated(ctx, testCoerce(payload), state) handleMessagePartUpdated(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.currentTool).toBe("read") expect(state.currentTool).toBe("read")
@@ -200,7 +201,7 @@ describe("handleMessagePartUpdated", () => {
} }
//#when //#when
handleMessagePartUpdated(ctx, testCoerce(payload), state) handleMessagePartUpdated(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.currentTool).toBeNull() expect(state.currentTool).toBeNull()
@@ -225,7 +226,7 @@ describe("handleMessagePartUpdated", () => {
} }
//#when //#when
handleMessagePartUpdated(ctx, testCoerce(payload), state) handleMessagePartUpdated(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.hasReceivedMeaningfulWork).toBe(true) expect(state.hasReceivedMeaningfulWork).toBe(true)
@@ -243,7 +244,7 @@ describe("handleMessagePartUpdated", () => {
handleMessageUpdated( handleMessageUpdated(
ctx, ctx,
testCoerce({ unsafeTestValue({
type: "message.updated", type: "message.updated",
properties: { properties: {
info: { info: {
@@ -262,7 +263,7 @@ describe("handleMessagePartUpdated", () => {
// when // when
handleMessagePartUpdated( handleMessagePartUpdated(
ctx, ctx,
testCoerce({ unsafeTestValue({
type: "message.part.updated", type: "message.part.updated",
properties: { properties: {
part: { part: {
@@ -280,7 +281,7 @@ describe("handleMessagePartUpdated", () => {
handleMessagePartUpdated( handleMessagePartUpdated(
ctx, ctx,
testCoerce({ unsafeTestValue({
type: "message.part.updated", type: "message.part.updated",
properties: { properties: {
part: { part: {
@@ -323,7 +324,7 @@ describe("handleTuiToast", () => {
} }
//#when //#when
handleTuiToast(ctx, testCoerce(payload), state) handleTuiToast(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.mainSessionError).toBe(true) expect(state.mainSessionError).toBe(true)
@@ -344,7 +345,7 @@ describe("handleTuiToast", () => {
} }
//#when //#when
handleTuiToast(ctx, testCoerce(payload), state) handleTuiToast(ctx, unsafeTestValue(payload), state)
//#then //#then
expect(state.mainSessionError).toBe(false) expect(state.mainSessionError).toBe(false)
+8 -7
View File
@@ -7,6 +7,7 @@ import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hid
import type { OpencodeClient } from "./types" import type { OpencodeClient } from "./types"
import * as originalSdk from "@opencode-ai/sdk" import * as originalSdk from "@opencode-ai/sdk"
import * as originalPortUtils from "../../shared/port-utils" import * as originalPortUtils from "../../shared/port-utils"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const mockServerClose = mock(() => {}) const mockServerClose = mock(() => {})
const mockCreateOpencode = mock(() => const mockCreateOpencode = mock(() =>
@@ -56,7 +57,7 @@ function createMockWriteStream(): MockWriteStream {
const createMockClient = ( const createMockClient = (
getResult?: { error?: unknown; data?: { id: string } } getResult?: { error?: unknown; data?: { id: string } }
): OpencodeClient => (testCoerce<OpencodeClient>({ ): OpencodeClient => (unsafeTestValue<OpencodeClient>({
session: { session: {
get: mock((opts: { path: { id: string } }) => get: mock((opts: { path: { id: string } }) =>
Promise.resolve(getResult ?? { data: { id: opts.path.id } }) Promise.resolve(getResult ?? { data: { id: opts.path.id } })
@@ -78,8 +79,8 @@ describe("integration: --json mode", () => {
summary: "Test summary", summary: "Test summary",
} }
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
// when // when
@@ -103,8 +104,8 @@ describe("integration: --json mode", () => {
const mockStdout = createMockWriteStream() const mockStdout = createMockWriteStream()
const mockStderr = createMockWriteStream() const mockStderr = createMockWriteStream()
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
manager.redirectToStderr() manager.redirectToStderr()
@@ -272,8 +273,8 @@ describe("integration: option combinations", () => {
summary: "Test completed", summary: "Test completed",
} }
const jsonManager = createJsonOutputManager({ const jsonManager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
jsonManager.redirectToStderr() jsonManager.redirectToStderr()
spawnSpy.mockClear() spawnSpy.mockClear()
+13 -12
View File
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach } from "bun:test" import { describe, it, expect, beforeEach } from "bun:test"
import type { RunResult } from "./types" import type { RunResult } from "./types"
import { createJsonOutputManager } from "./json-output" import { createJsonOutputManager } from "./json-output"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
interface MockWriteStream { interface MockWriteStream {
write: (chunk: string) => boolean write: (chunk: string) => boolean
@@ -31,8 +32,8 @@ describe("createJsonOutputManager", () => {
it("causes stdout writes to go to stderr", () => { it("causes stdout writes to go to stderr", () => {
// given // given
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
manager.redirectToStderr() manager.redirectToStderr()
@@ -49,8 +50,8 @@ describe("createJsonOutputManager", () => {
it("reverses the redirect", () => { it("reverses the redirect", () => {
// given // given
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
manager.redirectToStderr() manager.redirectToStderr()
@@ -75,8 +76,8 @@ describe("createJsonOutputManager", () => {
summary: "Test summary", summary: "Test summary",
} }
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
// when // when
@@ -98,8 +99,8 @@ describe("createJsonOutputManager", () => {
summary: "Test summary", summary: "Test summary",
} }
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
// when // when
@@ -126,8 +127,8 @@ describe("createJsonOutputManager", () => {
summary: "Test", summary: "Test",
} }
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
manager.redirectToStderr() manager.redirectToStderr()
@@ -148,8 +149,8 @@ describe("createJsonOutputManager", () => {
it("work correctly", () => { it("work correctly", () => {
// given // given
const manager = createJsonOutputManager({ const manager = createJsonOutputManager({
stdout: testCoerce<NodeJS.WriteStream>(mockStdout), stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
stderr: testCoerce<NodeJS.WriteStream>(mockStderr), stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
}) })
// when // when
+8 -7
View File
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, it, expect, mock, spyOn } from "bun:te
import type { RunContext, Todo, ChildSession, SessionStatus } from "./types" import type { RunContext, Todo, ChildSession, SessionStatus } from "./types"
import { createEventState } from "./events" import { createEventState } from "./events"
import { pollForCompletion } from "./poll-for-completion" import { pollForCompletion } from "./poll-for-completion"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const createMockContext = (overrides: { const createMockContext = (overrides: {
todo?: Todo[] todo?: Todo[]
@@ -15,7 +16,7 @@ const createMockContext = (overrides: {
} = overrides } = overrides
return { return {
client: testCoerce<RunContext["client"]>({ client: unsafeTestValue<RunContext["client"]>({
session: { session: {
todo: mock(() => Promise.resolve({ data: todo })), todo: mock(() => Promise.resolve({ data: todo })),
children: mock((opts: { path: { id: string } }) => children: mock((opts: { path: { id: string } }) =>
@@ -124,7 +125,7 @@ describe("pollForCompletion", () => {
let todoCallCount = 0 let todoCallCount = 0
let busyInserted = false let busyInserted = false
;(testCoerce(ctx.client.session)).todo = mock(async () => { ;(unsafeTestValue(ctx.client.session)).todo = mock(async () => {
todoCallCount++ todoCallCount++
if (todoCallCount === 1 && !busyInserted) { if (todoCallCount === 1 && !busyInserted) {
busyInserted = true busyInserted = true
@@ -133,10 +134,10 @@ describe("pollForCompletion", () => {
} }
return { data: [] } return { data: [] }
}) })
;(testCoerce(ctx.client.session)).children = mock(() => ;(unsafeTestValue(ctx.client.session)).children = mock(() =>
Promise.resolve({ data: [] }) Promise.resolve({ data: [] })
) )
;(testCoerce(ctx.client.session)).status = mock(() => ;(unsafeTestValue(ctx.client.session)).status = mock(() =>
Promise.resolve({ data: {} }) Promise.resolve({ data: {} })
) )
@@ -322,17 +323,17 @@ describe("pollForCompletion", () => {
const abortController = new AbortController() const abortController = new AbortController()
let pollTick = 0 let pollTick = 0
;(testCoerce(ctx.client.session)).todo = mock(async () => { ;(unsafeTestValue(ctx.client.session)).todo = mock(async () => {
pollTick++ pollTick++
if (pollTick === 2) { if (pollTick === 2) {
eventState.currentTool = "task" eventState.currentTool = "task"
} }
return { data: [] } return { data: [] }
}) })
;(testCoerce(ctx.client.session)).children = mock(() => ;(unsafeTestValue(ctx.client.session)).children = mock(() =>
Promise.resolve({ data: [] }) Promise.resolve({ data: [] })
) )
;(testCoerce(ctx.client.session)).status = mock(() => ;(unsafeTestValue(ctx.client.session)).status = mock(() =>
Promise.resolve({ data: {} }) Promise.resolve({ data: {} })
) )
+2 -1
View File
@@ -1,4 +1,5 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
import { resolveSession } from "./session-resolver"; import { resolveSession } from "./session-resolver";
@@ -10,7 +11,7 @@ const createMockClient = (overrides: {
} = {}): OpencodeClient => { } = {}): OpencodeClient => {
const { getResult, createResults = [] } = overrides const { getResult, createResults = [] } = overrides
let createCallIndex = 0 let createCallIndex = 0
return testCoerce<OpencodeClient>({ return unsafeTestValue<OpencodeClient>({
session: { session: {
get: mock((opts: { path: { id: string } }) => get: mock((opts: { path: { id: string } }) =>
Promise.resolve(getResult ?? { data: { id: opts.path.id } }) Promise.resolve(getResult ?? { data: { id: opts.path.id } })
+4 -3
View File
@@ -2,6 +2,7 @@
import { describe, expect, it } from "bun:test" import { describe, expect, it } from "bun:test"
import { createTimestampTransformer, createTimestampedStdoutController } from "./timestamp-output" import { createTimestampTransformer, createTimestampedStdoutController } from "./timestamp-output"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createLocalDate(hours: number, minutes: number, seconds: number): Date { function createLocalDate(hours: number, minutes: number, seconds: number): Date {
return new Date(2026, 1, 19, hours, minutes, seconds) return new Date(2026, 1, 19, hours, minutes, seconds)
@@ -87,7 +88,7 @@ describe("createTimestampedStdoutController", () => {
it("prefixes stdout writes when enabled", () => { it("prefixes stdout writes when enabled", () => {
// given // given
const stdout = createMockWriteStream() const stdout = createMockWriteStream()
const controller = createTimestampedStdoutController(testCoerce<NodeJS.WriteStream>(stdout)) const controller = createTimestampedStdoutController(unsafeTestValue<NodeJS.WriteStream>(stdout))
// when // when
controller.enable() controller.enable()
@@ -101,7 +102,7 @@ describe("createTimestampedStdoutController", () => {
it("restores original write function", () => { it("restores original write function", () => {
// given // given
const stdout = createMockWriteStream() const stdout = createMockWriteStream()
const controller = createTimestampedStdoutController(testCoerce<NodeJS.WriteStream>(stdout)) const controller = createTimestampedStdoutController(unsafeTestValue<NodeJS.WriteStream>(stdout))
controller.enable() controller.enable()
// when // when
@@ -118,7 +119,7 @@ describe("createTimestampedStdoutController", () => {
it("supports Uint8Array chunks and encoding", () => { it("supports Uint8Array chunks and encoding", () => {
// given // given
const stdout = createMockWriteStream() const stdout = createMockWriteStream()
const controller = createTimestampedStdoutController(testCoerce<NodeJS.WriteStream>(stdout)) const controller = createTimestampedStdoutController(unsafeTestValue<NodeJS.WriteStream>(stdout))
// when // when
controller.enable() controller.enable()
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { buildBackgroundTaskNotificationText } from "./background-task-notification-template" import { buildBackgroundTaskNotificationText } from "./background-task-notification-template"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("buildBackgroundTaskNotificationText", () => { describe("buildBackgroundTaskNotificationText", () => {
describe("#given one task still running after a completed task notification", () => { describe("#given one task still running after a completed task notification", () => {
@@ -134,7 +135,7 @@ Use \`background_output(task_id="<id>")\` to retrieve each result.
const notification = buildBackgroundTaskNotificationText({ const notification = buildBackgroundTaskNotificationText({
task: { task: {
id: "bg_abc123", id: "bg_abc123",
description: testCoerce<string>(undefined), description: unsafeTestValue<string>(undefined),
status: "completed", status: "completed",
}, },
duration: "5s", duration: "5s",
@@ -142,8 +143,8 @@ Use \`background_output(task_id="<id>")\` to retrieve each result.
allComplete: true, allComplete: true,
remainingCount: 0, remainingCount: 0,
completedTasks: [ completedTasks: [
{ id: "bg_abc123", description: testCoerce<string>(undefined), status: "completed" }, { id: "bg_abc123", description: unsafeTestValue<string>(undefined), status: "completed" },
{ id: "bg_def456", description: testCoerce<string>(undefined), status: "completed" }, { id: "bg_def456", description: unsafeTestValue<string>(undefined), status: "completed" },
], ],
}) })
@@ -230,7 +231,7 @@ Use \`background_output(task_id="<id>")\` to retrieve each result.
const notification = buildBackgroundTaskNotificationText({ const notification = buildBackgroundTaskNotificationText({
task: { task: {
id: "bg_xyz789", id: "bg_xyz789",
description: testCoerce<string>(undefined), description: unsafeTestValue<string>(undefined),
status: "completed", status: "completed",
}, },
duration: "3s", duration: "3s",
@@ -12,6 +12,7 @@ import {
setCompactionAgentConfigCheckpoint, setCompactionAgentConfigCheckpoint,
} from "../../shared/compaction-agent-config-checkpoint" } from "../../shared/compaction-agent-config-checkpoint"
import { getCompactionPartStorageDir } from "../../shared/compaction-marker" import { getCompactionPartStorageDir } from "../../shared/compaction-marker"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("isCompactionAgent", () => { describe("isCompactionAgent", () => {
describe("#given agent name variations", () => { describe("#given agent name variations", () => {
@@ -49,7 +50,7 @@ describe("isCompactionAgent", () => {
test("returns false for null", () => { test("returns false for null", () => {
// when // when
const result = isCompactionAgent(testCoerce<string>(null)) const result = isCompactionAgent(unsafeTestValue<string>(null))
// then // then
expect(result).toBe(false) expect(result).toBe(false)
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os"
import type { BackgroundTaskConfig } from "../../config/schema" import type { BackgroundTaskConfig } from "../../config/schema"
import { BackgroundManager } from "./manager" import { BackgroundManager } from "./manager"
import type { BackgroundTask } from "./types" import type { BackgroundTask } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createManager(config?: BackgroundTaskConfig): BackgroundManager { function createManager(config?: BackgroundTaskConfig): BackgroundManager {
const client = { const client = {
@@ -16,8 +17,8 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager {
}, },
} }
const manager = new BackgroundManager({ pluginContext: testCoerce<PluginInput>({ client, directory: tmpdir() }), config: config }) const manager = new BackgroundManager({ pluginContext: unsafeTestValue<PluginInput>({ client, directory: tmpdir() }), config: config })
const testManager = testCoerce<{ const testManager = unsafeTestValue<{
enqueueNotificationForParent: (sessionId: string, fn: () => Promise<void>) => Promise<void> enqueueNotificationForParent: (sessionId: string, fn: () => Promise<void>) => Promise<void>
notifyParentSession: (task: BackgroundTask) => Promise<void> notifyParentSession: (task: BackgroundTask) => Promise<void>
tasks: Map<string, BackgroundTask> tasks: Map<string, BackgroundTask>
@@ -32,7 +33,7 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager {
} }
function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> { function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> {
return (testCoerce<{ tasks: Map<string, BackgroundTask> }>(manager)).tasks return (unsafeTestValue<{ tasks: Map<string, BackgroundTask> }>(manager)).tasks
} }
async function flushAsyncWork() { async function flushAsyncWork() {
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { BackgroundManager } from "./manager" import { BackgroundManager } from "./manager"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("BackgroundManager session permission", () => { describe("BackgroundManager session permission", () => {
test("passes query directory when loading the parent session", async () => { test("passes query directory when loading the parent session", async () => {
@@ -21,7 +22,7 @@ describe("BackgroundManager session permission", () => {
}, },
} }
const directory = tmpdir() const directory = tmpdir()
const manager = new BackgroundManager({ pluginContext: testCoerce<PluginInput>({ client, directory }) }) const manager = new BackgroundManager({ pluginContext: unsafeTestValue<PluginInput>({ client, directory }) })
// when // when
await manager.launch({ await manager.launch({
@@ -62,7 +63,7 @@ describe("BackgroundManager session permission", () => {
abort: async () => ({}), abort: async () => ({}),
}, },
} }
const manager = new BackgroundManager({ pluginContext: testCoerce<PluginInput>({ client, directory: tmpdir() }) }) const manager = new BackgroundManager({ pluginContext: unsafeTestValue<PluginInput>({ client, directory: tmpdir() }) })
// when // when
await manager.launch({ await manager.launch({
@@ -2,12 +2,13 @@ import { describe, expect, mock, test } from "bun:test"
import type { OpencodeClient } from "./opencode-client" import type { OpencodeClient } from "./opencode-client"
import { verifySessionExists } from "./session-existence" import { verifySessionExists } from "./session-existence"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("verifySessionExists", () => { describe("verifySessionExists", () => {
test("passes query directory to session lookup when provided", async () => { test("passes query directory to session lookup when provided", async () => {
// given // given
const get = mock(async () => ({ data: { id: "session-123" } })) const get = mock(async () => ({ data: { id: "session-123" } }))
const client = testCoerce<OpencodeClient>({ const client = unsafeTestValue<OpencodeClient>({
session: { session: {
get, get,
}, },
@@ -6,6 +6,7 @@ import {
DEFAULT_MAX_SUBAGENT_DEPTH, DEFAULT_MAX_SUBAGENT_DEPTH,
createSubagentDepthLimitError, createSubagentDepthLimitError,
} from "./subagent-spawn-limits" } from "./subagent-spawn-limits"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createMockClient(sessionGet: OpencodeClient["session"]["get"]): OpencodeClient { function createMockClient(sessionGet: OpencodeClient["session"]["get"]): OpencodeClient {
return { return {
@@ -20,7 +21,7 @@ describe("resolveSubagentSpawnContext", () => {
test("passes query.directory to each session.get call", async () => { test("passes query.directory to each session.get call", async () => {
// given // given
const sessionGetCalls: Array<Record<string, unknown>> = [] const sessionGetCalls: Array<Record<string, unknown>> = []
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (input) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (input) => {
sessionGetCalls.push(input as Record<string, unknown>) sessionGetCalls.push(input as Record<string, unknown>)
if (input.path.id === "child-session") { if (input.path.id === "child-session") {
return { data: { id: "child-session", parentID: "root-session" } } return { data: { id: "child-session", parentID: "root-session" } }
@@ -50,7 +51,7 @@ describe("resolveSubagentSpawnContext", () => {
describe("#given session.get returns an SDK error response", () => { describe("#given session.get returns an SDK error response", () => {
test("throws a fail-closed spawn blocked error", async () => { test("throws a fail-closed spawn blocked error", async () => {
// given // given
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async () => ({ const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async () => ({
error: "lookup failed", error: "lookup failed",
data: undefined, data: undefined,
})))) }))))
@@ -66,7 +67,7 @@ describe("resolveSubagentSpawnContext", () => {
describe("#given session.get returns no session data", () => { describe("#given session.get returns no session data", () => {
test("throws a fail-closed spawn blocked error", async () => { test("throws a fail-closed spawn blocked error", async () => {
// given // given
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async () => ({ const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async () => ({
data: undefined, data: undefined,
})))) }))))
@@ -81,7 +82,7 @@ describe("resolveSubagentSpawnContext", () => {
describe("depth calculation smoke tests (regression guard)", () => { describe("depth calculation smoke tests (regression guard)", () => {
test("root session (no parentID) reports depth 0 and childDepth 1", async () => { test("root session (no parentID) reports depth 0 and childDepth 1", async () => {
// given - a root session with no parent // given - a root session with no parent
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (opts) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (opts) => {
if (opts.path.id === "root-session") { if (opts.path.id === "root-session") {
return { data: { id: "root-session", parentID: undefined } } return { data: { id: "root-session", parentID: undefined } }
} }
@@ -99,7 +100,7 @@ describe("resolveSubagentSpawnContext", () => {
test("depth-1 child reports childDepth 2", async () => { test("depth-1 child reports childDepth 2", async () => {
// given - child -> root chain // given - child -> root chain
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (opts) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (opts) => {
if (opts.path.id === "child-1") { if (opts.path.id === "child-1") {
return { data: { id: "child-1", parentID: "root-session" } } return { data: { id: "child-1", parentID: "root-session" } }
} }
@@ -120,7 +121,7 @@ describe("resolveSubagentSpawnContext", () => {
test("depth-2 grandchild reports childDepth 3", async () => { test("depth-2 grandchild reports childDepth 3", async () => {
// given - grandchild -> child -> root chain // given - grandchild -> child -> root chain
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (opts) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (opts) => {
const sessions: Record<string, { id: string; parentID?: string }> = { const sessions: Record<string, { id: string; parentID?: string }> = {
"grandchild": { id: "grandchild", parentID: "child" }, "grandchild": { id: "grandchild", parentID: "child" },
"child": { id: "child", parentID: "root" }, "child": { id: "child", parentID: "root" },
@@ -153,7 +154,7 @@ describe("resolveSubagentSpawnContext", () => {
} }
} }
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (opts) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (opts) => {
const session = sessions[opts.path.id] const session = sessions[opts.path.id]
if (session) return { data: session } if (session) return { data: session }
return { error: "not found", data: undefined } return { error: "not found", data: undefined }
@@ -170,7 +171,7 @@ describe("resolveSubagentSpawnContext", () => {
test("detects parent cycle and throws", async () => { test("detects parent cycle and throws", async () => {
// given - A -> B -> A (cycle) // given - A -> B -> A (cycle)
const client = createMockClient(testCoerce<OpencodeClient["session"]["get"]>((async (opts) => { const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async (opts) => {
const sessions: Record<string, { id: string; parentID?: string }> = { const sessions: Record<string, { id: string; parentID?: string }> = {
"session-a": { id: "session-a", parentID: "session-b" }, "session-a": { id: "session-a", parentID: "session-b" },
"session-b": { id: "session-b", parentID: "session-a" }, "session-b": { id: "session-b", parentID: "session-a" },
@@ -3,6 +3,7 @@ import { ContextCollector } from "./collector"
import { import {
createContextInjectorMessagesTransformHook, createContextInjectorMessagesTransformHook,
} from "./injector" } from "./injector"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("createContextInjectorMessagesTransformHook", () => { describe("createContextInjectorMessagesTransformHook", () => {
let collector: ContextCollector let collector: ContextCollector
@@ -51,7 +52,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
createMockMessage("user", "Second message", sessionID), createMockMessage("user", "Second message", sessionID),
] ]
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const output = testCoerce({ messages }) const output = unsafeTestValue({ messages })
// when // when
await hook["experimental.chat.messages.transform"]!({}, output) await hook["experimental.chat.messages.transform"]!({}, output)
@@ -115,7 +116,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
const sessionID = "ses_transform2" const sessionID = "ses_transform2"
const messages = [createMockMessage("user", "Hello world", sessionID)] const messages = [createMockMessage("user", "Hello world", sessionID)]
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const output = testCoerce({ messages }) const output = unsafeTestValue({ messages })
// when // when
await hook["experimental.chat.messages.transform"]!({}, output) await hook["experimental.chat.messages.transform"]!({}, output)
@@ -135,7 +136,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
}) })
const messages = [createMockMessage("assistant", "Response", sessionID)] const messages = [createMockMessage("assistant", "Response", sessionID)]
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const output = testCoerce({ messages }) const output = unsafeTestValue({ messages })
// when // when
await hook["experimental.chat.messages.transform"]!({}, output) await hook["experimental.chat.messages.transform"]!({}, output)
@@ -156,7 +157,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
}) })
const messages = [createMockMessage("user", "Message", sessionID)] const messages = [createMockMessage("user", "Message", sessionID)]
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const output = testCoerce({ messages }) const output = unsafeTestValue({ messages })
// when // when
await hook["experimental.chat.messages.transform"]!({}, output) await hook["experimental.chat.messages.transform"]!({}, output)
@@ -11,6 +11,7 @@ import {
injectHookMessage, injectHookMessage,
} from "./injector" } from "./injector"
import { getCompactionPartStorageDir } from "../../shared/compaction-marker" import { getCompactionPartStorageDir } from "../../shared/compaction-marker"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
//#region Mocks //#region Mocks
@@ -73,7 +74,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
{ info: { agent: "sisyphus", model: { providerID: "anthropic", modelID: "claude-opus-4" } } }, { info: { agent: "sisyphus", model: { providerID: "anthropic", modelID: "claude-opus-4" } } },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toEqual({ expect(result).toEqual({
agent: "sisyphus", agent: "sisyphus",
@@ -87,7 +88,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
{ info: { agent: "sisyphus", providerID: "openai", modelID: "gpt-5" } }, { info: { agent: "sisyphus", providerID: "openai", modelID: "gpt-5" } },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toEqual({ expect(result).toEqual({
agent: "sisyphus", agent: "sisyphus",
@@ -102,7 +103,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
{ id: "msg_new", info: { agent: "new-agent", model: { providerID: "new", modelID: "model" }, time: { created: 20 } } }, { id: "msg_new", info: { agent: "new-agent", model: { providerID: "new", modelID: "model" }, time: { created: 20 } } },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result?.agent).toBe("new-agent") expect(result?.agent).toBe("new-agent")
}) })
@@ -112,7 +113,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
{ info: { agent: "partial-agent" } }, { info: { agent: "partial-agent" } },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result?.agent).toBe("partial-agent") expect(result?.agent).toBe("partial-agent")
}) })
@@ -123,7 +124,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
{ info: {} }, { info: {} },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBeNull() expect(result).toBeNull()
}) })
@@ -131,7 +132,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
it("returns null when messages array is empty", async () => { it("returns null when messages array is empty", async () => {
const mockClient = createMockClient([]) const mockClient = createMockClient([])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBeNull() expect(result).toBeNull()
}) })
@@ -145,7 +146,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
}, },
} }
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBeNull() expect(result).toBeNull()
}) })
@@ -161,7 +162,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
}, },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result?.tools).toEqual({ edit: true, write: false }) 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 } } }, { id: "msg_older", info: { agent: "newest-by-time", model: { providerID: "openai", modelID: "gpt-5" }, time: { created: 100 } } },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result?.agent).toBe("newest-by-time") expect(result?.agent).toBe("newest-by-time")
}) })
@@ -190,7 +191,7 @@ describe("findNearestMessageWithFieldsFromSDK", () => {
}, },
]) ])
const result = await findNearestMessageWithFieldsFromSDK(testCoerce(mockClient), "ses_123") const result = await findNearestMessageWithFieldsFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result?.agent).toBe("sisyphus") expect(result?.agent).toBe("sisyphus")
}) })
@@ -252,7 +253,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
{ info: { agent: "second-agent" } }, { info: { agent: "second-agent" } },
]) ])
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBe("first-agent") expect(result).toBe("first-agent")
}) })
@@ -263,7 +264,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
{ id: "msg_early", info: { agent: "earliest-agent", time: { created: 10 } } }, { id: "msg_early", info: { agent: "earliest-agent", time: { created: 10 } } },
]) ])
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBe("earliest-agent") expect(result).toBe("earliest-agent")
}) })
@@ -274,7 +275,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
{ id: "msg_real", info: { agent: "sisyphus", time: { created: 20 } } }, { id: "msg_real", info: { agent: "sisyphus", time: { created: 20 } } },
]) ])
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBe("sisyphus") expect(result).toBe("sisyphus")
}) })
@@ -285,7 +286,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
{ info: { agent: "first-real-agent" } }, { info: { agent: "first-real-agent" } },
]) ])
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBe("first-real-agent") expect(result).toBe("first-real-agent")
}) })
@@ -296,7 +297,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
{ info: {} }, { info: {} },
]) ])
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBeNull() expect(result).toBeNull()
}) })
@@ -310,7 +311,7 @@ describe("findFirstMessageWithAgentFromSDK", () => {
}, },
} }
const result = await findFirstMessageWithAgentFromSDK(testCoerce(mockClient), "ses_123") const result = await findFirstMessageWithAgentFromSDK(unsafeTestValue(mockClient), "ses_123")
expect(result).toBeNull() expect(result).toBeNull()
}) })
@@ -6,6 +6,7 @@ import type { OAuthTokenData } from "../mcp-oauth/storage"
import { setHttpClientDependenciesForTesting } from "./http-client" import { setHttpClientDependenciesForTesting } from "./http-client"
import { setStdioClientDependenciesForTesting } from "./stdio-client" import { setStdioClientDependenciesForTesting } from "./stdio-client"
import { SkillMcpManager } from "./manager" import { SkillMcpManager } from "./manager"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure"))) const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure")))
const mockHttpClose = mock(() => Promise.resolve()) const mockHttpClose = mock(() => Promise.resolve())
@@ -634,7 +635,7 @@ describe("SkillMcpManager", () => {
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
} }
const getOrCreateSpy = spyOn(testCoerce(manager), "getOrCreateClientWithRetry") const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry")
getOrCreateSpy.mockResolvedValue(mockClient) getOrCreateSpy.mockResolvedValue(mockClient)
// when // when
@@ -668,7 +669,7 @@ describe("SkillMcpManager", () => {
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
} }
const getOrCreateSpy = spyOn(testCoerce(manager), "getOrCreateClientWithRetry") const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry")
getOrCreateSpy.mockResolvedValue(mockClient) getOrCreateSpy.mockResolvedValue(mockClient)
// when / #then // when / #then
@@ -700,7 +701,7 @@ describe("SkillMcpManager", () => {
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
} }
const getOrCreateSpy = spyOn(testCoerce(manager), "getOrCreateClientWithRetry") const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry")
getOrCreateSpy.mockResolvedValue(mockClient) getOrCreateSpy.mockResolvedValue(mockClient)
// when / #then // when / #then
@@ -929,7 +930,7 @@ describe("SkillMcpManager", () => {
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
} }
const getOrCreateSpy = spyOn(testCoerce(manager), "getOrCreateClientWithRetry") const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry")
getOrCreateSpy.mockResolvedValue(mockClient) getOrCreateSpy.mockResolvedValue(mockClient)
// when // when
@@ -962,7 +963,7 @@ describe("SkillMcpManager", () => {
close: mock(() => Promise.resolve()), close: mock(() => Promise.resolve()),
} }
const getOrCreateSpy = spyOn(testCoerce(manager), "getOrCreateClientWithRetry") const getOrCreateSpy = spyOn(unsafeTestValue(manager), "getOrCreateClientWithRetry")
getOrCreateSpy.mockResolvedValue(mockClient) getOrCreateSpy.mockResolvedValue(mockClient)
// when / #then // when / #then
@@ -1,6 +1,7 @@
declare const require: (name: string) => any declare const require: (name: string) => any
const { describe, test, expect, beforeEach, afterEach, mock } = require("bun:test") const { describe, test, expect, beforeEach, afterEach, mock } = require("bun:test")
import type { ConcurrencyManager } from "../background-agent/concurrency" import type { ConcurrencyManager } from "../background-agent/concurrency"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type TaskToastManagerClass = typeof import("./manager").TaskToastManager type TaskToastManagerClass = typeof import("./manager").TaskToastManager
@@ -20,7 +21,7 @@ describe("TaskToastManager", () => {
showToast: mock(() => Promise.resolve()), showToast: mock(() => Promise.resolve()),
}, },
} }
mockConcurrencyManager = testCoerce<ConcurrencyManager>({ mockConcurrencyManager = unsafeTestValue<ConcurrencyManager>({
getConcurrencyLimit: mock(() => 5), getConcurrencyLimit: mock(() => 5),
}) })
@@ -28,7 +29,7 @@ describe("TaskToastManager", () => {
TaskToastManager = mod.TaskToastManager TaskToastManager = mod.TaskToastManager
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
toastManager = new TaskToastManager(testCoerce(mockClient), mockConcurrencyManager) toastManager = new TaskToastManager(unsafeTestValue(mockClient), mockConcurrencyManager)
}) })
afterEach(() => { afterEach(() => {
@@ -108,14 +109,14 @@ describe("TaskToastManager", () => {
test("should display concurrency limit info when available", () => { test("should display concurrency limit info when available", () => {
// given - a concurrency manager with known limit // given - a concurrency manager with known limit
const mockConcurrencyWithCounts = testCoerce<ConcurrencyManager>({ const mockConcurrencyWithCounts = unsafeTestValue<ConcurrencyManager>({
getConcurrencyLimit: mock(() => 5), getConcurrencyLimit: mock(() => 5),
getRunningCount: mock(() => 2), getRunningCount: mock(() => 2),
getQueuedCount: mock(() => 1), getQueuedCount: mock(() => 1),
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const managerWithConcurrency = new TaskToastManager(testCoerce(mockClient), mockConcurrencyWithCounts) const managerWithConcurrency = new TaskToastManager(unsafeTestValue(mockClient), mockConcurrencyWithCounts)
// when - a task is added // when - a task is added
managerWithConcurrency.addTask({ managerWithConcurrency.addTask({
@@ -357,11 +358,11 @@ describe("TaskToastManager", () => {
test("should show model name in queued tasks too", () => { test("should show model name in queued tasks too", () => {
// given - a concurrency manager that limits to 1 // given - a concurrency manager that limits to 1
const limitedConcurrency = testCoerce<ConcurrencyManager>({ const limitedConcurrency = unsafeTestValue<ConcurrencyManager>({
getConcurrencyLimit: mock(() => 1), getConcurrencyLimit: mock(() => 1),
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const limitedManager = new TaskToastManager(testCoerce(mockClient), limitedConcurrency) const limitedManager = new TaskToastManager(unsafeTestValue(mockClient), limitedConcurrency)
limitedManager.addTask({ limitedManager.addTask({
id: "task_running", id: "task_running",
@@ -16,6 +16,7 @@ import {
import { saveRuntimeState } from "../team-state-store/store" import { saveRuntimeState } from "../team-state-store/store"
import type { RuntimeState } from "../types" import type { RuntimeState } from "../types"
import { cleanupTeamRunResources } from "./cleanup-team-run-resources" import { cleanupTeamRunResources } from "./cleanup-team-run-resources"
import { unsafeTestValue } from "../../../../test-support/unsafe-test-value"
const temporaryDirectories: string[] = [] const temporaryDirectories: string[] = []
@@ -41,7 +42,7 @@ function createRuntimeState(teamRunId: string): RuntimeState {
} }
function createStubBgMgr(): BackgroundManager { function createStubBgMgr(): BackgroundManager {
return testCoerce<BackgroundManager>({ return unsafeTestValue<BackgroundManager>({
cancelTask: async () => undefined, cancelTask: async () => undefined,
}) })
} }
@@ -1,6 +1,7 @@
import { describe, test, expect } from "bun:test" import { describe, test, expect } from "bun:test"
import { TmuxPollingManager } from "./polling-manager" import { TmuxPollingManager } from "./polling-manager"
import type { TrackedSession } from "./types" import type { TrackedSession } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("TmuxPollingManager overlap", () => { describe("TmuxPollingManager overlap", () => {
test("skips overlapping pollSessions executions", async () => { test("skips overlapping pollSessions executions", async () => {
@@ -39,15 +40,15 @@ describe("TmuxPollingManager overlap", () => {
} }
const manager = new TmuxPollingManager( const manager = new TmuxPollingManager(
testCoerce<import("../../tools/delegate-task/types").OpencodeClient>(client), unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
sessions, sessions,
async () => {}, async () => {},
) )
//#when //#when
const firstPoll = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions() const firstPoll = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions()
await Promise.resolve() await Promise.resolve()
const secondPoll = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions() const secondPoll = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions()
releaseStatus?.() releaseStatus?.()
await Promise.all([firstPoll, secondPoll]) await Promise.all([firstPoll, secondPoll])
@@ -85,7 +86,7 @@ describe("TmuxPollingManager overlap", () => {
} }
const manager = new TmuxPollingManager( const manager = new TmuxPollingManager(
testCoerce<import("../../tools/delegate-task/types").OpencodeClient>(client), unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
sessions, sessions,
async (sessionId) => { async (sessionId) => {
closedSessionIds.push(sessionId) closedSessionIds.push(sessionId)
@@ -98,7 +99,7 @@ describe("TmuxPollingManager overlap", () => {
}) })
//#when //#when
const pollSessions = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions
await pollSessions.call(manager) await pollSessions.call(manager)
await pollSessions.call(manager) await pollSessions.call(manager)
await pollSessions.call(manager) await pollSessions.call(manager)
@@ -132,7 +133,7 @@ describe("TmuxPollingManager overlap", () => {
} }
const manager = new TmuxPollingManager( const manager = new TmuxPollingManager(
testCoerce<import("../../tools/delegate-task/types").OpencodeClient>(client), unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
sessions, sessions,
async (sessionId) => { async (sessionId) => {
closedSessionIds.push(sessionId) closedSessionIds.push(sessionId)
@@ -140,7 +141,7 @@ describe("TmuxPollingManager overlap", () => {
) )
// when // when
const pollSessions = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions
await pollSessions.call(manager) await pollSessions.call(manager)
// then // then
@@ -171,7 +172,7 @@ describe("TmuxPollingManager overlap", () => {
} }
const manager = new TmuxPollingManager( const manager = new TmuxPollingManager(
testCoerce<import("../../tools/delegate-task/types").OpencodeClient>(client), unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
sessions, sessions,
async (sessionId) => { async (sessionId) => {
closedSessionIds.push(sessionId) closedSessionIds.push(sessionId)
@@ -179,7 +180,7 @@ describe("TmuxPollingManager overlap", () => {
) )
// when // when
const pollSessions = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions
await pollSessions.call(manager) await pollSessions.call(manager)
// then // then
@@ -222,13 +223,13 @@ describe("TmuxPollingManager overlap", () => {
} }
manager = new TmuxPollingManager( manager = new TmuxPollingManager(
testCoerce<import("../../tools/delegate-task/types").OpencodeClient>(client), unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
sessions, sessions,
async (sessionId) => { async (sessionId) => {
closedSessionIds.push(sessionId) closedSessionIds.push(sessionId)
}, },
) )
const pollSessions = (testCoerce<{ pollSessions: () => Promise<void> }>(manager)).pollSessions const pollSessions = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions
// when // when
await pollSessions.call(manager) await pollSessions.call(manager)
@@ -5,6 +5,7 @@ import { executeCompact } from "./executor"
import type { AutoCompactState } from "./types" import type { AutoCompactState } from "./types"
import * as recoveryStrategy from "./recovery-strategy" import * as recoveryStrategy from "./recovery-strategy"
import * as messagesReader from "../session-recovery/storage/messages-reader" import * as messagesReader from "../session-recovery/storage/messages-reader"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type TimerCallback = (...args: any[]) => void type TimerCallback = (...args: any[]) => void
@@ -37,7 +38,7 @@ function createFakeTimeouts(): FakeTimeouts {
callback, callback,
args, args,
}) })
return testCoerce<ReturnType<typeof setTimeout>>(id) return unsafeTestValue<ReturnType<typeof setTimeout>>(id)
}) as typeof setTimeout }) as typeof setTimeout
globalThis.clearTimeout = ((id?: number) => { globalThis.clearTimeout = ((id?: number) => {
@@ -243,7 +244,7 @@ describe("executeCompact lock management", () => {
await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig) await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig)
// then: Toast should be shown // then: Toast should be shown
const toastCalls = (testCoerce(mockClient.tui.showToast)).mock.calls const toastCalls = (unsafeTestValue(mockClient.tui.showToast)).mock.calls
const blockedToast = toastCalls.find( const blockedToast = toastCalls.find(
(call: any) => call[0]?.body?.title === "Compact In Progress", (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) await executeCompact(sessionID, msg, autoCompactState, mockClient, directory, pluginConfig)
// then: Should show failure toast // then: Should show failure toast
const toastCalls = (testCoerce(mockClient.tui.showToast)).mock.calls const toastCalls = (unsafeTestValue(mockClient.tui.showToast)).mock.calls
const failureToast = toastCalls.find( const failureToast = toastCalls.find(
(call: any) => call[0]?.body?.title === "Auto Compact Failed", (call: any) => call[0]?.body?.title === "Auto Compact Failed",
) )
@@ -2,6 +2,7 @@ import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import type { ExperimentalConfig } from "../../config" import type { ExperimentalConfig } from "../../config"
import * as originalDeduplicationRecovery from "./deduplication-recovery" import * as originalDeduplicationRecovery from "./deduplication-recovery"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const attemptDeduplicationRecoveryMock = mock(async () => {}) const attemptDeduplicationRecoveryMock = mock(async () => {})
@@ -20,7 +21,7 @@ function createImmediateTimeouts(): () => void {
globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number, ...args: unknown[]) => { globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number, ...args: unknown[]) => {
callback(...args) callback(...args)
return testCoerce<ReturnType<typeof setTimeout>>(0) return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
}) as typeof setTimeout }) as typeof setTimeout
globalThis.clearTimeout = ((_: ReturnType<typeof setTimeout>) => {}) as typeof clearTimeout globalThis.clearTimeout = ((_: ReturnType<typeof setTimeout>) => {}) as typeof clearTimeout
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
import { runSummarizeRetryStrategy } from "./summarize-retry-strategy" import { runSummarizeRetryStrategy } from "./summarize-retry-strategy"
import type { AutoCompactState, ParsedTokenLimitError, RetryState } from "./types" import type { AutoCompactState, ParsedTokenLimitError, RetryState } from "./types"
import type { OhMyOpenCodeConfig } from "../../config" import type { OhMyOpenCodeConfig } from "../../config"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type TimeoutCall = { type TimeoutCall = {
handle: ReturnType<typeof setTimeout> handle: ReturnType<typeof setTimeout>
@@ -95,7 +96,7 @@ describe("runSummarizeRetryStrategy", () => {
//#given //#given
const timeoutCalls: TimeoutCall[] = [] const timeoutCalls: TimeoutCall[] = []
globalThis.setTimeout = ((_: (...args: unknown[]) => void, delay?: number) => { globalThis.setTimeout = ((_: (...args: unknown[]) => void, delay?: number) => {
const handle = testCoerce<ReturnType<typeof setTimeout>>(timeoutCalls.length + 1) const handle = unsafeTestValue<ReturnType<typeof setTimeout>>(timeoutCalls.length + 1)
timeoutCalls.push({ handle, delay: delay ?? 0 }) timeoutCalls.push({ handle, delay: delay ?? 0 })
return handle return handle
}) as typeof setTimeout }) as typeof setTimeout
@@ -132,7 +133,7 @@ describe("runSummarizeRetryStrategy", () => {
let scheduledCallback: (() => void) | undefined let scheduledCallback: (() => void) | undefined
globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number) => { globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _delay?: number) => {
scheduledCallback = () => callback() scheduledCallback = () => callback()
return testCoerce<ReturnType<typeof setTimeout>>(1) return unsafeTestValue<ReturnType<typeof setTimeout>>(1)
}) as typeof setTimeout }) as typeof setTimeout
autoCompactState.pendingCompact.add(sessionID) autoCompactState.pendingCompact.add(sessionID)
@@ -176,7 +177,7 @@ describe("runSummarizeRetryStrategy", () => {
autoCompactState.emptyContentAttemptBySession.set(sessionID, 3) autoCompactState.emptyContentAttemptBySession.set(sessionID, 3)
autoCompactState.retryTimerBySession.set( autoCompactState.retryTimerBySession.set(
sessionID, sessionID,
testCoerce<ReturnType<typeof setTimeout>>(1), unsafeTestValue<ReturnType<typeof setTimeout>>(1),
) )
//#when //#when
+19 -18
View File
@@ -7,6 +7,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { createAtlasHook } from "./atlas-hook" import { createAtlasHook } from "./atlas-hook"
import { clearBoulderState, writeBoulderState } from "../../features/boulder-state" import { clearBoulderState, writeBoulderState } from "../../features/boulder-state"
import { _resetForTesting, clearSessionAgent, registerAgentName, setSessionAgent } from "../../features/claude-code-session-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) // Force process isolation in CI runner (globalThis.setTimeout override conflicts with other atlas tests)
mock.module("../../shared/opencode-storage-detection", () => ({ mock.module("../../shared/opencode-storage-detection", () => ({
@@ -79,7 +80,7 @@ describe("atlas background task retry", () => {
callback: () => (callback as LongTimerCallback)(...args), callback: () => (callback as LongTimerCallback)(...args),
cleared: false, cleared: false,
}) })
return testCoerce<ReturnType<typeof setTimeout>>(id) return unsafeTestValue<ReturnType<typeof setTimeout>>(id)
} }
return originalSetTimeout(callback, delay, ...args) return originalSetTimeout(callback, delay, ...args)
@@ -120,7 +121,7 @@ describe("atlas background task retry", () => {
let backgroundRunning = true let backgroundRunning = true
const promptMock = mock(async () => ({})) const promptMock = mock(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -130,7 +131,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
@@ -161,7 +162,7 @@ describe("atlas background task retry", () => {
let backgroundRunning = true let backgroundRunning = true
const promptMock = mock(async () => ({})) const promptMock = mock(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -171,7 +172,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
@@ -204,7 +205,7 @@ describe("atlas background task retry", () => {
let remainingRunningRetries = 2 let remainingRunningRetries = 2
const promptMock = mock(async () => ({})) const promptMock = mock(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -214,7 +215,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => { getTasksByParentSession: () => {
@@ -258,7 +259,7 @@ describe("atlas background task retry", () => {
const promptAsyncMock = mock(async () => ({})) const promptAsyncMock = mock(async () => ({}))
let backgroundCheckCount = 0 let backgroundCheckCount = 0
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -268,7 +269,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => { getTasksByParentSession: () => {
@@ -313,7 +314,7 @@ describe("atlas background task retry", () => {
let backgroundRunning = true let backgroundRunning = true
const promptAsyncMock = mock(async () => ({})) const promptAsyncMock = mock(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -323,7 +324,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
@@ -366,7 +367,7 @@ describe("atlas background task retry", () => {
let backgroundRunning = true let backgroundRunning = true
let descendantAgent = "atlas" let descendantAgent = "atlas"
const promptAsyncMock = mock(async () => ({})) const promptAsyncMock = mock(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -386,7 +387,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: (currentSessionID: string) => { getTasksByParentSession: (currentSessionID: string) => {
@@ -424,7 +425,7 @@ describe("atlas background task retry", () => {
const deferredPrompt = createDeferred<{}>() const deferredPrompt = createDeferred<{}>()
const promptAsyncMock = mock(() => deferredPrompt.promise) const promptAsyncMock = mock(() => deferredPrompt.promise)
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -462,7 +463,7 @@ describe("atlas background task retry", () => {
promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise) promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise)
promptAsyncMock.mockImplementationOnce(async () => ({})) promptAsyncMock.mockImplementationOnce(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -472,7 +473,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => [], getTasksByParentSession: () => [],
@@ -515,7 +516,7 @@ describe("atlas background task retry", () => {
}) })
promptAsyncMock.mockImplementationOnce(async () => ({})) promptAsyncMock.mockImplementationOnce(async () => ({}))
const hook = createAtlasHook(testCoerce<PluginInput>({ const hook = createAtlasHook(unsafeTestValue<PluginInput>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -525,7 +526,7 @@ describe("atlas background task retry", () => {
}, },
}), { }), {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & { backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
getTasksByParentSession: (sessionID: string) => Array<{ status: string }> getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
}>({ }>({
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
@@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { registerAgentName, _resetForTesting } from "../../features/claude-code-session-state" import { registerAgentName, _resetForTesting } from "../../features/claude-code-session-state"
import { injectBoulderContinuation } from "./boulder-continuation-injector" import { injectBoulderContinuation } from "./boulder-continuation-injector"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("injectBoulderContinuation", () => { describe("injectBoulderContinuation", () => {
beforeEach(() => { beforeEach(() => {
@@ -20,7 +21,7 @@ describe("injectBoulderContinuation", () => {
const promptAsyncMock = mock(async (_request: unknown) => undefined) const promptAsyncMock = mock(async (_request: unknown) => undefined)
const messagesMock = mock(async () => ({ data: [] })) const messagesMock = mock(async () => ({ data: [] }))
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -60,7 +61,7 @@ describe("injectBoulderContinuation", () => {
const messagesMock = mock(async () => ({ data: [] })) const messagesMock = mock(async () => ({ data: [] }))
const sessionState = { promptFailureCount: 2, lastContinuationInjectedAt: 123 } const sessionState = { promptFailureCount: 2, lastContinuationInjectedAt: 123 }
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -78,7 +79,7 @@ describe("injectBoulderContinuation", () => {
remaining: 1, remaining: 1,
total: 2, total: 2,
agent: "atlas", agent: "atlas",
backgroundManager: testCoerce<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({ backgroundManager: unsafeTestValue<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({
getTasksByParentSession: () => [{ status: "running" }], getTasksByParentSession: () => [{ status: "running" }],
}), }),
sessionState, sessionState,
@@ -98,7 +99,7 @@ describe("injectBoulderContinuation", () => {
const messagesMock = mock(async () => ({ data: [] })) const messagesMock = mock(async () => ({ data: [] }))
const sessionState = { promptFailureCount: 1, lastContinuationInjectedAt: 456 } const sessionState = { promptFailureCount: 1, lastContinuationInjectedAt: 456 }
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -116,7 +117,7 @@ describe("injectBoulderContinuation", () => {
remaining: 1, remaining: 1,
total: 2, total: 2,
agent: "atlas", agent: "atlas",
backgroundManager: testCoerce<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({ backgroundManager: unsafeTestValue<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({
getTasksByParentSession: () => [{ status: "pending" }], getTasksByParentSession: () => [{ status: "pending" }],
}), }),
sessionState, sessionState,
@@ -134,7 +135,7 @@ describe("injectBoulderContinuation", () => {
const promptAsyncMock = mock(async (_request: unknown) => undefined) const promptAsyncMock = mock(async (_request: unknown) => undefined)
const messagesMock = mock(async () => ({ data: [] })) const messagesMock = mock(async () => ({ data: [] }))
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -189,7 +190,7 @@ describe("injectBoulderContinuation", () => {
}], }],
})) }))
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { join } from "node:path" import { join } from "node:path"
import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const { createAtlasHook } = await import("./index") const { createAtlasHook } = await import("./index")
@@ -49,7 +50,7 @@ describe("atlas hook idle-event complete boulder", () => {
}, },
}) })
const hook = createAtlasHook(testCoerce<Parameters<typeof createAtlasHook>[0]>({ const hook = createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDirectory, directory: testDirectory,
client: { client: {
session: { session: {
+2 -1
View File
@@ -7,6 +7,7 @@ import { join } from "node:path"
import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state"
import type { BoulderState } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state"
import { _resetForTesting, registerAgentName, setSessionAgent, subagentSessions } from "../../features/claude-code-session-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") const { createAtlasHook } = await import("./index")
@@ -32,7 +33,7 @@ describe("atlas hook idle-event session lineage", () => {
} }
function createHook(parentSessionIDs?: Record<string, string | undefined>) { function createHook(parentSessionIDs?: Record<string, string | undefined>) {
return createAtlasHook(testCoerce<Parameters<typeof createAtlasHook>[0]>({ return createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDirectory, directory: testDirectory,
client: { client: {
session: { session: {
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto"
import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state"
import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state" import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state"
import type { BoulderState } from "../../features/boulder-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_STORAGE_ROOT = join(tmpdir(), `atlas-persisted-lineage-storage-${randomUUID()}`)
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message")
@@ -58,7 +59,7 @@ describe("atlas hook idle-event persisted lineage", () => {
parentSessionIDs?: Record<string, string | undefined>, parentSessionIDs?: Record<string, string | undefined>,
messagesBySession?: Record<string, Array<{ info: { agent: string; providerID: string; modelID: string } }>>, messagesBySession?: Record<string, Array<{ info: { agent: string; providerID: string; modelID: string } }>>,
) { ) {
return createAtlasHook(testCoerce<Parameters<typeof createAtlasHook>[0]>({ return createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDirectory, directory: testDirectory,
client: { client: {
session: { session: {
@@ -173,7 +174,7 @@ describe("atlas hook idle-event persisted lineage", () => {
}, },
}) })
const hook = createAtlasHook(testCoerce<Parameters<typeof createAtlasHook>[0]>({ const hook = createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDirectory, directory: testDirectory,
client: { client: {
session: { session: {
+2 -1
View File
@@ -8,6 +8,7 @@ import { createBoulderState, readBoulderState, writeBoulderState } from "../../f
import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state" import { _resetForTesting, registerAgentName } from "../../features/claude-code-session-state"
import { handleAtlasSessionIdle } from "./idle-event" import { handleAtlasSessionIdle } from "./idle-event"
import type { SessionState } from "./types" import type { SessionState } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("handleAtlasSessionIdle completion nudge", () => { describe("handleAtlasSessionIdle completion nudge", () => {
const SESSION_ID = "session-main-1" const SESSION_ID = "session-main-1"
@@ -76,7 +77,7 @@ describe("handleAtlasSessionIdle completion nudge", () => {
return { data: {} } return { data: {} }
}) })
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
directory: testDirectory, directory: testDirectory,
client: { client: {
session: { session: {
@@ -1,11 +1,12 @@
import { describe, expect, mock, test } from "bun:test" import { describe, expect, mock, test } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { resolveRecentPromptContextForSession } from "./recent-model-resolver" import { resolveRecentPromptContextForSession } from "./recent-model-resolver"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("resolveRecentPromptContextForSession", () => { describe("resolveRecentPromptContextForSession", () => {
test("uses message time.created rather than SDK array order for recent prompt context", async () => { test("uses message time.created rather than SDK array order for recent prompt context", async () => {
// given // given
const ctx = testCoerce<PluginInput>({ const ctx = unsafeTestValue<PluginInput>({
client: { client: {
session: { session: {
messages: mock(async () => ({ messages: mock(async () => ({
@@ -8,6 +8,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { Project } from "@opencode-ai/sdk" import type { Project } from "@opencode-ai/sdk"
import { readBoulderState, writeBoulderState } from "../../features/boulder-state" import { readBoulderState, writeBoulderState } from "../../features/boulder-state"
import { createToolExecuteBeforeHandler } from "./tool-execute-before" import { createToolExecuteBeforeHandler } from "./tool-execute-before"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const isCallerOrchestratorMock = mock(async () => true) const isCallerOrchestratorMock = mock(async () => true)
const collectGitDiffStatsMock = mock(() => ({ const collectGitDiffStatsMock = mock(() => ({
@@ -80,7 +81,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
function createHandler(parentSessionIDs?: Record<string, string | undefined>) { function createHandler(parentSessionIDs?: Record<string, string | undefined>) {
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async (input: SessionGetInput) => createSessionGetResult(parentSessionIDs?.[input.path.id]), get: async (input: SessionGetInput) => createSessionGetResult(parentSessionIDs?.[input.path.id]),
}, },
@@ -141,7 +142,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
const childSessionID = "ses_child123" const childSessionID = "ses_child123"
const planPath = join(testDirectory, "background-launch-plan.md") const planPath = join(testDirectory, "background-launch-plan.md")
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async () => createSessionGetResult(undefined), get: async () => createSessionGetResult(undefined),
}, },
@@ -215,7 +216,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
const childSessionID = "ses_child_lookup_failure" const childSessionID = "ses_child_lookup_failure"
const planPath = join(testDirectory, "background-launch-plan.md") const planPath = join(testDirectory, "background-launch-plan.md")
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async () => createSessionGetResult(undefined), get: async () => createSessionGetResult(undefined),
}, },
@@ -288,7 +289,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
const childSessionID = "ses_outside_lineage" const childSessionID = "ses_outside_lineage"
const planPath = join(testDirectory, "background-launch-plan.md") const planPath = join(testDirectory, "background-launch-plan.md")
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async () => createSessionGetResult(undefined), get: async () => createSessionGetResult(undefined),
}, },
@@ -358,7 +359,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
const childSessionID = "ses_unrelated_child" const childSessionID = "ses_unrelated_child"
const planPath = join(testDirectory, "background-launch-plan.md") const planPath = join(testDirectory, "background-launch-plan.md")
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async () => createSessionGetResult(undefined), get: async () => createSessionGetResult(undefined),
}, },
@@ -431,7 +432,7 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
const planPathA = join(testDirectory, "background-launch-work-a.md") const planPathA = join(testDirectory, "background-launch-work-a.md")
const planPathB = join(testDirectory, "background-launch-work-b.md") const planPathB = join(testDirectory, "background-launch-work-b.md")
const project = createProject() const project = createProject()
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
get: async () => createSessionGetResult(undefined), get: async () => createSessionGetResult(undefined),
}, },
@@ -15,7 +15,7 @@ mock.module("../constants", () => ({
const current = mockState.candidates const current = mockState.candidates
// Forward array methods/properties to the mutable candidates list // Forward array methods/properties to the mutable candidates list
// so getCachedVersion's `for (... of ...)` sees fresh data per test. // so getCachedVersion's `for (... of ...)` sees fresh data per test.
const value = (testCoerce<Record<PropertyKey, unknown>>(current))[prop] const value = (unsafeTestValue<Record<PropertyKey, unknown>>(current))[prop]
if (typeof value === "function") { if (typeof value === "function") {
return (value as (...args: unknown[]) => unknown).bind(current) return (value as (...args: unknown[]) => unknown).bind(current)
} }
@@ -29,6 +29,7 @@ mock.module("./package-json-locator", () => ({
})) }))
import { getCachedVersion } from "./cached-version" import { getCachedVersion } from "./cached-version"
import { unsafeTestValue } from "../../../../test-support/unsafe-test-value"
describe("getCachedVersion (GH-3257)", () => { describe("getCachedVersion (GH-3257)", () => {
let cacheRoot: string let cacheRoot: string
@@ -3,6 +3,7 @@ import { createCategorySkillReminderHook } from "./index"
import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state" import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state"
import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder"
import * as sharedModule from "../../shared" import * as sharedModule from "../../shared"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("category-skill-reminder hook", () => { describe("category-skill-reminder hook", () => {
let logCalls: Array<{ msg: string; data?: unknown }> let logCalls: Array<{ msg: string; data?: unknown }>
@@ -21,7 +22,7 @@ describe("category-skill-reminder hook", () => {
}) })
function createMockPluginInput() { function createMockPluginInput() {
return testCoerce({ return unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async () => {}, showToast: async () => {},
@@ -3,6 +3,7 @@
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"
import type { HookHttp } from "./types" import type { HookHttp } from "./types"
import * as sharedModule from "../../shared" import * as sharedModule from "../../shared"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const mockFetch = mock(() => const mockFetch = mock(() =>
Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
@@ -31,7 +32,7 @@ describe("executeHttpHook TLS security", () => {
let logCalls: Array<{ message: string; data?: unknown }> let logCalls: Array<{ message: string; data?: unknown }>
beforeEach(() => { beforeEach(() => {
globalThis.fetch = testCoerce<typeof fetch>(mockFetch) globalThis.fetch = unsafeTestValue<typeof fetch>(mockFetch)
mockFetch.mockReset() mockFetch.mockReset()
mockFetch.mockImplementation(() => mockFetch.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
@@ -1,5 +1,6 @@
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"
import type { HookHttp } from "./types" import type { HookHttp } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const mockFetch = mock(() => const mockFetch = mock(() =>
Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
@@ -9,7 +10,7 @@ const originalFetch = globalThis.fetch
describe("executeHttpHook", () => { describe("executeHttpHook", () => {
beforeEach(() => { beforeEach(() => {
globalThis.fetch = testCoerce<typeof fetch>(mockFetch) globalThis.fetch = unsafeTestValue<typeof fetch>(mockFetch)
mockFetch.mockReset() mockFetch.mockReset()
mockFetch.mockImplementation(() => mockFetch.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify({}), { status: 200 })) Promise.resolve(new Response(JSON.stringify({}), { status: 200 }))
@@ -33,7 +34,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, stdinData) await executeHttpHook(hook, stdinData)
expect(mockFetch).toHaveBeenCalledTimes(1) expect(mockFetch).toHaveBeenCalledTimes(1)
const [url, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [url, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
expect(url).toBe("http://localhost:8080/hooks/pre-tool-use") expect(url).toBe("http://localhost:8080/hooks/pre-tool-use")
expect(options.method).toBe("POST") expect(options.method).toBe("POST")
expect(options.body).toBe(stdinData) expect(options.body).toBe(stdinData)
@@ -44,7 +45,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, stdinData) await executeHttpHook(hook, stdinData)
const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
const headers = options.headers as Record<string, string> const headers = options.headers as Record<string, string>
expect(headers["Content-Type"]).toBe("application/json") expect(headers["Content-Type"]).toBe("application/json")
}) })
@@ -72,7 +73,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, "{}") await executeHttpHook(hook, "{}")
const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
const headers = options.headers as Record<string, string> const headers = options.headers as Record<string, string>
expect(headers["Authorization"]).toBe("Bearer secret-123") expect(headers["Authorization"]).toBe("Bearer secret-123")
}) })
@@ -88,7 +89,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, "{}") await executeHttpHook(hook, "{}")
const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
const headers = options.headers as Record<string, string> const headers = options.headers as Record<string, string>
expect(headers["Authorization"]).toBe("Bearer secret-123") expect(headers["Authorization"]).toBe("Bearer secret-123")
}) })
@@ -104,7 +105,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, "{}") await executeHttpHook(hook, "{}")
const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
const headers = options.headers as Record<string, string> const headers = options.headers as Record<string, string>
expect(headers["Authorization"]).toBe("Bearer ") expect(headers["Authorization"]).toBe("Bearer ")
}) })
@@ -121,7 +122,7 @@ describe("executeHttpHook", () => {
await executeHttpHook(hook, "{}") await executeHttpHook(hook, "{}")
const [, options] = testCoerce<[string, RequestInit]>(mockFetch.mock.calls[0]) const [, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
expect(options.signal).toBeDefined() expect(options.signal).toBeDefined()
}) })
}) })
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("tool-input-cache", () => { describe("tool-input-cache", () => {
const originalSetInterval = globalThis.setInterval 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 () => { test("#given cleanup timer started #when stop cleanup runs #then interval is cleared and cache is emptied", async () => {
//#given //#given
const intervalHandle = testCoerce<ReturnType<typeof setInterval>>({ unref: mock(() => {}) }) const intervalHandle = unsafeTestValue<ReturnType<typeof setInterval>>({ unref: mock(() => {}) })
const setIntervalMock = mock(() => intervalHandle) const setIntervalMock = mock(() => intervalHandle)
const clearIntervalMock = mock(() => {}) const clearIntervalMock = mock(() => {})
globalThis.setInterval = testCoerce<typeof setInterval>(setIntervalMock) globalThis.setInterval = unsafeTestValue<typeof setInterval>(setIntervalMock)
globalThis.clearInterval = testCoerce<typeof clearInterval>(clearIntervalMock) globalThis.clearInterval = unsafeTestValue<typeof clearInterval>(clearIntervalMock)
const modulePath = new URL("./tool-input-cache.ts", import.meta.url).pathname const modulePath = new URL("./tool-input-cache.ts", import.meta.url).pathname
const cacheModule = await import(`${modulePath}?stop-clear`) const cacheModule = await import(`${modulePath}?stop-clear`)
+3 -2
View File
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os"
import { processWithCli } from "./cli-runner" import { processWithCli } from "./cli-runner"
import type { PendingCall } from "./types" import type { PendingCall } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createMockInput() { function createMockInput() {
return { return {
@@ -74,7 +75,7 @@ done
const originalSetTimeout = globalThis.setTimeout const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
fn() fn()
return testCoerce<ReturnType<typeof setTimeout>>(0) return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
}) as typeof setTimeout }) as typeof setTimeout
try { try {
@@ -102,7 +103,7 @@ done
const originalSetTimeout = globalThis.setTimeout const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
fn() fn()
return testCoerce<ReturnType<typeof setTimeout>>(0) return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
}) as typeof setTimeout }) as typeof setTimeout
try { try {
@@ -1,4 +1,5 @@
import { describe, test, expect } from "bun:test" import { describe, test, expect } from "bun:test"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("pending-calls cleanup interval", () => { describe("pending-calls cleanup interval", () => {
test("starts cleanup once and unrefs timer", async () => { test("starts cleanup once and unrefs timer", async () => {
@@ -7,13 +8,13 @@ describe("pending-calls cleanup interval", () => {
const setIntervalCalls: number[] = [] const setIntervalCalls: number[] = []
let unrefCalled = 0 let unrefCalled = 0
globalThis.setInterval = testCoerce<typeof setInterval>((( globalThis.setInterval = unsafeTestValue<typeof setInterval>(((
_handler: TimerHandler, _handler: TimerHandler,
timeout?: number, timeout?: number,
..._args: unknown[] ..._args: unknown[]
) => { ) => {
setIntervalCalls.push(timeout as number) setIntervalCalls.push(timeout as number)
return testCoerce<ReturnType<typeof setInterval>>({ return unsafeTestValue<ReturnType<typeof setInterval>>({
unref: () => { unref: () => {
unrefCalled += 1 unrefCalled += 1
}, },
@@ -43,16 +44,16 @@ describe("pending-calls cleanup interval", () => {
let intervalHandle: ReturnType<typeof setInterval> | undefined let intervalHandle: ReturnType<typeof setInterval> | undefined
let clearCalls = 0 let clearCalls = 0
globalThis.setInterval = testCoerce<typeof setInterval>((( globalThis.setInterval = unsafeTestValue<typeof setInterval>(((
_handler: TimerHandler, _handler: TimerHandler,
_timeout?: number, _timeout?: number,
..._args: unknown[] ..._args: unknown[]
) => { ) => {
intervalHandle = testCoerce<ReturnType<typeof setInterval>>({ unref: () => {} }) intervalHandle = unsafeTestValue<ReturnType<typeof setInterval>>({ unref: () => {} })
return intervalHandle return intervalHandle
})) }))
globalThis.clearInterval = testCoerce<typeof clearInterval>(((handle?: ReturnType<typeof setInterval>) => { globalThis.clearInterval = unsafeTestValue<typeof clearInterval>(((handle?: ReturnType<typeof setInterval>) => {
if (handle === intervalHandle) { if (handle === intervalHandle) {
clearCalls += 1 clearCalls += 1
} }
+3 -2
View File
@@ -1,11 +1,12 @@
import { describe, it, expect, beforeEach } from "bun:test" import { describe, it, expect, beforeEach } from "bun:test"
import { createEditErrorRecoveryHook, EDIT_ERROR_REMINDER, EDIT_ERROR_PATTERNS } from "./index" import { createEditErrorRecoveryHook, EDIT_ERROR_REMINDER, EDIT_ERROR_PATTERNS } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("createEditErrorRecoveryHook", () => { describe("createEditErrorRecoveryHook", () => {
let hook: ReturnType<typeof createEditErrorRecoveryHook> let hook: ReturnType<typeof createEditErrorRecoveryHook>
beforeEach(() => { beforeEach(() => {
hook = createEditErrorRecoveryHook(testCoerce({})) hook = createEditErrorRecoveryHook(unsafeTestValue({}))
}) })
describe("tool.execute.after", () => { describe("tool.execute.after", () => {
@@ -108,7 +109,7 @@ describe("createEditErrorRecoveryHook", () => {
const input = createInput("Edit") const input = createInput("Edit")
const output = { const output = {
title: "Edit", title: "Edit",
output: testCoerce<string>(undefined), output: unsafeTestValue<string>(undefined),
metadata: {}, metadata: {},
} }
@@ -1,6 +1,7 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test" import { describe, expect, test, beforeEach, afterEach } from "bun:test"
import { createKeywordDetectorHook } from "./index" import { createKeywordDetectorHook } from "./index"
import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type StartLoopCall = { type StartLoopCall = {
sessionID: string sessionID: string
@@ -11,7 +12,7 @@ type StartLoopCall = {
type CancelLoopCall = { sessionID: string } type CancelLoopCall = { sessionID: string }
function createMockPluginInput() { function createMockPluginInput() {
return testCoerce({ return unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async () => {}, showToast: async () => {},
@@ -4,6 +4,7 @@ import { createKeywordDetectorHook } from "./index"
import { setMainSession, _resetForTesting } from "../../features/claude-code-session-state" import { setMainSession, _resetForTesting } from "../../features/claude-code-session-state"
import * as sharedModule from "../../shared" import * as sharedModule from "../../shared"
import * as sessionState from "../../features/claude-code-session-state" import * as sessionState from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("keyword-detector hyperplan-ultrawork combo", () => { describe("keyword-detector hyperplan-ultrawork combo", () => {
let logSpy: ReturnType<typeof spyOn> let logSpy: ReturnType<typeof spyOn>
@@ -22,7 +23,7 @@ describe("keyword-detector hyperplan-ultrawork combo", () => {
function createMockPluginInput(options: { toastCalls?: string[] } = {}) { function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
const toastCalls = options.toastCalls ?? [] const toastCalls = options.toastCalls ?? []
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { client: {
tui: { tui: {
showToast: async (opts: { body: { title: string } }) => { showToast: async (opts: { body: { title: string } }) => {
+3 -2
View File
@@ -7,6 +7,7 @@ import { setMainSession, updateSessionAgent, clearSessionAgent, _resetForTesting
import { ContextCollector } from "../../features/context-injector" import { ContextCollector } from "../../features/context-injector"
import * as sharedModule from "../../shared" import * as sharedModule from "../../shared"
import * as sessionState from "../../features/claude-code-session-state" import * as sessionState from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type ToastOptions = { body: { title: string } } type ToastOptions = { body: { title: string } }
@@ -881,7 +882,7 @@ describe("keyword-detector team mode", () => {
}) })
function createMockPluginInput() { function createMockPluginInput() {
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { client: {
tui: { tui: {
showToast: async () => {}, showToast: async () => {},
@@ -1063,7 +1064,7 @@ describe("keyword-detector disabled_keywords config", () => {
function createMockPluginInput(options: { toastCalls?: string[] } = {}) { function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
const toastCalls = options.toastCalls ?? [] const toastCalls = options.toastCalls ?? []
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { client: {
tui: { tui: {
showToast: async (opts: { body: { title: string } }) => { showToast: async (opts: { body: { title: string } }) => {
@@ -3,6 +3,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { createKeywordDetectorHook } from "./index" import { createKeywordDetectorHook } from "./index"
import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type StartLoopCall = { type StartLoopCall = {
sessionID: string sessionID: string
@@ -11,7 +12,7 @@ type StartLoopCall = {
} }
function createMockPluginInput(toastCalls: string[] = []) { function createMockPluginInput(toastCalls: string[] = []) {
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { client: {
tui: { tui: {
showToast: async (opts: { body: { title: string } }) => { showToast: async (opts: { body: { title: string } }) => {
@@ -1,9 +1,10 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { createKeywordDetectorHook } from "./index" import { createKeywordDetectorHook } from "./index"
import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createMockPluginInput(toastMessages: string[]) { function createMockPluginInput(toastMessages: string[]) {
return testCoerce({ return unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async (opts: { body: { message: string } }) => { showToast: async (opts: { body: { message: string } }) => {
+9 -8
View File
@@ -1,3 +1,4 @@
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
declare const require: (name: string) => any declare const require: (name: string) => any
const { beforeEach, describe, expect, mock, test, afterAll } = require("bun:test") const { beforeEach, describe, expect, mock, test, afterAll } = require("bun:test")
@@ -86,7 +87,7 @@ describe("model fallback hook", () => {
}) })
test("applies pending fallback on chat.message by overriding model", async () => { test("applies pending fallback on chat.message by overriding model", async () => {
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -122,7 +123,7 @@ describe("model fallback hook", () => {
}) })
test("preserves fallback progression across repeated session.error retries", async () => { test("preserves fallback progression across repeated session.error retries", async () => {
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -212,7 +213,7 @@ describe("model fallback hook", () => {
const sessionID = "ses_model_fallback_noop_skip" const sessionID = "ses_model_fallback_noop_skip"
clearPendingModelFallback(modelFallback, sessionID) clearPendingModelFallback(modelFallback, sessionID)
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -254,7 +255,7 @@ describe("model fallback hook", () => {
const sessionID = "ses_model_fallback_noop_variant_skip" const sessionID = "ses_model_fallback_noop_variant_skip"
clearPendingModelFallback(modelFallback, sessionID) clearPendingModelFallback(modelFallback, sessionID)
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -299,7 +300,7 @@ describe("model fallback hook", () => {
clearPendingModelFallback(modelFallback, sessionID) clearPendingModelFallback(modelFallback, sessionID)
readConnectedProvidersCacheMock.mockReturnValue(["provider-x"]) readConnectedProvidersCacheMock.mockReturnValue(["provider-x"])
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -355,7 +356,7 @@ describe("model fallback hook", () => {
test("shows toast when fallback is applied", async () => { test("shows toast when fallback is applied", async () => {
const toastCalls: Array<{ title: string; message: string }> = [] const toastCalls: Array<{ title: string; message: string }> = []
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -393,7 +394,7 @@ describe("model fallback hook", () => {
const sessionID = "ses_model_fallback_ghcp" const sessionID = "ses_model_fallback_ghcp"
clearPendingModelFallback(modelFallback, sessionID) clearPendingModelFallback(modelFallback, sessionID)
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -434,7 +435,7 @@ describe("model fallback hook", () => {
const sessionID = "ses_model_fallback_google" const sessionID = "ses_model_fallback_google"
clearPendingModelFallback(modelFallback, sessionID) clearPendingModelFallback(modelFallback, sessionID)
const hook = testCoerce<{ const hook = unsafeTestValue<{
"chat.message"?: ( "chat.message"?: (
input: { sessionID: string }, input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> }, output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
@@ -4,6 +4,7 @@ import { describe, expect, spyOn, test } from "bun:test"
import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state" import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state"
import { getAgentDisplayName } from "../../shared/agent-display-names" import { getAgentDisplayName } from "../../shared/agent-display-names"
import { createNoHephaestusNonGptHook } from "./index" import { createNoHephaestusNonGptHook } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus") const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus")
const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus") const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus")
@@ -19,7 +20,7 @@ describe("no-hephaestus-non-gpt hook", () => {
test("shows toast on every chat.message when hephaestus uses non-gpt model", async () => { test("shows toast on every chat.message when hephaestus uses non-gpt model", async () => {
// given - hephaestus with claude model // given - hephaestus with claude model
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook(testCoerce({ const hook = createNoHephaestusNonGptHook(unsafeTestValue({
client: { tui: { showToast } }, client: { tui: { showToast } },
})) }))
@@ -54,7 +55,7 @@ describe("no-hephaestus-non-gpt hook", () => {
test("shows warning and does not switch agent when allow_non_gpt_model is enabled", async () => { 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 // given - hephaestus with claude model and opt-out enabled
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook(testCoerce({ const hook = createNoHephaestusNonGptHook(unsafeTestValue({
client: { tui: { showToast } }, client: { tui: { showToast } },
}), { }), {
allowNonGptModel: true, allowNonGptModel: true,
@@ -83,7 +84,7 @@ describe("no-hephaestus-non-gpt hook", () => {
test("does not show toast when hephaestus uses gpt model", async () => { test("does not show toast when hephaestus uses gpt model", async () => {
// given - hephaestus with gpt model // given - hephaestus with gpt model
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook(testCoerce({ const hook = createNoHephaestusNonGptHook(unsafeTestValue({
client: { tui: { showToast } }, client: { tui: { showToast } },
})) }))
@@ -104,7 +105,7 @@ describe("no-hephaestus-non-gpt hook", () => {
test("does not show toast for non-hephaestus agent", async () => { test("does not show toast for non-hephaestus agent", async () => {
// given - sisyphus with claude model (non-gpt) // given - sisyphus with claude model (non-gpt)
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook(testCoerce({ const hook = createNoHephaestusNonGptHook(unsafeTestValue({
client: { tui: { showToast } }, client: { tui: { showToast } },
})) }))
@@ -127,7 +128,7 @@ describe("no-hephaestus-non-gpt hook", () => {
_resetForTesting() _resetForTesting()
updateSessionAgent("ses_4", HEPHAESTUS_DISPLAY) updateSessionAgent("ses_4", HEPHAESTUS_DISPLAY)
const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn") const showToast = spyOn({ fn: async (_input: unknown) => ({}) }, "fn")
const hook = createNoHephaestusNonGptHook(testCoerce({ const hook = createNoHephaestusNonGptHook(unsafeTestValue({
client: { tui: { showToast } }, client: { tui: { showToast } },
})) }))
+2 -1
View File
@@ -5,6 +5,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state" import { _resetForTesting, updateSessionAgent } from "../../features/claude-code-session-state"
import { getAgentDisplayName } from "../../shared/agent-display-names" import { getAgentDisplayName } from "../../shared/agent-display-names"
import { createNoSisyphusGptHook } from "./index" import { createNoSisyphusGptHook } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus") const SISYPHUS_DISPLAY = getAgentDisplayName("sisyphus")
const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus") const HEPHAESTUS_DISPLAY = getAgentDisplayName("hephaestus")
@@ -22,7 +23,7 @@ function createOutput(): HookOutput {
} }
function createHookContext(showToast: (input: unknown) => Promise<unknown>): PluginInput { function createHookContext(showToast: (input: unknown) => Promise<unknown>): PluginInput {
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { tui: { showToast } }, client: { tui: { showToast } },
}) })
} }
@@ -1,3 +1,4 @@
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
import { describe, it, expect } from "bun:test"; import { describe, it, expect } from "bun:test";
import { createQuestionLabelTruncatorHook } from "./index"; import { createQuestionLabelTruncatorHook } from "./index";
@@ -23,10 +24,10 @@ describe("createQuestionLabelTruncatorHook", () => {
}; };
// when // when
await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
// then // then
const truncatedLabel = (testCoerce(output.args)).questions[0].options[0].label; const truncatedLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
expect(truncatedLabel.length).toBeLessThanOrEqual(30); expect(truncatedLabel.length).toBeLessThanOrEqual(30);
expect(truncatedLabel).toBe("This is a very long label t..."); expect(truncatedLabel).toBe("This is a very long label t...");
expect(truncatedLabel.endsWith("...")).toBe(true); expect(truncatedLabel.endsWith("...")).toBe(true);
@@ -50,10 +51,10 @@ describe("createQuestionLabelTruncatorHook", () => {
}; };
// when // when
await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
// then // then
const resultLabel = (testCoerce(output.args)).questions[0].options[0].label; const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
expect(resultLabel).toBe(shortLabel); expect(resultLabel).toBe(shortLabel);
}); });
@@ -74,10 +75,10 @@ describe("createQuestionLabelTruncatorHook", () => {
}; };
// when // when
await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
// then // then
const resultLabel = (testCoerce(output.args)).questions[0].options[0].label; const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
expect(resultLabel).toBe(exactLabel); expect(resultLabel).toBe(exactLabel);
}); });
@@ -90,7 +91,7 @@ describe("createQuestionLabelTruncatorHook", () => {
const originalArgs = { ...output.args }; const originalArgs = { ...output.args };
// when // when
await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
// then // then
expect(output.args).toEqual(originalArgs); expect(output.args).toEqual(originalArgs);
@@ -120,11 +121,11 @@ describe("createQuestionLabelTruncatorHook", () => {
}; };
// when // when
await hook["tool.execute.before"]?.(testCoerce(input), testCoerce(output)); await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
// then // then
const q1opts = (testCoerce(output.args)).questions[0].options; const q1opts = (unsafeTestValue(output.args)).questions[0].options;
const q2opts = (testCoerce(output.args)).questions[1].options; const q2opts = (unsafeTestValue(output.args)).questions[1].options;
expect(q1opts[0].label).toBe("Very long label number one ..."); expect(q1opts[0].label).toBe("Very long label number one ...");
expect(q1opts[0].label.length).toBeLessThanOrEqual(30); expect(q1opts[0].label.length).toBeLessThanOrEqual(30);
@@ -1,5 +1,6 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
export type SessionMessage = { export type SessionMessage = {
info?: { role?: string } info?: { role?: string }
@@ -16,7 +17,7 @@ export function createPluginInput(messages: SessionMessage[]): PluginInput {
$: {} as PluginInput["$"], $: {} as PluginInput["$"],
} as PluginInput } as PluginInput
const messagesFunction = testCoerce<PluginInput["client"]["session"]["messages"]>(async () => ({ data: messages })) const messagesFunction = unsafeTestValue<PluginInput["client"]["session"]["messages"]>(async () => ({ data: messages }))
pluginInput.client.session.messages = messagesFunction pluginInput.client.session.messages = messagesFunction
return pluginInput return pluginInput
@@ -1,6 +1,7 @@
/// <reference types="bun-types" /> /// <reference types="bun-types" />
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { createRalphLoopHook } from "./index" import { createRalphLoopHook } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createDeferred(): { function createDeferred(): {
promise: Promise<void> promise: Promise<void>
@@ -44,7 +45,7 @@ describe("ralph-loop reset strategy race condition", () => {
const selectSessionDeferred = createDeferred() const selectSessionDeferred = createDeferred()
const hook = createRalphLoopHook( const hook = createRalphLoopHook(
testCoerce<Parameters<typeof createRalphLoopHook>[0]>({ unsafeTestValue<Parameters<typeof createRalphLoopHook>[0]>({
directory: process.cwd(), directory: process.cwd(),
client: { client: {
session: { session: {
@@ -5,6 +5,7 @@ import { join } from "node:path"
import { createRalphLoopHook } from "./index" import { createRalphLoopHook } from "./index"
import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants" import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants"
import { clearState, writeState } from "./storage" import { clearState, writeState } from "./storage"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("ulw-loop verification", () => { describe("ulw-loop verification", () => {
const testDir = join(tmpdir(), `ulw-loop-verification-${Date.now()}`) const testDir = join(tmpdir(), `ulw-loop-verification-${Date.now()}`)
@@ -15,7 +16,7 @@ describe("ulw-loop verification", () => {
let oracleTranscriptPath: string let oracleTranscriptPath: string
function createMockPluginInput() { function createMockPluginInput() {
return testCoerce<Parameters<typeof createRalphLoopHook>[0]>({ return unsafeTestValue<Parameters<typeof createRalphLoopHook>[0]>({
client: { client: {
session: { session: {
promptAsync: async (opts: { path: { id: string }; body: { parts: Array<{ type: string; text: string }> } }) => { promptAsync: async (opts: { path: { id: string }; body: { parts: Array<{ type: string; text: string }> } }) => {
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
import { getFallbackModelsForSession } from "./fallback-models" import { getFallbackModelsForSession } from "./fallback-models"
import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("runtime-fallback fallback-models", () => { describe("runtime-fallback fallback-models", () => {
afterEach(() => { afterEach(() => {
@@ -12,7 +13,7 @@ describe("runtime-fallback fallback-models", () => {
//#given //#given
const sessionID = "ses_runtime_fallback_category" const sessionID = "ses_runtime_fallback_category"
SessionCategoryRegistry.register(sessionID, "quick") SessionCategoryRegistry.register(sessionID, "quick")
const pluginConfig = testCoerce({ const pluginConfig = unsafeTestValue({
categories: { categories: {
quick: { quick: {
fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"],
@@ -29,7 +30,7 @@ describe("runtime-fallback fallback-models", () => {
test("uses agent-specific fallback_models when agent is resolved", () => { test("uses agent-specific fallback_models when agent is resolved", () => {
//#given //#given
const pluginConfig = testCoerce({ const pluginConfig = unsafeTestValue({
agents: { agents: {
oracle: { oracle: {
fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"], fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"],
@@ -46,7 +47,7 @@ describe("runtime-fallback fallback-models", () => {
test("does not fall back to another agent chain when agent cannot be resolved", () => { test("does not fall back to another agent chain when agent cannot be resolved", () => {
//#given //#given
const pluginConfig = testCoerce({ const pluginConfig = unsafeTestValue({
agents: { agents: {
sisyphus: { sisyphus: {
fallback_models: ["quotio/gpt-5.2", "quotio/glm-5", "quotio/kimi-k2.5"], fallback_models: ["quotio/gpt-5.2", "quotio/glm-5", "quotio/kimi-k2.5"],
+2 -1
View File
@@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"
import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config" import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config"
import * as loggerModule from "../../shared/logger" import * as loggerModule from "../../shared/logger"
import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type RuntimeFallbackModule = typeof import("./hook") type RuntimeFallbackModule = typeof import("./hook")
@@ -41,7 +42,7 @@ describe("runtime-fallback", () => {
abort?: (args: unknown) => Promise<unknown> abort?: (args: unknown) => Promise<unknown>
} }
}) { }) {
return testCoerce({ return unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async (opts: { body: { title: string; message: string; variant: string; duration: number } }) => { showToast: async (opts: { body: { title: string; message: string; variant: string; duration: number } }) => {
+13 -12
View File
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, jest, spyOn, test } from "bun:
import * as sender from "./session-notification-sender" import * as sender from "./session-notification-sender"
import * as utils from "./session-notification-utils" import * as utils from "./session-notification-utils"
import type { PluginInput } from "@opencode-ai/plugin" 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", () => { describe("#when calling ctx.$ for notifications", () => {
test("#then should call .quiet() on all shell commands to suppress stdout/stderr", async () => { test("#then should call .quiet() on all shell commands to suppress stdout/stderr", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -107,7 +108,7 @@ describe("session-notification-sender", () => {
spyOn(utils, "getTerminalNotifierPath").mockResolvedValue(null) spyOn(utils, "getTerminalNotifierPath").mockResolvedValue(null)
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -142,7 +143,7 @@ describe("session-notification-sender", () => {
spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux")
const calls: string[] = [] const calls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }), $: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
}) })
@@ -157,7 +158,7 @@ describe("session-notification-sender", () => {
test("#then should fall back to terminal-notifier when cmux fails", async () => { test("#then should fall back to terminal-notifier when cmux fails", async () => {
spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux")
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")), $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")),
}) })
@@ -180,7 +181,7 @@ describe("session-notification-sender", () => {
spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux") spyOn(utils, "getCmuxPath").mockResolvedValue("/usr/local/bin/cmux")
const trackingCalls: string[] = [] const trackingCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")), $: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")),
}) })
@@ -200,7 +201,7 @@ describe("session-notification-sender", () => {
test("#then should skip cmux when not available and use terminal-notifier", async () => { test("#then should skip cmux when not available and use terminal-notifier", async () => {
const calls: string[] = [] const calls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }), $: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
}) })
@@ -213,7 +214,7 @@ describe("session-notification-sender", () => {
test("#then should call .quiet() on linux notify-send", async () => { test("#then should call .quiet() on linux notify-send", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -246,7 +247,7 @@ describe("session-notification-sender", () => {
test("#then should call .quiet() on win32 powershell", async () => { test("#then should call .quiet() on win32 powershell", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -283,7 +284,7 @@ describe("session-notification-sender", () => {
describe("#when calling ctx.$ for sound playback", () => { describe("#when calling ctx.$ for sound playback", () => {
test("#then should call .quiet() on darwin afplay", async () => { test("#then should call .quiet() on darwin afplay", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -316,7 +317,7 @@ describe("session-notification-sender", () => {
test("#then should call .quiet() on linux paplay", async () => { test("#then should call .quiet() on linux paplay", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -351,7 +352,7 @@ describe("session-notification-sender", () => {
spyOn(utils, "getPaplayPath").mockResolvedValue(null) spyOn(utils, "getPaplayPath").mockResolvedValue(null)
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -384,7 +385,7 @@ describe("session-notification-sender", () => {
test("#then should call .quiet() on win32 powershell sound", async () => { test("#then should call .quiet() on win32 powershell sound", async () => {
const quietCalls: string[] = [] const quietCalls: string[] = []
const mockCtx = testCoerce<PluginInput>({ const mockCtx = unsafeTestValue<PluginInput>({
$: (cmd: TemplateStringsArray, ...values: unknown[]) => { $: (cmd: TemplateStringsArray, ...values: unknown[]) => {
const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "") const cmdStr = cmd.reduce((acc, part, i) => acc + part + (values[i] ?? ""), "")
const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 } const result = { stdout: Buffer.from(""), stderr: Buffer.from(""), exitCode: 0 }
@@ -1,4 +1,5 @@
import { describe, expect, it } from "bun:test" import { describe, expect, it } from "bun:test"
import { unsafeTestValue } from "../../../../test-support/unsafe-test-value"
async function importFreshReaders() { async function importFreshReaders() {
const token = `${Date.now()}-${Math.random()}` const token = `${Date.now()}-${Math.random()}`
const [{ readMessagesFromSDK, readMessages }, { readPartsFromSDK, readParts }] = await Promise.all([ const [{ readMessagesFromSDK, readMessages }, { readPartsFromSDK, readParts }] = await Promise.all([
@@ -13,7 +14,7 @@ function createMockClient(handlers: {
messages?: (sessionID: string) => unknown[] messages?: (sessionID: string) => unknown[]
message?: (sessionID: string, messageID: string) => unknown message?: (sessionID: string, messageID: string) => unknown
}) { }) {
return testCoerce({ return unsafeTestValue({
session: { session: {
messages: async (opts: { path: { id: string } }) => { messages: async (opts: { path: { id: string } }) => {
if (handlers.messages) { if (handlers.messages) {
+7 -6
View File
@@ -16,6 +16,7 @@ import {
import type { BoulderState } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state"
import * as sessionState from "../../features/claude-code-session-state" import * as sessionState from "../../features/claude-code-session-state"
import * as worktreeDetector from "./worktree-detector" import * as worktreeDetector from "./worktree-detector"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("start-work hook", () => { describe("start-work hook", () => {
let testDir: string let testDir: string
@@ -738,7 +739,7 @@ You are starting a Sisyphus work session.
const promptAsyncMock = spyOn({ const promptAsyncMock = spyOn({
promptAsync: async (_request: unknown) => undefined, promptAsync: async (_request: unknown) => undefined,
}, "promptAsync") }, "promptAsync")
const ctx = testCoerce<Parameters<typeof createAtlasHook>[0]>({ const ctx = unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -784,18 +785,18 @@ You are starting a Sisyphus work session.
promptAsync: async (_request: unknown) => undefined, promptAsync: async (_request: unknown) => undefined,
}, "promptAsync") }, "promptAsync")
globalThis.setTimeout = testCoerce<typeof setTimeout>(((callback: Function, delay?: number, ...args: unknown[]) => { globalThis.setTimeout = unsafeTestValue<typeof setTimeout>(((callback: Function, delay?: number, ...args: unknown[]) => {
const normalized = typeof delay === "number" ? delay : 0 const normalized = typeof delay === "number" ? delay : 0
if (normalized >= 5000) { if (normalized >= 5000) {
const id = nextTimerId++ const id = nextTimerId++
capturedTimers.set(id, { callback: () => callback(...args), cleared: false }) capturedTimers.set(id, { callback: () => callback(...args), cleared: false })
return testCoerce<ReturnType<typeof setTimeout>>(id) return unsafeTestValue<ReturnType<typeof setTimeout>>(id)
} }
return originalSetTimeout(callback as Parameters<typeof originalSetTimeout>[0], delay) return originalSetTimeout(callback as Parameters<typeof originalSetTimeout>[0], delay)
})) }))
globalThis.clearTimeout = testCoerce<typeof clearTimeout>(((id?: number | ReturnType<typeof setTimeout>) => { globalThis.clearTimeout = unsafeTestValue<typeof clearTimeout>(((id?: number | ReturnType<typeof setTimeout>) => {
if (typeof id === "number" && capturedTimers.has(id)) { if (typeof id === "number" && capturedTimers.has(id)) {
capturedTimers.get(id)!.cleared = true capturedTimers.get(id)!.cleared = true
capturedTimers.delete(id) capturedTimers.delete(id)
@@ -807,7 +808,7 @@ You are starting a Sisyphus work session.
Date.now = () => fakeNow Date.now = () => fakeNow
const ctx = testCoerce<Parameters<typeof createAtlasHook>[0]>({ const ctx = unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -820,7 +821,7 @@ You are starting a Sisyphus work session.
const startWorkHook = createStartWorkHook(ctx) const startWorkHook = createStartWorkHook(ctx)
const atlasHook = createAtlasHook(ctx, { const atlasHook = createAtlasHook(ctx, {
directory: testDir, directory: testDir,
backgroundManager: testCoerce<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"]>({ backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"]>({
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [], getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
}), }),
}) })
@@ -6,6 +6,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundManager, BackgroundTask } from "../../features/background-agent" import type { BackgroundManager, BackgroundTask } from "../../features/background-agent"
import { readContinuationMarker } from "../../features/run-continuation-state" import { readContinuationMarker } from "../../features/run-continuation-state"
import { createStopContinuationGuardHook } from "./index" import { createStopContinuationGuardHook } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type CancelCall = { type CancelCall = {
taskId: string taskId: string
@@ -31,7 +32,7 @@ describe("stop-continuation-guard", () => {
}) })
function createMockPluginInput() { function createMockPluginInput() {
return testCoerce<PluginInput>({ return unsafeTestValue<PluginInput>({
client: { client: {
tui: { tui: {
showToast: async () => ({}), showToast: async () => ({}),
+2 -1
View File
@@ -2,6 +2,7 @@
import { describe, it, expect } from "bun:test" import { describe, it, expect } from "bun:test"
import { createTaskResumeInfoHook } from "./index" import { createTaskResumeInfoHook } from "./index"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("createTaskResumeInfoHook", () => { describe("createTaskResumeInfoHook", () => {
const hook = createTaskResumeInfoHook() const hook = createTaskResumeInfoHook()
@@ -19,7 +20,7 @@ describe("createTaskResumeInfoHook", () => {
const input = createInput("task") const input = createInput("task")
const output = { const output = {
title: "delegate_task", title: "delegate_task",
output: testCoerce<string>(undefined), output: unsafeTestValue<string>(undefined),
metadata: {}, metadata: {},
} }
+5 -4
View File
@@ -2,10 +2,11 @@ import { describe, expect, test } from "bun:test"
import { resolveGateway, validateGatewayUrl, normalizeReplyListenerConfig } from "../config" import { resolveGateway, validateGatewayUrl, normalizeReplyListenerConfig } from "../config"
import type { OpenClawConfig } from "../types" import type { OpenClawConfig } from "../types"
import { OpenClawConfigSchema } from "../../config/schema/openclaw" import { OpenClawConfigSchema } from "../../config/schema/openclaw"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("OpenClaw Config", () => { describe("OpenClaw Config", () => {
test("resolveGateway resolves HTTP gateway", () => { test("resolveGateway resolves HTTP gateway", () => {
const config: OpenClawConfig = testCoerce({ const config: OpenClawConfig = unsafeTestValue({
enabled: true, enabled: true,
gateways: { gateways: {
discord: { discord: {
@@ -30,7 +31,7 @@ describe("OpenClaw Config", () => {
}) })
test("resolveGateway returns null for disabled config", () => { test("resolveGateway returns null for disabled config", () => {
const config: OpenClawConfig = testCoerce({ const config: OpenClawConfig = unsafeTestValue({
enabled: false, enabled: false,
gateways: {}, gateways: {},
hooks: {}, hooks: {},
@@ -39,7 +40,7 @@ describe("OpenClaw Config", () => {
}) })
test("resolveGateway returns null for unknown hook", () => { test("resolveGateway returns null for unknown hook", () => {
const config: OpenClawConfig = testCoerce({ const config: OpenClawConfig = unsafeTestValue({
enabled: true, enabled: true,
gateways: {}, gateways: {},
hooks: {}, hooks: {},
@@ -48,7 +49,7 @@ describe("OpenClaw Config", () => {
}) })
test("resolveGateway returns null for disabled hook", () => { test("resolveGateway returns null for disabled hook", () => {
const config: OpenClawConfig = testCoerce({ const config: OpenClawConfig = unsafeTestValue({
enabled: true, enabled: true,
gateways: { g: { type: "http", url: "https://example.com" } }, gateways: { g: { type: "http", url: "https://example.com" } },
hooks: { hooks: {
@@ -8,6 +8,7 @@ import * as injectionModule from "../reply-listener-injection"
import * as sessionRegistryModule from "../session-registry" import * as sessionRegistryModule from "../session-registry"
import type { ReplyListenerDaemonState } from "../reply-listener-state" import type { ReplyListenerDaemonState } from "../reply-listener-state"
import type { OpenClawConfig } from "../types" import type { OpenClawConfig } from "../types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const originalFetch = globalThis.fetch const originalFetch = globalThis.fetch
@@ -75,7 +76,7 @@ describe("pollDiscordReplies", () => {
status: 401, status: 401,
}), }),
)) ))
globalThis.fetch = testCoerce<typeof fetch>(fetchMock) globalThis.fetch = unsafeTestValue<typeof fetch>(fetchMock)
const state = createState() const state = createState()
@@ -109,7 +110,7 @@ describe("pollDiscordReplies", () => {
), ),
) )
.mockResolvedValueOnce(new Response(null, { status: 204 })) .mockResolvedValueOnce(new Response(null, { status: 204 }))
globalThis.fetch = testCoerce<typeof fetch>(fetchMock) globalThis.fetch = unsafeTestValue<typeof fetch>(fetchMock)
const lookupSpy = spyOn(sessionRegistryModule, "lookupByMessageId").mockReturnValue({ const lookupSpy = spyOn(sessionRegistryModule, "lookupByMessageId").mockReturnValue({
sessionId: "ses-1", sessionId: "ses-1",
tmuxSession: "session-1", tmuxSession: "session-1",
+103 -95
View File
@@ -22,6 +22,7 @@ import * as modelResolver from "../shared/model-resolver"
import * as configErrors from "../shared/config-errors" import * as configErrors from "../shared/config-errors"
import * as agentPriorityOrder from "./agent-priority-order" import * as agentPriorityOrder from "./agent-priority-order"
import * as prometheusAgentConfigBuilder from "./prometheus-agent-config-builder" import * as prometheusAgentConfigBuilder from "./prometheus-agent-config-builder"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
let createConfigHandler: (typeof import("./config-handler"))["createConfigHandler"] let createConfigHandler: (typeof import("./config-handler"))["createConfigHandler"]
@@ -46,36 +47,36 @@ beforeEach(async () => {
mock.restore() mock.restore()
configErrors.clearConfigLoadErrors() configErrors.clearConfigLoadErrors()
spyOn(agents, testCoerce("createBuiltinAgents")).mockResolvedValue({ spyOn(agents, unsafeTestValue("createBuiltinAgents")).mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
oracle: { name: "oracle", prompt: "test", mode: "subagent" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" },
}) })
spyOn(commandLoader, testCoerce("loadUserCommands")).mockResolvedValue({}) spyOn(commandLoader, unsafeTestValue("loadUserCommands")).mockResolvedValue({})
spyOn(commandLoader, testCoerce("loadProjectCommands")).mockResolvedValue({}) spyOn(commandLoader, unsafeTestValue("loadProjectCommands")).mockResolvedValue({})
spyOn(commandLoader, testCoerce("loadOpencodeGlobalCommands")).mockResolvedValue({}) spyOn(commandLoader, unsafeTestValue("loadOpencodeGlobalCommands")).mockResolvedValue({})
spyOn(commandLoader, testCoerce("loadOpencodeProjectCommands")).mockResolvedValue({}) spyOn(commandLoader, unsafeTestValue("loadOpencodeProjectCommands")).mockResolvedValue({})
spyOn(builtinCommands, testCoerce("loadBuiltinCommands")).mockReturnValue({}) spyOn(builtinCommands, unsafeTestValue("loadBuiltinCommands")).mockReturnValue({})
spyOn(skillLoader, testCoerce("loadUserSkills")).mockResolvedValue({}) spyOn(skillLoader, unsafeTestValue("loadUserSkills")).mockResolvedValue({})
spyOn(skillLoader, testCoerce("loadProjectSkills")).mockResolvedValue({}) spyOn(skillLoader, unsafeTestValue("loadProjectSkills")).mockResolvedValue({})
spyOn(skillLoader, testCoerce("loadOpencodeGlobalSkills")).mockResolvedValue({}) spyOn(skillLoader, unsafeTestValue("loadOpencodeGlobalSkills")).mockResolvedValue({})
spyOn(skillLoader, testCoerce("loadOpencodeProjectSkills")).mockResolvedValue({}) spyOn(skillLoader, unsafeTestValue("loadOpencodeProjectSkills")).mockResolvedValue({})
spyOn(skillLoader, testCoerce("discoverUserClaudeSkills")).mockResolvedValue([]) spyOn(skillLoader, unsafeTestValue("discoverUserClaudeSkills")).mockResolvedValue([])
spyOn(skillLoader, testCoerce("discoverProjectClaudeSkills")).mockResolvedValue([]) spyOn(skillLoader, unsafeTestValue("discoverProjectClaudeSkills")).mockResolvedValue([])
spyOn(skillLoader, testCoerce("discoverOpencodeGlobalSkills")).mockResolvedValue([]) spyOn(skillLoader, unsafeTestValue("discoverOpencodeGlobalSkills")).mockResolvedValue([])
spyOn(skillLoader, testCoerce("discoverOpencodeProjectSkills")).mockResolvedValue([]) spyOn(skillLoader, unsafeTestValue("discoverOpencodeProjectSkills")).mockResolvedValue([])
spyOn(agentLoader, testCoerce("loadUserAgents")).mockReturnValue({}) spyOn(agentLoader, unsafeTestValue("loadUserAgents")).mockReturnValue({})
spyOn(agentLoader, testCoerce("loadProjectAgents")).mockReturnValue({}) spyOn(agentLoader, unsafeTestValue("loadProjectAgents")).mockReturnValue({})
spyOn(agentLoader, testCoerce("loadOpencodeGlobalAgents")).mockReturnValue({}) spyOn(agentLoader, unsafeTestValue("loadOpencodeGlobalAgents")).mockReturnValue({})
spyOn(agentLoader, testCoerce("loadOpencodeProjectAgents")).mockReturnValue({}) spyOn(agentLoader, unsafeTestValue("loadOpencodeProjectAgents")).mockReturnValue({})
spyOn(mcpLoader, testCoerce("loadMcpConfigs")).mockResolvedValue({ servers: {} }) spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({ servers: {}, loadedServers: [] })
setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {}) setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {})
spyOn(pluginLoader, testCoerce("loadAllPluginComponents")).mockResolvedValue({ spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockResolvedValue({
commands: {}, commands: {},
skills: {}, skills: {},
agents: {}, agents: {},
@@ -85,54 +86,57 @@ beforeEach(async () => {
errors: [], errors: [],
}) })
spyOn(mcpModule, testCoerce("createBuiltinMcps")).mockReturnValue({}) spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({})
spyOn(shared, testCoerce("log")).mockImplementation(() => {}) spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {})
spyOn(shared, testCoerce("fetchAvailableModels")).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"])) spyOn(shared, unsafeTestValue("fetchAvailableModels")).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"]))
spyOn(shared, testCoerce("readConnectedProvidersCache")).mockReturnValue(null) spyOn(shared, unsafeTestValue("readConnectedProvidersCache")).mockReturnValue(null)
spyOn(configDir, testCoerce("getOpenCodeConfigPaths")).mockReturnValue({ spyOn(configDir, unsafeTestValue("getOpenCodeConfigPaths")).mockReturnValue({
global: "/tmp/.config/opencode", configDir: "/tmp/.config/opencode",
project: "/tmp/.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, testCoerce("migrateAgentConfig")).mockImplementation((config: Record<string, unknown>) => config) spyOn(permissionCompat, unsafeTestValue("migrateAgentConfig")).mockImplementation((config: Record<string, unknown>) => config)
spyOn(modelResolver, testCoerce("resolveModelWithFallback")).mockReturnValue({ model: "anthropic/claude-opus-4-7" }) spyOn(modelResolver, unsafeTestValue("resolveModelWithFallback")).mockReturnValue({ model: "anthropic/claude-opus-4-7", source: "provider-fallback" })
;({ createConfigHandler } = await importFreshConfigHandlerModule()) ;({ createConfigHandler } = await importFreshConfigHandlerModule())
}) })
afterEach(() => { afterEach(() => {
(testCoerce(agents.createBuiltinAgents))?.mockRestore?.() (unsafeTestValue(agents.createBuiltinAgents))?.mockRestore?.()
;(testCoerce(sisyphusJunior.createSisyphusJuniorAgentWithOverrides))?.mockRestore?.() ;(unsafeTestValue(sisyphusJunior.createSisyphusJuniorAgentWithOverrides))?.mockRestore?.()
;(testCoerce(commandLoader.loadUserCommands))?.mockRestore?.() ;(unsafeTestValue(commandLoader.loadUserCommands))?.mockRestore?.()
;(testCoerce(commandLoader.loadProjectCommands))?.mockRestore?.() ;(unsafeTestValue(commandLoader.loadProjectCommands))?.mockRestore?.()
;(testCoerce(commandLoader.loadOpencodeGlobalCommands))?.mockRestore?.() ;(unsafeTestValue(commandLoader.loadOpencodeGlobalCommands))?.mockRestore?.()
;(testCoerce(commandLoader.loadOpencodeProjectCommands))?.mockRestore?.() ;(unsafeTestValue(commandLoader.loadOpencodeProjectCommands))?.mockRestore?.()
;(testCoerce(builtinCommands.loadBuiltinCommands))?.mockRestore?.() ;(unsafeTestValue(builtinCommands.loadBuiltinCommands))?.mockRestore?.()
;(testCoerce(skillLoader.loadUserSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.loadUserSkills))?.mockRestore?.()
;(testCoerce(skillLoader.loadProjectSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.loadProjectSkills))?.mockRestore?.()
;(testCoerce(skillLoader.loadOpencodeGlobalSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.loadOpencodeGlobalSkills))?.mockRestore?.()
;(testCoerce(skillLoader.loadOpencodeProjectSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.loadOpencodeProjectSkills))?.mockRestore?.()
;(testCoerce(skillLoader.discoverUserClaudeSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.discoverUserClaudeSkills))?.mockRestore?.()
;(testCoerce(skillLoader.discoverProjectClaudeSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.discoverProjectClaudeSkills))?.mockRestore?.()
;(testCoerce(skillLoader.discoverOpencodeGlobalSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.discoverOpencodeGlobalSkills))?.mockRestore?.()
;(testCoerce(skillLoader.discoverOpencodeProjectSkills))?.mockRestore?.() ;(unsafeTestValue(skillLoader.discoverOpencodeProjectSkills))?.mockRestore?.()
;(testCoerce(agentLoader.loadUserAgents))?.mockRestore?.() ;(unsafeTestValue(agentLoader.loadUserAgents))?.mockRestore?.()
;(testCoerce(agentLoader.loadProjectAgents))?.mockRestore?.() ;(unsafeTestValue(agentLoader.loadProjectAgents))?.mockRestore?.()
;(testCoerce(agentLoader.loadOpencodeGlobalAgents))?.mockRestore?.() ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents))?.mockRestore?.()
;(testCoerce(agentLoader.loadOpencodeProjectAgents))?.mockRestore?.() ;(unsafeTestValue(agentLoader.loadOpencodeProjectAgents))?.mockRestore?.()
;(testCoerce(mcpLoader.loadMcpConfigs))?.mockRestore?.() ;(unsafeTestValue(mcpLoader.loadMcpConfigs))?.mockRestore?.()
setAdditionalAllowedMcpEnvVarsSpy?.mockRestore() setAdditionalAllowedMcpEnvVarsSpy?.mockRestore()
;(testCoerce(pluginLoader.loadAllPluginComponents))?.mockRestore?.() ;(unsafeTestValue(pluginLoader.loadAllPluginComponents))?.mockRestore?.()
;(testCoerce(mcpModule.createBuiltinMcps))?.mockRestore?.() ;(unsafeTestValue(mcpModule.createBuiltinMcps))?.mockRestore?.()
;(testCoerce(shared.log))?.mockRestore?.() ;(unsafeTestValue(shared.log))?.mockRestore?.()
;(testCoerce(shared.fetchAvailableModels))?.mockRestore?.() ;(unsafeTestValue(shared.fetchAvailableModels))?.mockRestore?.()
;(testCoerce(shared.readConnectedProvidersCache))?.mockRestore?.() ;(unsafeTestValue(shared.readConnectedProvidersCache))?.mockRestore?.()
;(testCoerce(configDir.getOpenCodeConfigPaths))?.mockRestore?.() ;(unsafeTestValue(configDir.getOpenCodeConfigPaths))?.mockRestore?.()
;(testCoerce(permissionCompat.migrateAgentConfig))?.mockRestore?.() ;(unsafeTestValue(permissionCompat.migrateAgentConfig))?.mockRestore?.()
;(testCoerce(modelResolver.resolveModelWithFallback))?.mockRestore?.() ;(unsafeTestValue(modelResolver.resolveModelWithFallback))?.mockRestore?.()
;(testCoerce(agentPriorityOrder.reorderAgentsByPriority))?.mockRestore?.() ;(unsafeTestValue(agentPriorityOrder.reorderAgentsByPriority))?.mockRestore?.()
configErrors.clearConfigLoadErrors() configErrors.clearConfigLoadErrors()
mock.restore() mock.restore()
}) })
@@ -230,7 +234,7 @@ describe("MCP env allowlist initialization", () => {
describe("Plan agent demote behavior", () => { describe("Plan agent demote behavior", () => {
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => { test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
// #given // #given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -275,7 +279,7 @@ describe("Plan agent demote behavior", () => {
test("assembles core agents first before priority reorder runs", async () => { test("assembles core agents first before priority reorder runs", async () => {
// #given // #given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -285,7 +289,7 @@ describe("Plan agent demote behavior", () => {
oracle: { name: "oracle", prompt: "test", mode: "subagent" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" },
atlas: { name: "atlas", prompt: "test", mode: "primary" }, atlas: { name: "atlas", prompt: "test", mode: "primary" },
}) })
const reorderSpy = testCoerce(spyOn(agentPriorityOrder, "reorderAgentsByPriority")) const reorderSpy = unsafeTestValue(spyOn(agentPriorityOrder, "reorderAgentsByPriority"))
const pluginConfig = createPluginConfig({ const pluginConfig = createPluginConfig({
sisyphus_agent: { sisyphus_agent: {
planner_enabled: true, planner_enabled: true,
@@ -321,7 +325,7 @@ describe("Plan agent demote behavior", () => {
test("backfills runtime core agent names when builtin configs omit name", async () => { test("backfills runtime core agent names when builtin configs omit name", async () => {
// #given // #given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
createBuiltinAgentsMock.mockResolvedValue({ createBuiltinAgentsMock.mockResolvedValue({
@@ -485,7 +489,7 @@ describe("Plan agent demote behavior", () => {
describe("Agent permission defaults", () => { describe("Agent permission defaults", () => {
test("hephaestus should allow task", async () => { test("hephaestus should allow task", async () => {
// #given // #given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
createBuiltinAgentsMock.mockResolvedValue({ createBuiltinAgentsMock.mockResolvedValue({
@@ -1054,7 +1058,7 @@ describe("Plan agent model inheritance from prometheus", () => {
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => { test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
//#given - prometheus configured with category that has temperature and reasoningEffort //#given - prometheus configured with category that has temperature and reasoningEffort
spyOn(shared, testCoerce("resolveModelPipeline")).mockReturnValue({ spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({
model: "openai/gpt-5.4", model: "openai/gpt-5.4",
provenance: "override", provenance: "override",
variant: "high", 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 () => { 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 //#given - prometheus resolves to opus, but user has plan override for gpt-5.4
spyOn(shared, testCoerce("resolveModelPipeline")).mockReturnValue({ spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
provenance: "provider-fallback", provenance: "provider-fallback",
variant: "max", 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 () => { test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => {
//#given //#given
spyOn(shared, testCoerce("resolveModelPipeline")).mockReturnValue({ spyOn(shared, unsafeTestValue("resolveModelPipeline")).mockReturnValue({
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
provenance: "provider-fallback", provenance: "provider-fallback",
variant: "max", variant: "max",
@@ -1229,8 +1233,10 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
describe("config-handler plugin loading error boundary (#1559)", () => { describe("config-handler plugin loading error boundary (#1559)", () => {
test("returns empty defaults when loadAllPluginComponents throws", async () => { test("returns empty defaults when loadAllPluginComponents throws", async () => {
//#given //#given
;(testCoerce(pluginLoader.loadAllPluginComponents)).mockRestore?.() ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
spyOn(pluginLoader, testCoerce("loadAllPluginComponents")).mockRejectedValue(new Error("crash")) spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation(async () => {
throw new Error("crash")
})
const pluginConfig = createPluginConfig({}) const pluginConfig = createPluginConfig({})
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", 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 () => { test("returns empty defaults when loadAllPluginComponents times out", async () => {
//#given //#given
;(testCoerce(pluginLoader.loadAllPluginComponents)).mockRestore?.() ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
spyOn(pluginLoader, testCoerce("loadAllPluginComponents")).mockImplementation( spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation(
() => new Promise(() => {}) () => new Promise(() => {})
) )
const pluginConfig = createPluginConfig({ 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 () => { test("records a config load error when loadAllPluginComponents fails", async () => {
//#given //#given
;(testCoerce(pluginLoader.loadAllPluginComponents)).mockRestore?.() ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
spyOn(pluginLoader, testCoerce("loadAllPluginComponents")).mockRejectedValue(new Error("crash")) spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockImplementation(async () => {
throw new Error("crash")
})
const pluginConfig = createPluginConfig({}) const pluginConfig = createPluginConfig({})
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", 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 () => { test("passes through plugin data on successful load (identity test)", async () => {
//#given //#given
;(testCoerce(pluginLoader.loadAllPluginComponents)).mockRestore?.() ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
spyOn(pluginLoader, testCoerce("loadAllPluginComponents")).mockResolvedValue({ spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockResolvedValue({
commands: { "test-cmd": { description: "test", template: "test" } }, commands: { "test-cmd": { name: "test-cmd", description: "test", template: "test" } },
skills: {}, skills: {},
agents: {}, agents: {},
mcpServers: {}, mcpServers: {},
hooksConfigs: [], 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: [], errors: [],
}) })
const pluginConfig = createPluginConfig({}) const pluginConfig = createPluginConfig({})
@@ -1351,14 +1359,14 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
describe("command agent routing coherence", () => { describe("command agent routing coherence", () => {
test("keeps start-work aligned with the exported Atlas list key opencode matches exactly", async () => { test("keeps start-work aligned with the exported Atlas list key opencode matches exactly", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
createBuiltinAgentsMock.mockResolvedValue({ createBuiltinAgentsMock.mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
atlas: { name: "atlas", prompt: "test", mode: "primary" }, atlas: { name: "atlas", prompt: "test", mode: "primary" },
}) })
;(testCoerce<{ ;(unsafeTestValue<{
mockReturnValue: (value: Record<string, unknown>) => void mockReturnValue: (value: Record<string, unknown>) => void
}>(builtinCommands.loadBuiltinCommands)).mockReturnValue({ }>(builtinCommands.loadBuiltinCommands)).mockReturnValue({
"start-work": { "start-work": {
@@ -1404,7 +1412,7 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
test("denies todowrite and todoread for primary agents when task_system is enabled", async () => { test("denies todowrite and todoread for primary agents when task_system is enabled", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
createBuiltinAgentsMock.mockResolvedValue({ createBuiltinAgentsMock.mockResolvedValue({
@@ -1445,7 +1453,7 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
test("does not deny todowrite/todoread when task_system is disabled", async () => { test("does not deny todowrite/todoread when task_system is disabled", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -1487,7 +1495,7 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
test("does not deny todowrite/todoread when task_system is undefined", async () => { test("does not deny todowrite/todoread when task_system is undefined", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -1526,7 +1534,7 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
describe("disable_omo_env pass-through", () => { describe("disable_omo_env pass-through", () => {
test("passes disable_omo_env=true to createBuiltinAgents", async () => { test("passes disable_omo_env=true to createBuiltinAgents", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -1563,7 +1571,7 @@ describe("disable_omo_env pass-through", () => {
test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => { test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => {
//#given //#given
const createBuiltinAgentsMock = testCoerce<{ const createBuiltinAgentsMock = unsafeTestValue<{
mockResolvedValue: (value: Record<string, unknown>) => void mockResolvedValue: (value: Record<string, unknown>) => void
mock: { calls: unknown[][] } mock: { calls: unknown[][] }
}>(agents.createBuiltinAgents) }>(agents.createBuiltinAgents)
@@ -1600,14 +1608,14 @@ describe("disable_omo_env pass-through", () => {
describe("Agent merge priority — project-local overrides global", () => { describe("Agent merge priority — project-local overrides global", () => {
test("project-local Claude agent overrides global Claude agent with same name", async () => { test("project-local Claude agent overrides global Claude agent with same name", async () => {
// #given — same agent name in both global (user) and project scopes // #given — same agent name in both global (user) and project scopes
;(testCoerce(agentLoader.loadUserAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadUserAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(user) global version", description: "(user) global version",
mode: "subagent", mode: "subagent",
prompt: "I am the global agent", prompt: "I am the global agent",
}, },
}) })
;(testCoerce(agentLoader.loadProjectAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadProjectAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(project) project version", description: "(project) project version",
mode: "subagent", mode: "subagent",
@@ -1615,7 +1623,7 @@ describe("Agent merge priority — project-local overrides global", () => {
}, },
}) })
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
agent: {}, 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 () => { test("opencode project agent overrides opencode global agent with same name", async () => {
// #given — same agent name in opencode global vs opencode project // #given — same agent name in opencode global vs opencode project
;(testCoerce(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(opencode) global version", description: "(opencode) global version",
mode: "subagent", mode: "subagent",
prompt: "I am the opencode global agent", prompt: "I am the opencode global agent",
}, },
}) })
;(testCoerce(agentLoader.loadOpencodeProjectAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadOpencodeProjectAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(opencode-project) project version", description: "(opencode-project) project version",
mode: "subagent", mode: "subagent",
@@ -1655,7 +1663,7 @@ describe("Agent merge priority — project-local overrides global", () => {
}, },
}) })
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
agent: {}, 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 () => { test("project Claude agent overrides opencode global agent with same name", async () => {
// #given — project-scope Claude agent vs global-scope opencode agent // #given — project-scope Claude agent vs global-scope opencode agent
;(testCoerce(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(opencode) global version", description: "(opencode) global version",
mode: "subagent", mode: "subagent",
prompt: "I am the opencode global agent", prompt: "I am the opencode global agent",
}, },
}) })
;(testCoerce(agentLoader.loadProjectAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadProjectAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(project) project version", description: "(project) project version",
mode: "subagent", mode: "subagent",
@@ -1695,7 +1703,7 @@ describe("Agent merge priority — project-local overrides global", () => {
}, },
}) })
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
agent: {}, 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 () => { test("plugin agents have lowest priority — overridden by all other sources", async () => {
// #given — same agent in plugin, global, and project scopes // #given — same agent in plugin, global, and project scopes
;(testCoerce(pluginLoader.loadAllPluginComponents)).mockResolvedValue({ ;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockResolvedValue({
commands: {}, commands: {},
skills: {}, skills: {},
agents: { agents: {
@@ -1735,7 +1743,7 @@ describe("Agent merge priority — project-local overrides global", () => {
plugins: [], plugins: [],
errors: [], errors: [],
}) })
;(testCoerce(agentLoader.loadUserAgents)).mockReturnValue({ ;(unsafeTestValue(agentLoader.loadUserAgents)).mockReturnValue({
"my-custom-agent": { "my-custom-agent": {
description: "(user) global version", description: "(user) global version",
mode: "subagent", mode: "subagent",
@@ -1743,7 +1751,7 @@ describe("Agent merge priority — project-local overrides global", () => {
}, },
}) })
const pluginConfig: OhMyOpenCodeConfig = {} const pluginConfig = createPluginConfig()
const config: Record<string, unknown> = { const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-7", model: "anthropic/claude-opus-4-7",
agent: {}, agent: {},
@@ -6,22 +6,23 @@ import type { OhMyOpenCodeConfig } from "../config"
import * as mcpLoader from "../features/claude-code-mcp-loader" import * as mcpLoader from "../features/claude-code-mcp-loader"
import * as mcpModule from "../mcp" import * as mcpModule from "../mcp"
import * as shared from "../shared" import * as shared from "../shared"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
let loadMcpConfigsSpy: ReturnType<typeof spyOn> let loadMcpConfigsSpy: ReturnType<typeof spyOn>
let createBuiltinMcpsSpy: ReturnType<typeof spyOn> let createBuiltinMcpsSpy: ReturnType<typeof spyOn>
beforeEach(() => { beforeEach(() => {
loadMcpConfigsSpy = spyOn(mcpLoader, testCoerce("loadMcpConfigs")).mockResolvedValue({ loadMcpConfigsSpy = spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({
servers: {}, servers: {},
}) })
createBuiltinMcpsSpy = spyOn(mcpModule, testCoerce("createBuiltinMcps")).mockReturnValue({}) createBuiltinMcpsSpy = spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({})
spyOn(shared, testCoerce("log")).mockImplementation(() => {}) spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {})
}) })
afterEach(() => { afterEach(() => {
loadMcpConfigsSpy.mockRestore() loadMcpConfigsSpy.mockRestore()
createBuiltinMcpsSpy.mockRestore() createBuiltinMcpsSpy.mockRestore()
;(testCoerce(shared.log))?.mockRestore?.() ;(unsafeTestValue(shared.log))?.mockRestore?.()
}) })
function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig { function createPluginConfig(overrides: Partial<OhMyOpenCodeConfig> = {}): OhMyOpenCodeConfig {
@@ -82,7 +83,7 @@ describe("applyMcpConfig", () => {
}) })
const config: Record<string, unknown> = { mcp: {} } const config: Record<string, unknown> = { mcp: {} }
const pluginConfig = createPluginConfig({ disabled_mcps: testCoerce(["playwright"]) }) const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["playwright"]) })
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await import("./mcp-config-handler")
@@ -107,7 +108,7 @@ describe("applyMcpConfig", () => {
test("passes disabled_mcps to loadMcpConfigs", async () => { test("passes disabled_mcps to loadMcpConfigs", async () => {
//#given //#given
const config: Record<string, unknown> = { mcp: {} } const config: Record<string, unknown> = { mcp: {} }
const pluginConfig = createPluginConfig({ disabled_mcps: testCoerce(["firecrawl", "exa"]) }) const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["firecrawl", "exa"]) })
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await import("./mcp-config-handler")
@@ -145,7 +146,7 @@ describe("applyMcpConfig", () => {
test("deletes plugin MCPs that are in disabled_mcps", async () => { test("deletes plugin MCPs that are in disabled_mcps", async () => {
//#given //#given
const config: Record<string, unknown> = { mcp: {} } const config: Record<string, unknown> = { mcp: {} }
const pluginConfig = createPluginConfig({ disabled_mcps: testCoerce(["plugin:custom"]) }) const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["plugin:custom"]) })
//#when //#when
const { applyMcpConfig } = await import("./mcp-config-handler") const { applyMcpConfig } = await import("./mcp-config-handler")
+4 -3
View File
@@ -13,6 +13,7 @@ import { _resetForTesting, setMainSession, subagentSessions, registerAgentName,
import { getAgentListDisplayName } from "../shared/agent-display-names" import { getAgentListDisplayName } from "../shared/agent-display-names"
import { getOmoOpenCodeCacheDir, getOpenCodeCacheDir } from "../shared/data-path" import { getOmoOpenCodeCacheDir, getOpenCodeCacheDir } from "../shared/data-path"
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state" 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 ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
type ChatMessageHandlerOutput = { message: Record<string, unknown>; parts: ChatMessagePart[] } type ChatMessageHandlerOutput = { message: Record<string, unknown>; parts: ChatMessagePart[] }
@@ -56,13 +57,13 @@ function createMockHandlerArgs(overrides?: {
}) { }) {
const appliedSessions: string[] = [] const appliedSessions: string[] = []
return { return {
ctx: testCoerce({ client: { tui: { showToast: async () => {} } } }), ctx: unsafeTestValue({ client: { tui: { showToast: async () => {} } } }),
pluginConfig: testCoerce((overrides?.pluginConfig ?? {})), pluginConfig: unsafeTestValue((overrides?.pluginConfig ?? {})),
firstMessageVariantGate: { firstMessageVariantGate: {
shouldOverride: () => overrides?.shouldOverride ?? false, shouldOverride: () => overrides?.shouldOverride ?? false,
markApplied: (sessionID: string) => { appliedSessions.push(sessionID) }, markApplied: (sessionID: string) => { appliedSessions.push(sessionID) },
}, },
hooks: testCoerce({ hooks: unsafeTestValue({
stopContinuationGuard: null, stopContinuationGuard: null,
backgroundNotificationHook: null, backgroundNotificationHook: null,
keywordDetector: null, keywordDetector: null,
+9 -8
View File
@@ -6,6 +6,7 @@ import { createChatMessageHandler } from "./chat-message"
import { _resetForTesting, setSessionAgent } from "../features/claude-code-session-state" import { _resetForTesting, setSessionAgent } from "../features/claude-code-session-state"
import { clearPendingModelFallback, createModelFallbackHook, setSessionFallbackChain } from "../hooks/model-fallback/hook" import { clearPendingModelFallback, createModelFallbackHook, setSessionFallbackChain } from "../hooks/model-fallback/hook"
import * as connectedProvidersCache from "../shared/connected-providers-cache" import * as connectedProvidersCache from "../shared/connected-providers-cache"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
type EventInput = { event: { type: string; properties?: unknown } } type EventInput = { event: { type: string; properties?: unknown } }
type EventHandlerArgs = Parameters<typeof createEventHandler>[0] type EventHandlerArgs = Parameters<typeof createEventHandler>[0]
@@ -13,27 +14,27 @@ type EventHandlerInput = Parameters<ReturnType<typeof createEventHandler>>[0]
type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0] type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0]
function asEventHandlerInput(input: EventInput): EventHandlerInput { function asEventHandlerInput(input: EventInput): EventHandlerInput {
return testCoerce<EventHandlerInput>(input) return unsafeTestValue<EventHandlerInput>(input)
} }
function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] { function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] {
return testCoerce<EventHandlerArgs["ctx"]>(ctx) return unsafeTestValue<EventHandlerArgs["ctx"]>(ctx)
} }
function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] { function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] {
return testCoerce<EventHandlerArgs["pluginConfig"]>(config) return unsafeTestValue<EventHandlerArgs["pluginConfig"]>(config)
} }
function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] { function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] {
return testCoerce<ChatMessageHandlerArgs["ctx"]>(ctx) return unsafeTestValue<ChatMessageHandlerArgs["ctx"]>(ctx)
} }
function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] { function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] {
return testCoerce<ChatMessageHandlerArgs["pluginConfig"]>(config) return unsafeTestValue<ChatMessageHandlerArgs["pluginConfig"]>(config)
} }
function createEventHandlerManagers(): EventHandlerArgs["managers"] { function createEventHandlerManagers(): EventHandlerArgs["managers"] {
return testCoerce<EventHandlerArgs["managers"]>({ return unsafeTestValue<EventHandlerArgs["managers"]>({
tmuxSessionManager: { tmuxSessionManager: {
onSessionCreated: async () => {}, onSessionCreated: async () => {},
onSessionDeleted: async () => {}, onSessionDeleted: async () => {},
@@ -45,13 +46,13 @@ function createEventHandlerManagers(): EventHandlerArgs["managers"] {
} }
function createEventHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): EventHandlerArgs["hooks"] { function createEventHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): EventHandlerArgs["hooks"] {
return testCoerce<EventHandlerArgs["hooks"]>({ return unsafeTestValue<EventHandlerArgs["hooks"]>({
modelFallback, modelFallback,
}) })
} }
function createChatMessageHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): ChatMessageHandlerArgs["hooks"] { function createChatMessageHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): ChatMessageHandlerArgs["hooks"] {
return testCoerce<ChatMessageHandlerArgs["hooks"]>({ return unsafeTestValue<ChatMessageHandlerArgs["hooks"]>({
modelFallback, modelFallback,
stopContinuationGuard: null, stopContinuationGuard: null,
keywordDetector: null, keywordDetector: null,
@@ -5,6 +5,7 @@ import { createEventHandler } from "./event"
import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state"
import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook"
import * as connectedProvidersCache from "../shared/connected-providers-cache" import * as connectedProvidersCache from "../shared/connected-providers-cache"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined
@@ -50,16 +51,16 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model
} }
const handler = createEventHandler({ const handler = createEventHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
directory: "/tmp", directory: "/tmp",
client: { session: sessionClient }, client: { session: sessionClient },
}), }),
pluginConfig: testCoerce((args?.pluginConfig ?? {})), pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})),
firstMessageVariantGate: { firstMessageVariantGate: {
markSessionCreated: () => {}, markSessionCreated: () => {},
clear: () => {}, clear: () => {},
}, },
managers: testCoerce({ managers: unsafeTestValue({
tmuxSessionManager: { tmuxSessionManager: {
onSessionCreated: async () => {}, onSessionCreated: async () => {},
onSessionDeleted: async () => {}, onSessionDeleted: async () => {},
@@ -68,7 +69,7 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model
disconnectSession: async () => {}, disconnectSession: async () => {},
}, },
}), }),
hooks: args?.hooks ?? (testCoerce({})), hooks: args?.hooks ?? (unsafeTestValue({})),
}) })
return { handler, promptAsyncBodies, promptBodies } return { handler, promptAsyncBodies, promptBodies }
+18 -17
View File
@@ -6,6 +6,7 @@ import { createChatMessageHandler } from "./chat-message"
import { _resetForTesting, setMainSession } from "../features/claude-code-session-state" import { _resetForTesting, setMainSession } from "../features/claude-code-session-state"
import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook" import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook"
import * as connectedProvidersCache from "../shared/connected-providers-cache" import * as connectedProvidersCache from "../shared/connected-providers-cache"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined
@@ -22,7 +23,7 @@ describe("createEventHandler - model fallback", () => {
const promptCalls: string[] = [] const promptCalls: string[] = []
const handler = createEventHandler({ const handler = createEventHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -37,12 +38,12 @@ describe("createEventHandler - model fallback", () => {
}, },
}, },
}), }),
pluginConfig: testCoerce((args?.pluginConfig ?? {})), pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})),
firstMessageVariantGate: { firstMessageVariantGate: {
markSessionCreated: () => {}, markSessionCreated: () => {},
clear: () => {}, clear: () => {},
}, },
managers: testCoerce({ managers: unsafeTestValue({
tmuxSessionManager: { tmuxSessionManager: {
onSessionCreated: async () => {}, onSessionCreated: async () => {},
onSessionDeleted: async () => {}, onSessionDeleted: async () => {},
@@ -51,7 +52,7 @@ describe("createEventHandler - model fallback", () => {
disconnectSession: async () => {}, disconnectSession: async () => {},
}, },
}), }),
hooks: args?.hooks ?? (testCoerce({})), hooks: args?.hooks ?? (unsafeTestValue({})),
}) })
return { handler, abortCalls, promptCalls } return { handler, abortCalls, promptCalls }
@@ -148,19 +149,19 @@ describe("createEventHandler - model fallback", () => {
const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback } }) const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback } })
const chatMessageHandler = createChatMessageHandler({ const chatMessageHandler = createChatMessageHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async () => ({}), showToast: async () => ({}),
}, },
}, },
}), }),
pluginConfig: testCoerce({}), pluginConfig: unsafeTestValue({}),
firstMessageVariantGate: { firstMessageVariantGate: {
shouldOverride: () => false, shouldOverride: () => false,
markApplied: () => {}, markApplied: () => {},
}, },
hooks: testCoerce({ hooks: unsafeTestValue({
modelFallback, modelFallback,
stopContinuationGuard: null, stopContinuationGuard: null,
keywordDetector: null, keywordDetector: null,
@@ -358,19 +359,19 @@ describe("createEventHandler - model fallback", () => {
const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback }, pluginConfig }) const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback }, pluginConfig })
const chatMessageHandler = createChatMessageHandler({ const chatMessageHandler = createChatMessageHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async () => ({}), showToast: async () => ({}),
}, },
}, },
}), }),
pluginConfig: testCoerce({}), pluginConfig: unsafeTestValue({}),
firstMessageVariantGate: { firstMessageVariantGate: {
shouldOverride: () => false, shouldOverride: () => false,
markApplied: () => {}, markApplied: () => {},
}, },
hooks: testCoerce({ hooks: unsafeTestValue({
modelFallback, modelFallback,
stopContinuationGuard: null, stopContinuationGuard: null,
keywordDetector: null, keywordDetector: null,
@@ -449,7 +450,7 @@ describe("createEventHandler - model fallback", () => {
setupConnectedProviderCacheMocks() setupConnectedProviderCacheMocks()
const eventHandler = createEventHandler({ const eventHandler = createEventHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
directory: "/tmp", directory: "/tmp",
client: { client: {
session: { session: {
@@ -464,12 +465,12 @@ describe("createEventHandler - model fallback", () => {
}, },
}, },
}), }),
pluginConfig: testCoerce({}), pluginConfig: unsafeTestValue({}),
firstMessageVariantGate: { firstMessageVariantGate: {
markSessionCreated: () => {}, markSessionCreated: () => {},
clear: () => {}, clear: () => {},
}, },
managers: testCoerce({ managers: unsafeTestValue({
tmuxSessionManager: { tmuxSessionManager: {
onSessionCreated: async () => {}, onSessionCreated: async () => {},
onSessionDeleted: async () => {}, onSessionDeleted: async () => {},
@@ -478,13 +479,13 @@ describe("createEventHandler - model fallback", () => {
disconnectSession: async () => {}, disconnectSession: async () => {},
}, },
}), }),
hooks: testCoerce({ hooks: unsafeTestValue({
modelFallback, modelFallback,
}), }),
}) })
const chatMessageHandler = createChatMessageHandler({ const chatMessageHandler = createChatMessageHandler({
ctx: testCoerce({ ctx: unsafeTestValue({
client: { client: {
tui: { tui: {
showToast: async ({ body }: { body: { title?: string } }) => { showToast: async ({ body }: { body: { title?: string } }) => {
@@ -494,12 +495,12 @@ describe("createEventHandler - model fallback", () => {
}, },
}, },
}), }),
pluginConfig: testCoerce({}), pluginConfig: unsafeTestValue({}),
firstMessageVariantGate: { firstMessageVariantGate: {
shouldOverride: () => false, shouldOverride: () => false,
markApplied: () => {}, markApplied: () => {},
}, },
hooks: testCoerce({ hooks: unsafeTestValue({
modelFallback, modelFallback,
stopContinuationGuard: null, stopContinuationGuard: null,
keywordDetector: null, keywordDetector: null,
+10 -9
View File
@@ -11,6 +11,7 @@ import type { RuntimeFallbackPluginInput } from "../hooks/runtime-fallback/types
import { _resetForTesting } from "../features/claude-code-session-state" import { _resetForTesting } from "../features/claude-code-session-state"
import { SessionCategoryRegistry } from "../shared/session-category-registry" import { SessionCategoryRegistry } from "../shared/session-category-registry"
import * as connectedProvidersCache from "../shared/connected-providers-cache" import * as connectedProvidersCache from "../shared/connected-providers-cache"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
type EventHandlerArgs = Parameters<typeof createEventHandler>[0] type EventHandlerArgs = Parameters<typeof createEventHandler>[0]
type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0] type ChatMessageHandlerArgs = Parameters<typeof createChatMessageHandler>[0]
@@ -18,17 +19,17 @@ type HarnessContext = EventHandlerArgs["ctx"] & RuntimeFallbackPluginInput
type HarnessEventInput = Parameters<ReturnType<typeof createHarness>["eventHandler"]>[0] type HarnessEventInput = Parameters<ReturnType<typeof createHarness>["eventHandler"]>[0]
function asHarnessEventInput(input: unknown): HarnessEventInput { function asHarnessEventInput(input: unknown): HarnessEventInput {
return testCoerce<HarnessEventInput>(input) return unsafeTestValue<HarnessEventInput>(input)
} }
function asHarnessContext(ctx: unknown): HarnessContext { function asHarnessContext(ctx: unknown): HarnessContext {
return testCoerce<HarnessContext>(ctx) return unsafeTestValue<HarnessContext>(ctx)
} }
function createEventHandlerManagers( function createEventHandlerManagers(
overrides: Record<string, unknown> = {}, overrides: Record<string, unknown> = {},
): EventHandlerArgs["managers"] { ): EventHandlerArgs["managers"] {
return testCoerce<EventHandlerArgs["managers"]>({ return unsafeTestValue<EventHandlerArgs["managers"]>({
...({} as EventHandlerArgs["managers"]), ...({} as EventHandlerArgs["managers"]),
tmuxSessionManager: { tmuxSessionManager: {
onSessionCreated: async () => {}, onSessionCreated: async () => {},
@@ -41,7 +42,7 @@ function createEventHandlerManagers(
function createEventHandlerHooks( function createEventHandlerHooks(
overrides: Record<string, unknown>, overrides: Record<string, unknown>,
): EventHandlerArgs["hooks"] { ): EventHandlerArgs["hooks"] {
return testCoerce<EventHandlerArgs["hooks"]>({ return unsafeTestValue<EventHandlerArgs["hooks"]>({
...({} as EventHandlerArgs["hooks"]), ...({} as EventHandlerArgs["hooks"]),
...overrides, ...overrides,
}) })
@@ -50,7 +51,7 @@ function createEventHandlerHooks(
function createChatMessageHandlerHooks( function createChatMessageHandlerHooks(
overrides: Record<string, unknown>, overrides: Record<string, unknown>,
): ChatMessageHandlerArgs["hooks"] { ): ChatMessageHandlerArgs["hooks"] {
return testCoerce<ChatMessageHandlerArgs["hooks"]>({ return unsafeTestValue<ChatMessageHandlerArgs["hooks"]>({
...({} as ChatMessageHandlerArgs["hooks"]), ...({} as ChatMessageHandlerArgs["hooks"]),
...overrides, ...overrides,
}) })
@@ -87,7 +88,7 @@ let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined let readProviderModelsCacheSpy: { mockRestore: () => void } | undefined
function createPluginConfig(mode: HarnessMode) { function createPluginConfig(mode: HarnessMode) {
return testCoerce<EventHandlerArgs["pluginConfig"]>({ return unsafeTestValue<EventHandlerArgs["pluginConfig"]>({
agents: { agents: {
sisyphus: { sisyphus: {
fallback_models: CLIPROXYAPI_FALLBACKS, fallback_models: CLIPROXYAPI_FALLBACKS,
@@ -187,14 +188,14 @@ function createHarness(args: {
timeout_seconds: args.sessionTimeoutMs ? 30 : 0, timeout_seconds: args.sessionTimeoutMs ? 30 : 0,
notify_on_fallback: false, notify_on_fallback: false,
}, },
pluginConfig: testCoerce<EventHandlerArgs["pluginConfig"]>(pluginConfig), pluginConfig: unsafeTestValue<EventHandlerArgs["pluginConfig"]>(pluginConfig),
...(args.sessionTimeoutMs ? { session_timeout_ms: args.sessionTimeoutMs } : {}), ...(args.sessionTimeoutMs ? { session_timeout_ms: args.sessionTimeoutMs } : {}),
}) })
} }
const eventHandler = createEventHandler({ const eventHandler = createEventHandler({
ctx, ctx,
pluginConfig: testCoerce<EventHandlerArgs["pluginConfig"]>(pluginConfig), pluginConfig: unsafeTestValue<EventHandlerArgs["pluginConfig"]>(pluginConfig),
firstMessageVariantGate: { firstMessageVariantGate: {
markSessionCreated: () => {}, markSessionCreated: () => {},
clear: () => {}, clear: () => {},
@@ -209,7 +210,7 @@ function createHarness(args: {
const chatMessageHandler = createChatMessageHandler({ const chatMessageHandler = createChatMessageHandler({
ctx, ctx,
pluginConfig: testCoerce<ChatMessageHandlerArgs["pluginConfig"]>(pluginConfig), pluginConfig: unsafeTestValue<ChatMessageHandlerArgs["pluginConfig"]>(pluginConfig),
firstMessageVariantGate: { firstMessageVariantGate: {
shouldOverride: () => false, shouldOverride: () => false,
markApplied: () => {}, markApplied: () => {},
@@ -3,8 +3,9 @@ import type { OhMyOpenCodeConfig } from "../../config"
import type { ModelCacheState } from "../../plugin-state" import type { ModelCacheState } from "../../plugin-state"
import type { PluginContext } from "../types" import type { PluginContext } from "../types"
import { createSessionHooks } from "./create-session-hooks" import { createSessionHooks } from "./create-session-hooks"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const mockContext = testCoerce<PluginContext>({ const mockContext = unsafeTestValue<PluginContext>({
directory: "/tmp", directory: "/tmp",
client: { client: {
tui: { tui: {
+11 -10
View File
@@ -6,6 +6,7 @@ import { createToolExecuteAfterHandler } from "./tool-execute-after"
import { createToolExecuteBeforeHandler } from "./tool-execute-before" import { createToolExecuteBeforeHandler } from "./tool-execute-before"
import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants" import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants"
import { clearState, readState, writeState } from "../hooks/ralph-loop/storage" import { clearState, readState, writeState } from "../hooks/ralph-loop/storage"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
describe("tool.execute.before ultrawork oracle verification", () => { describe("tool.execute.before ultrawork oracle verification", () => {
function createCtx(directory: string) { function createCtx(directory: string) {
@@ -56,7 +57,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
}) })
const handler = createToolExecuteBeforeHandler({ const handler = createToolExecuteBeforeHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"],
}) })
const output = { args: createOracleTaskArgs("Check it") } 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`) const directory = join(tmpdir(), `tool-before-ulw-${Date.now()}-plain`)
mkdirSync(directory, { recursive: true }) mkdirSync(directory, { recursive: true })
const handler = createToolExecuteBeforeHandler({ const handler = createToolExecuteBeforeHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"],
}) })
const output = { args: createOracleTaskArgs("Check it") } const output = { args: createOracleTaskArgs("Check it") }
@@ -96,8 +97,8 @@ describe("tool.execute.before ultrawork oracle verification", () => {
mkdirSync(directory, { recursive: true }) mkdirSync(directory, { recursive: true })
const startLoopCalls: Array<{ sessionID: string; prompt: string; options: Record<string, unknown> }> = [] const startLoopCalls: Array<{ sessionID: string; prompt: string; options: Record<string, unknown> }> = []
const handler = createToolExecuteBeforeHandler({ const handler = createToolExecuteBeforeHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
hooks: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"]>({ hooks: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"]>({
ralphLoop: { ralphLoop: {
startLoop: (sessionID: string, prompt: string, options?: Record<string, unknown>) => { startLoop: (sessionID: string, prompt: string, options?: Record<string, unknown>) => {
startLoopCalls.push({ sessionID, prompt, options: options ?? {} }) startLoopCalls.push({ sessionID, prompt, options: options ?? {} })
@@ -148,7 +149,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
}) })
const beforeHandler = createToolExecuteBeforeHandler({ const beforeHandler = createToolExecuteBeforeHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"],
}) })
const beforeOutput = { args: createOracleTaskArgs("Check it") } 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 metadataFromSyncTask = createSyncTaskMetadata(beforeOutput.args, "ses-oracle")
const handler = createToolExecuteAfterHandler({ const handler = createToolExecuteAfterHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
}) })
@@ -191,7 +192,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
}) })
const handler = createToolExecuteAfterHandler({ const handler = createToolExecuteAfterHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
}) })
@@ -230,7 +231,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
}) })
const handler = createToolExecuteAfterHandler({ const handler = createToolExecuteAfterHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
}) })
@@ -269,11 +270,11 @@ describe("tool.execute.before ultrawork oracle verification", () => {
}) })
const beforeHandler = createToolExecuteBeforeHandler({ const beforeHandler = createToolExecuteBeforeHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"],
}) })
const afterHandler = createToolExecuteAfterHandler({ const afterHandler = createToolExecuteAfterHandler({
ctx: testCoerce<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)), ctx: unsafeTestValue<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)),
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"], hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
}) })
+4 -3
View File
@@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
import * as sharedModule from "../shared" import * as sharedModule from "../shared"
import * as dbOverrideModule from "./ultrawork-db-model-override" import * as dbOverrideModule from "./ultrawork-db-model-override"
import * as sessionStateModule from "../features/claude-code-session-state" 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 resolveUltraworkOverride: (typeof import("./ultrawork-model-override"))["resolveUltraworkOverride"]
let detectUltrawork: (typeof import("./ultrawork-model-override"))["detectUltrawork"] let detectUltrawork: (typeof import("./ultrawork-model-override"))["detectUltrawork"]
@@ -70,7 +71,7 @@ describe("resolveUltraworkOverride", () => {
} }
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
return testCoerce<Parameters<typeof resolveUltraworkOverride>[0]>({ return unsafeTestValue<Parameters<typeof resolveUltraworkOverride>[0]>({
agents: { agents: {
[agentName]: { ultrawork }, [agentName]: { ultrawork },
}, },
@@ -139,7 +140,7 @@ describe("resolveUltraworkOverride", () => {
test("should return null when agent has no ultrawork config", () => { test("should return null when agent has no ultrawork config", () => {
//#given //#given
const config = testCoerce<Parameters<typeof resolveUltraworkOverride>[0]>({ const config = unsafeTestValue<Parameters<typeof resolveUltraworkOverride>[0]>({
agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } }, agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } },
}) })
const output = createOutput("ultrawork do something") const output = createOutput("ultrawork do something")
@@ -278,7 +279,7 @@ describe("applyUltraworkModelOverrideOnMessage", () => {
} }
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
return testCoerce<Parameters<typeof applyUltraworkModelOverrideOnMessage>[0]>({ return unsafeTestValue<Parameters<typeof applyUltraworkModelOverrideOnMessage>[0]>({
agents: { agents: {
[agentName]: { ultrawork }, [agentName]: { ultrawork },
}, },
@@ -3,6 +3,7 @@
import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
import * as childProcess from "node:child_process" import * as childProcess from "node:child_process"
import * as fs from "node:fs" import * as fs from "node:fs"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("collectGitDiffStats", () => { describe("collectGitDiffStats", () => {
let execFileSyncSpy: ReturnType<typeof spyOn> let execFileSyncSpy: ReturnType<typeof spyOn>
@@ -52,7 +53,7 @@ describe("collectGitDiffStats", () => {
expect(execSyncSpy).not.toHaveBeenCalled() expect(execSyncSpy).not.toHaveBeenCalled()
expect(execFileSyncSpy.mock.calls.length).toBeGreaterThanOrEqual(3) expect(execFileSyncSpy.mock.calls.length).toBeGreaterThanOrEqual(3)
const calls = testCoerce<Array<[string, string[], { cwd?: string }]>>(execFileSyncSpy.mock.calls) const calls = unsafeTestValue<Array<[string, string[], { cwd?: string }]>>(execFileSyncSpy.mock.calls)
const diffCall = calls.find(([, args]) => args[0] === "diff") const diffCall = calls.find(([, args]) => args[0] === "diff")
const statusCall = calls.find(([, args]) => args[0] === "status") const statusCall = calls.find(([, args]) => args[0] === "status")
const untrackedCall = calls.find(([, args]) => args[0] === "ls-files") const untrackedCall = calls.find(([, args]) => args[0] === "ls-files")
+14 -13
View File
@@ -1,5 +1,6 @@
import { describe, it, expect, mock } from "bun:test" import { describe, it, expect, mock } from "bun:test"
import { parseModelSuggestion, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "./model-suggestion-retry" import { parseModelSuggestion, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "./model-suggestion-retry"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
describe("parseModelSuggestion", () => { describe("parseModelSuggestion", () => {
describe("structured NamedError format", () => { describe("structured NamedError format", () => {
@@ -217,7 +218,7 @@ describe("promptWithModelSuggestionRetry", () => {
const client = { session: { promptAsync: promptMock } } const client = { session: { promptAsync: promptMock } }
// when calling promptWithModelSuggestionRetry // when calling promptWithModelSuggestionRetry
await promptWithModelSuggestionRetry(testCoerce(client), { await promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -244,7 +245,7 @@ describe("promptWithModelSuggestionRetry", () => {
// when calling promptWithModelSuggestionRetry // when calling promptWithModelSuggestionRetry
// then should throw the error without retrying // then should throw the error without retrying
await expect( await expect(
promptWithModelSuggestionRetry(testCoerce(client), { promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
agent: "explore", agent: "explore",
@@ -267,7 +268,7 @@ describe("promptWithModelSuggestionRetry", () => {
// when calling promptWithModelSuggestionRetry // when calling promptWithModelSuggestionRetry
// then should throw the original error // then should throw the original error
await expect( await expect(
promptWithModelSuggestionRetry(testCoerce(client), { promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -288,7 +289,7 @@ describe("promptWithModelSuggestionRetry", () => {
// when calling promptWithModelSuggestionRetry // when calling promptWithModelSuggestionRetry
// then should throw the error // then should throw the error
await expect( await expect(
promptWithModelSuggestionRetry(testCoerce(client), { promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -307,7 +308,7 @@ describe("promptWithModelSuggestionRetry", () => {
const client = { session: { promptAsync: promptMock } } const client = { session: { promptAsync: promptMock } }
// when calling with additional body fields // when calling with additional body fields
await promptWithModelSuggestionRetry(testCoerce(client), { await promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
agent: "explore", agent: "explore",
@@ -341,7 +342,7 @@ describe("promptWithModelSuggestionRetry", () => {
// when calling promptWithModelSuggestionRetry // when calling promptWithModelSuggestionRetry
// then should throw the error // then should throw the error
await expect( await expect(
promptWithModelSuggestionRetry(testCoerce(client), { promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -365,7 +366,7 @@ describe("promptWithModelSuggestionRetry", () => {
// when calling without model in body // when calling without model in body
// then should throw the error // then should throw the error
await expect( await expect(
promptWithModelSuggestionRetry(testCoerce(client), { promptWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -386,7 +387,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
const client = { session: { prompt: promptMock, promptAsync: promptAsyncMock } } const client = { session: { prompt: promptMock, promptAsync: promptAsyncMock } }
// when calling promptSyncWithModelSuggestionRetry // when calling promptSyncWithModelSuggestionRetry
await promptSyncWithModelSuggestionRetry(testCoerce(client), { await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -424,7 +425,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
// when calling with short timeout // when calling with short timeout
// then should abort the request and throw timeout error // then should abort the request and throw timeout error
await expect( await expect(
promptSyncWithModelSuggestionRetry(testCoerce(client), { promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -451,7 +452,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
const client = { session: { prompt: promptMock } } const client = { session: { prompt: promptMock } }
// when calling promptSyncWithModelSuggestionRetry // when calling promptSyncWithModelSuggestionRetry
await promptSyncWithModelSuggestionRetry(testCoerce(client), { await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -477,7 +478,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
// when calling promptSyncWithModelSuggestionRetry // when calling promptSyncWithModelSuggestionRetry
// then should throw the original error // then should throw the original error
await expect( await expect(
promptSyncWithModelSuggestionRetry(testCoerce(client), { promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -504,7 +505,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
// when calling without model in body // when calling without model in body
// then should throw (cannot retry without original model) // then should throw (cannot retry without original model)
await expect( await expect(
promptSyncWithModelSuggestionRetry(testCoerce(client), { promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
parts: [{ type: "text", text: "hello" }], parts: [{ type: "text", text: "hello" }],
@@ -521,7 +522,7 @@ describe("promptSyncWithModelSuggestionRetry", () => {
const client = { session: { prompt: promptMock } } const client = { session: { prompt: promptMock } }
// when calling with additional body fields // when calling with additional body fields
await promptSyncWithModelSuggestionRetry(testCoerce(client), { await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
path: { id: "session-1" }, path: { id: "session-1" },
body: { body: {
agent: "multimodal-looker", agent: "multimodal-looker",
-5
View File
@@ -1,5 +0,0 @@
declare global {
function testCoerce<TValue>(value: TValue): TValue
}
export {}
@@ -6,6 +6,7 @@ import { describe, expect, mock, test } from "bun:test"
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
import { clearPendingStore, consumeToolMetadata } from "../../features/tool-metadata-store" import { clearPendingStore, consumeToolMetadata } from "../../features/tool-metadata-store"
import { createBackgroundTask } from "./create-background-task" import { createBackgroundTask } from "./create-background-task"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode" const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode"
@@ -18,7 +19,7 @@ describe("createBackgroundTask metadata", () => {
// #given // #given
clearPendingStore() clearPendingStore()
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
launch: mock(() => Promise.resolve({ launch: mock(() => Promise.resolve({
id: "task-1", id: "task-1",
sessionID: null, sessionID: null,
@@ -28,7 +29,7 @@ describe("createBackgroundTask metadata", () => {
})), })),
getTask: mock(() => undefined), getTask: mock(() => undefined),
}) })
const client = testCoerce<PluginInput["client"]>({ const client = unsafeTestValue<PluginInput["client"]>({
session: { session: {
messages: mock(() => Promise.resolve({ data: [] })), messages: mock(() => Promise.resolve({ data: [] })),
}, },
@@ -4,6 +4,7 @@ import { describe, test, expect, mock } from "bun:test"
import type { BackgroundManager } from "../../features/background-agent" import type { BackgroundManager } from "../../features/background-agent"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { createBackgroundTask } from "./create-background-task" import { createBackgroundTask } from "./create-background-task"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("createBackgroundTask", () => { describe("createBackgroundTask", () => {
const launchMock = mock(async (): Promise<{ const launchMock = mock(async (): Promise<{
@@ -21,12 +22,12 @@ describe("createBackgroundTask", () => {
})) }))
const getTaskMock = mock() const getTaskMock = mock()
const mockManager = testCoerce<BackgroundManager>({ const mockManager = unsafeTestValue<BackgroundManager>({
launch: launchMock, launch: launchMock,
getTask: getTaskMock, getTask: getTaskMock,
}) })
const mockClient = testCoerce<PluginInput["client"]>({ const mockClient = unsafeTestValue<PluginInput["client"]>({
session: { session: {
messages: mock(() => Promise.resolve({ data: [] })), messages: mock(() => Promise.resolve({ data: [] })),
}, },
+8 -7
View File
@@ -6,6 +6,7 @@ import type { BackgroundManager, BackgroundTask } from "../../features/backgroun
import type { ToolContext } from "@opencode-ai/plugin/tool" import type { ToolContext } from "@opencode-ai/plugin/tool"
import type { BackgroundCancelClient, BackgroundOutputManager, BackgroundOutputClient } from "./tools" import type { BackgroundCancelClient, BackgroundOutputManager, BackgroundOutputClient } from "./tools"
import { consumeToolMetadata, clearPendingStore } from "../../features/tool-metadata-store" 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" const projectDir = "/Users/yeongyu/local-workspaces/oh-my-opencode"
@@ -66,7 +67,7 @@ describe("background_output full_session", () => {
const manager = createMockManager(task) const manager = createMockManager(task)
const client = createMockClient({}) const client = createMockClient({})
const tool = createBackgroundOutput(manager, client) const tool = createBackgroundOutput(manager, client)
const ctxWithCallId = testCoerce<ToolContext>({ const ctxWithCallId = unsafeTestValue<ToolContext>({
...mockContext, ...mockContext,
callID: "call-1", callID: "call-1",
}) })
@@ -93,7 +94,7 @@ describe("background_output full_session", () => {
const manager = createMockManager(task) const manager = createMockManager(task)
const client = createMockClient({}) const client = createMockClient({})
const tool = createBackgroundOutput(manager, client) const tool = createBackgroundOutput(manager, client)
const ctxWithCallId = testCoerce<ToolContext>({ const ctxWithCallId = unsafeTestValue<ToolContext>({
...mockContext, ...mockContext,
callID: "call-1", callID: "call-1",
}) })
@@ -387,7 +388,7 @@ describe("background_cancel", () => {
// #given // #given
const task = createTask({ status: "running" }) const task = createTask({ status: "running" })
const cancelled: string[] = [] const cancelled: string[] = []
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
getTask: (id: string) => (id === task.id ? task : undefined), getTask: (id: string) => (id === task.id ? task : undefined),
getAllDescendantTasks: () => [task], getAllDescendantTasks: () => [task],
cancelTask: async (taskId: string) => { cancelTask: async (taskId: string) => {
@@ -412,7 +413,7 @@ describe("background_cancel", () => {
const taskA = createTask({ id: "task-a", status: "running" }) const taskA = createTask({ id: "task-a", status: "running" })
const taskB = createTask({ id: "task-b", status: "pending" }) const taskB = createTask({ id: "task-b", status: "pending" })
const cancelled: string[] = [] const cancelled: string[] = []
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
getTask: () => undefined, getTask: () => undefined,
getAllDescendantTasks: () => [taskA, taskB], getAllDescendantTasks: () => [taskA, taskB],
cancelTask: async (taskId: string) => { cancelTask: async (taskId: string) => {
@@ -437,7 +438,7 @@ describe("background_cancel", () => {
// #given // #given
const taskA = createTask({ id: "task-a", status: "running", sessionId: "ses-a", description: "running task" }) 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 taskB = createTask({ id: "task-b", status: "pending", sessionId: undefined, description: "pending task" })
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
getTask: () => undefined, getTask: () => undefined,
getAllDescendantTasks: () => [taskA, taskB], getAllDescendantTasks: () => [taskA, taskB],
cancelTask: async (taskId: string) => { cancelTask: async (taskId: string) => {
@@ -461,7 +462,7 @@ describe("background_cancel", () => {
// #given // #given
const task = createTask({ id: "task-1", status: "running" }) const task = createTask({ id: "task-1", status: "running" })
const cancelOptions: Array<{ taskId: string; options: unknown }> = [] const cancelOptions: Array<{ taskId: string; options: unknown }> = []
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
getTask: (id: string) => (id === task.id ? task : undefined), getTask: (id: string) => (id === task.id ? task : undefined),
getAllDescendantTasks: () => [task], getAllDescendantTasks: () => [task],
cancelTask: async (taskId: string, options?: unknown) => { cancelTask: async (taskId: string, options?: unknown) => {
@@ -487,7 +488,7 @@ describe("background_cancel", () => {
// #given // #given
const task = createTask({ id: "task-1", status: "running" }) const task = createTask({ id: "task-1", status: "running" })
const cancelOptions: Array<{ taskId: string; options: unknown }> = [] const cancelOptions: Array<{ taskId: string; options: unknown }> = []
const manager = testCoerce<BackgroundManager>({ const manager = unsafeTestValue<BackgroundManager>({
getTask: (id: string) => (id === task.id ? task : undefined), getTask: (id: string) => (id === task.id ? task : undefined),
getAllDescendantTasks: () => [task], getAllDescendantTasks: () => [task],
cancelTask: async (taskId: string, options?: unknown) => { cancelTask: async (taskId: string, options?: unknown) => {
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { createOrGetSession } from "./session-creator" import { createOrGetSession } from "./session-creator"
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("call-omo-agent createOrGetSession", () => { describe("call-omo-agent createOrGetSession", () => {
test("creates child session without overriding permission and tracks it as subagent session", async () => { test("creates child session without overriding permission and tracks it as subagent session", async () => {
@@ -37,12 +38,12 @@ describe("call-omo-agent createOrGetSession", () => {
} }
// when // when
const result = await createOrGetSession(testCoerce(args), testCoerce(toolContext), testCoerce(ctx)) const result = await createOrGetSession(unsafeTestValue(args), unsafeTestValue(toolContext), unsafeTestValue(ctx))
// then // then
expect(result).toEqual({ sessionID: "ses_child", isNew: true }) expect(result).toEqual({ sessionID: "ses_child", isNew: true })
expect(createCalls).toHaveLength(1) expect(createCalls).toHaveLength(1)
const createBody = (testCoerce(createCalls[0]))?.body const createBody = (unsafeTestValue(createCalls[0]))?.body
expect(createBody?.parentID).toBe("ses_parent") expect(createBody?.parentID).toBe("ses_parent")
expect(createBody?.permission).toBeUndefined() expect(createBody?.permission).toBeUndefined()
expect(subagentSessions.has("ses_child")).toBe(true) expect(subagentSessions.has("ses_child")).toBe(true)
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { resolveOrCreateSessionId } from "./subagent-session-creator" import { resolveOrCreateSessionId } from "./subagent-session-creator"
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state" import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("call-omo-agent resolveOrCreateSessionId", () => { describe("call-omo-agent resolveOrCreateSessionId", () => {
const originalPlatform = process.platform const originalPlatform = process.platform
@@ -19,7 +20,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => {
const { parentDirectory, contextDirectory } = options const { parentDirectory, contextDirectory } = options
const parentSessionData = parentDirectory ? { data: { directory: parentDirectory } } : { data: {} } const parentSessionData = parentDirectory ? { data: { directory: parentDirectory } } : { data: {} }
const ctx = testCoerce<Parameters<typeof resolveOrCreateSessionId>[0]>({ const ctx = unsafeTestValue<Parameters<typeof resolveOrCreateSessionId>[0]>({
directory: contextDirectory, directory: contextDirectory,
client: { client: {
session: { session: {
@@ -1,3 +1,4 @@
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const { describe, test, expect, mock } = require("bun:test") const { describe, test, expect, mock } = require("bun:test")
type ExecuteSync = typeof import("./sync-executor").executeSync type ExecuteSync = typeof import("./sync-executor").executeSync
@@ -389,7 +390,7 @@ describe("executeSync", () => {
} }
//#when //#when
await executeSync(args, toolContext, testCoerce(ctx), deps, undefined, spawnReservation) await executeSync(args, toolContext, unsafeTestValue(ctx), deps, undefined, spawnReservation)
//#then //#then
expect(spawnReservation.commit).toHaveBeenCalledTimes(1) expect(spawnReservation.commit).toHaveBeenCalledTimes(1)
@@ -3,6 +3,7 @@ const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("
import { resolveCategoryExecution } from "./category-resolver" import { resolveCategoryExecution } from "./category-resolver"
import type { ExecutorContext } from "./executor-types" import type { ExecutorContext } from "./executor-types"
import * as connectedProvidersCache from "../../shared/connected-providers-cache" import * as connectedProvidersCache from "../../shared/connected-providers-cache"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("resolveCategoryExecution", () => { describe("resolveCategoryExecution", () => {
let connectedProvidersSpy: ReturnType<typeof spyOn> | undefined let connectedProvidersSpy: ReturnType<typeof spyOn> | undefined
@@ -26,8 +27,8 @@ describe("resolveCategoryExecution", () => {
}) })
const createMockExecutorContext = (): ExecutorContext => ({ const createMockExecutorContext = (): ExecutorContext => ({
client: testCoerce({}), client: unsafeTestValue({}),
manager: testCoerce({}), manager: unsafeTestValue({}),
directory: "/tmp/test", directory: "/tmp/test",
userCategories: {}, userCategories: {},
sisyphusJuniorModel: undefined, sisyphusJuniorModel: undefined,
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
import { executeBackgroundTask } from "./executor" import { executeBackgroundTask } from "./executor"
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("task tool metadata awaiting", () => { describe("task tool metadata awaiting", () => {
test("executeBackgroundTask awaits ctx.metadata before returning", async () => { test("executeBackgroundTask awaits ctx.metadata before returning", async () => {
@@ -28,7 +29,7 @@ describe("task tool metadata awaiting", () => {
subagent_type: "explore", subagent_type: "explore",
} }
const executorCtx = testCoerce({ const executorCtx = unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "task_1", id: "task_1",
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
import type { ParentContext } from "./executor-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 = { providerID: "anthropic", modelID: "claude-sonnet-4-6" }
const MODEL_WITH_VARIANT = { providerID: "google", modelID: "gemini-3.1-pro", variant: "high" } 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", load_skills: [], run_in_background: true, subagent_type: "explore",
} }
await executeBackgroundTask(args, ctx, testCoerce({ await executeBackgroundTask(args, ctx, unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "bg_1", description: "test", agent: "explore", id: "bg_1", description: "test", agent: "explore",
@@ -92,7 +93,7 @@ describe("metadata model unification", () => {
} }
await executeUnstableAgentTask( await executeUnstableAgentTask(
args, ctx, args, ctx,
testCoerce({ unsafeTestValue({
manager: { manager: {
launch: async () => launchedTask, launch: async () => launchedTask,
getTask: () => launchedTask, getTask: () => launchedTask,
@@ -126,7 +127,7 @@ describe("metadata model unification", () => {
load_skills: [], run_in_background: true, task_id: "ses_resumed", load_skills: [], run_in_background: true, task_id: "ses_resumed",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_2", description: "continue", agent: "explore", id: "bg_2", description: "continue", agent: "explore",
@@ -153,7 +154,7 @@ describe("metadata model unification", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -206,7 +207,7 @@ describe("metadata model unification", () => {
load_skills: [], run_in_background: true, subagent_type: "explore", load_skills: [], run_in_background: true, subagent_type: "explore",
} }
await executeBackgroundTask(args, ctx, testCoerce({ await executeBackgroundTask(args, ctx, unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "bg_1", description: "test", agent: "explore", id: "bg_1", description: "test", agent: "explore",
@@ -236,7 +237,7 @@ describe("metadata model unification", () => {
await executeUnstableAgentTask( await executeUnstableAgentTask(
args, ctx, args, ctx,
testCoerce({ unsafeTestValue({
manager: { manager: {
launch: async () => launchedTask, launch: async () => launchedTask,
getTask: () => launchedTask, getTask: () => launchedTask,
@@ -270,7 +271,7 @@ describe("metadata model unification", () => {
load_skills: [], run_in_background: true, task_id: "ses_resumed", load_skills: [], run_in_background: true, task_id: "ses_resumed",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_2", description: "continue", agent: "explore", id: "bg_2", description: "continue", agent: "explore",
@@ -297,7 +298,7 @@ describe("metadata model unification", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ data: [] }), messages: async () => ({ data: [] }),
@@ -381,7 +382,7 @@ describe("metadata model unification", () => {
category: "visual-engineering", load_skills: [], run_in_background: true, subagent_type: "explore", category: "visual-engineering", load_skills: [], run_in_background: true, subagent_type: "explore",
} }
await executeBackgroundTask(args, ctx, testCoerce({ await executeBackgroundTask(args, ctx, unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "bg_variant", description: "test", agent: "explore", id: "bg_variant", description: "test", agent: "explore",
@@ -411,7 +412,7 @@ describe("metadata model unification", () => {
await executeUnstableAgentTask( await executeUnstableAgentTask(
args, ctx, args, ctx,
testCoerce({ unsafeTestValue({
manager: { manager: {
launch: async () => launchedTask, launch: async () => launchedTask,
getTask: () => launchedTask, getTask: () => launchedTask,
@@ -445,7 +446,7 @@ describe("metadata model unification", () => {
load_skills: [], run_in_background: true, task_id: "ses_resumed_variant", load_skills: [], run_in_background: true, task_id: "ses_resumed_variant",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_resume_variant", description: "continue", agent: "explore", id: "bg_resume_variant", description: "continue", agent: "explore",
@@ -472,7 +473,7 @@ describe("metadata model unification", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -2,6 +2,7 @@ const { describe, test, expect } = require("bun:test")
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types" import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
import type { ParentContext } from "./executor-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 = { 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", load_skills: [], run_in_background: true, subagent_type: "explore",
} }
await executeBackgroundTask(args, ctx, testCoerce({ await executeBackgroundTask(args, ctx, unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "bg_abc123", description: "test", agent: "explore", id: "bg_abc123", description: "test", agent: "explore",
@@ -98,7 +99,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
await executeUnstableAgentTask( await executeUnstableAgentTask(
args, ctx, args, ctx,
testCoerce({ unsafeTestValue({
manager: { manager: {
launch: async () => launchedTask, launch: async () => launchedTask,
getTask: () => launchedTask, getTask: () => launchedTask,
@@ -136,7 +137,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
load_skills: [], run_in_background: true, task_id: "ses_resumed_x", load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_resumed_y", description: "continue", agent: "explore", id: "bg_resumed_y", description: "continue", agent: "explore",
@@ -160,7 +161,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
load_skills: [], run_in_background: true, task_id: "ses_resumed_x", load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_resumed_y", description: "continue", agent: "explore", id: "bg_resumed_y", description: "continue", agent: "explore",
@@ -187,7 +188,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
task_id: "ses_resumed_x", task_id: "ses_resumed_x",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_resumed_y", description: "continue", agent: "explore", id: "bg_resumed_y", description: "continue", agent: "explore",
@@ -216,7 +217,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -246,7 +247,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -275,7 +276,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -309,7 +310,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -368,7 +369,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
run_in_background: true, run_in_background: true,
} }
await executeBackgroundTask(args, ctx, testCoerce({ await executeBackgroundTask(args, ctx, unsafeTestValue({
manager: { manager: {
launch: async () => ({ launch: async () => ({
id: "bg_abc123", description: "test", agent: "Sisyphus-Junior", id: "bg_abc123", description: "test", agent: "Sisyphus-Junior",
@@ -402,7 +403,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
await executeUnstableAgentTask( await executeUnstableAgentTask(
args, ctx, args, ctx,
testCoerce({ unsafeTestValue({
manager: { manager: {
launch: async () => launchedTask, launch: async () => launchedTask,
getTask: () => launchedTask, getTask: () => launchedTask,
@@ -438,7 +439,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
load_skills: [], run_in_background: true, task_id: "ses_resume_title", load_skills: [], run_in_background: true, task_id: "ses_resume_title",
} }
await executeBackgroundContinuation(args, ctx, testCoerce({ await executeBackgroundContinuation(args, ctx, unsafeTestValue({
manager: { manager: {
resume: async () => ({ resume: async () => ({
id: "bg_resume_title", description: "continue work", agent: "explore", id: "bg_resume_title", description: "continue work", agent: "explore",
@@ -460,7 +461,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
load_skills: [], run_in_background: false, task_id: "ses_sync_title", load_skills: [], run_in_background: false, task_id: "ses_sync_title",
} }
await executeSyncContinuation(args, ctx, testCoerce({ await executeSyncContinuation(args, ctx, unsafeTestValue({
client: { client: {
session: { session: {
messages: async () => ({ messages: async () => ({
@@ -500,8 +501,8 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
}, },
} }
const bgOutput = createBackgroundOutput(testCoerce(manager), testCoerce(client)) const bgOutput = createBackgroundOutput(unsafeTestValue(manager), unsafeTestValue(client))
await bgOutput.execute(testCoerce({ task_id: "bg_output_xyz" }), testCoerce(ctx)) await bgOutput.execute(unsafeTestValue({ task_id: "bg_output_xyz" }), unsafeTestValue(ctx))
const meta = ctx.captured.find((m: any) => m.metadata?.backgroundTaskId) const meta = ctx.captured.find((m: any) => m.metadata?.backgroundTaskId)
expect(meta).toBeDefined() expect(meta).toBeDefined()
+2 -1
View File
@@ -1,3 +1,4 @@
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
const { describe, expect, test } = require("bun:test") const { describe, expect, test } = require("bun:test")
function requireFresh<T>(modulePath: string): T { function requireFresh<T>(modulePath: string): T {
@@ -18,7 +19,7 @@ function createDelegateTask(...args: Parameters<typeof import("./tools").createD
const toolDefinition = createDelegateTask({ manager: {} as never, client: {} as never, directory: "/tmp/test" }) const toolDefinition = createDelegateTask({ manager: {} as never, client: {} as never, directory: "/tmp/test" })
//#when //#when
const categorySchema = testCoerce<{ const categorySchema = unsafeTestValue<{
def: { def: {
type: string type: string
innerType: { innerType: {
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { executeUnstableAgentTask } from "./unstable-agent-task" import { executeUnstableAgentTask } from "./unstable-agent-task"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("executeUnstableAgentTask session permission", () => { describe("executeUnstableAgentTask session permission", () => {
test("passes question-deny session permission into background launch", async () => { test("passes question-deny session permission into background launch", async () => {
@@ -33,7 +34,7 @@ describe("executeUnstableAgentTask session permission", () => {
metadata: () => {}, metadata: () => {},
abort: new AbortController().signal, abort: new AbortController().signal,
} satisfies Parameters<typeof executeUnstableAgentTask>[1] } satisfies Parameters<typeof executeUnstableAgentTask>[1]
const executorContext = testCoerce<Parameters<typeof executeUnstableAgentTask>[2]>({ const executorContext = unsafeTestValue<Parameters<typeof executeUnstableAgentTask>[2]>({
manager: mockManager, manager: mockManager,
client: { client: {
session: { session: {
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test" import { describe, expect, it } from "bun:test"
import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits" import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("normalizeHashlineEdits", () => { describe("normalizeHashlineEdits", () => {
it("maps replace with pos to replace", () => { it("maps replace with pos to replace", () => {
@@ -51,7 +52,7 @@ describe("normalizeHashlineEdits", () => {
it("rejects legacy payload without op", () => { it("rejects legacy payload without op", () => {
//#given //#given
const input = testCoerce<Parameters< const input = unsafeTestValue<Parameters<
typeof normalizeHashlineEdits typeof normalizeHashlineEdits
>[0]>([{ type: "set_line", line: "2#VK", text: "updated" }]) >[0]>([{ type: "set_line", line: "2#VK", text: "updated" }])
+2 -1
View File
@@ -6,9 +6,10 @@ import { canonicalizeFileText } from "./file-text-canonicalization"
import * as fs from "node:fs" import * as fs from "node:fs"
import * as os from "node:os" import * as os from "node:os"
import * as path from "node:path" import * as path from "node:path"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createMockContext(): ToolContext { function createMockContext(): ToolContext {
return testCoerce<ToolContext>({ return unsafeTestValue<ToolContext>({
sessionID: "test", sessionID: "test",
messageID: "test", messageID: "test",
agent: "test", agent: "test",
@@ -6,6 +6,7 @@ import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadat
import { setVisionCapableModelsCache, clearVisionCapableModelsCache } from "../../shared/vision-capable-models-cache" import { setVisionCapableModelsCache, clearVisionCapableModelsCache } from "../../shared/vision-capable-models-cache"
import * as connectedProvidersCache from "../../shared/connected-providers-cache" import * as connectedProvidersCache from "../../shared/connected-providers-cache"
import * as modelAvailability from "../../shared/model-availability" import * as modelAvailability from "../../shared/model-availability"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
function createPluginInput(agentData: Array<Record<string, unknown>>): PluginInput { function createPluginInput(agentData: Array<Record<string, unknown>>): PluginInput {
const client = {} as PluginInput["client"] const client = {} as PluginInput["client"]
@@ -32,8 +33,8 @@ describe("resolveMultimodalLookerAgentMetadata", () => {
afterEach(() => { afterEach(() => {
clearVisionCapableModelsCache() clearVisionCapableModelsCache()
;(testCoerce<{ mockRestore?: () => void }>(modelAvailability.fetchAvailableModels)).mockRestore?.() ;(unsafeTestValue<{ mockRestore?: () => void }>(modelAvailability.fetchAvailableModels)).mockRestore?.()
;(testCoerce<{ mockRestore?: () => void }>(connectedProvidersCache.readConnectedProvidersCache)).mockRestore?.() ;(unsafeTestValue<{ mockRestore?: () => void }>(connectedProvidersCache.readConnectedProvidersCache)).mockRestore?.()
}) })
test("returns configured multimodal-looker model when it already matches a vision-capable override", async () => { test("returns configured multimodal-looker model when it already matches a vision-capable override", async () => {
+7 -6
View File
@@ -1,5 +1,6 @@
import { describe, expect, test, mock } from "bun:test" import { describe, expect, test, mock } from "bun:test"
import { pollSessionUntilIdle } from "./session-poller" import { pollSessionUntilIdle } from "./session-poller"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type SessionStatusResult = { type SessionStatusResult = {
data?: Record<string, { type: string; attempt?: number; message?: string; next?: number }> data?: Record<string, { type: string; attempt?: number; message?: string; next?: number }>
@@ -30,7 +31,7 @@ describe("pollSessionUntilIdle", () => {
{ data: { ses_test: { type: "idle" } } }, { data: { ses_test: { type: "idle" } } },
]) ])
await pollSessionUntilIdle(testCoerce(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
expect(client.session.status).toHaveBeenCalledTimes(3) expect(client.session.status).toHaveBeenCalledTimes(3)
}) })
@@ -43,7 +44,7 @@ describe("pollSessionUntilIdle", () => {
{ data: {} }, { data: {} },
]) ])
await pollSessionUntilIdle(testCoerce(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
expect(client.session.status).toHaveBeenCalledTimes(1) expect(client.session.status).toHaveBeenCalledTimes(1)
}) })
@@ -57,7 +58,7 @@ describe("pollSessionUntilIdle", () => {
]) ])
await expect( await expect(
pollSessionUntilIdle(testCoerce(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 50 }) pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 50 })
).rejects.toThrow("timed out") ).rejects.toThrow("timed out")
}) })
@@ -69,7 +70,7 @@ describe("pollSessionUntilIdle", () => {
{ error: new Error("API error") }, { error: new Error("API error") },
]) ])
await pollSessionUntilIdle(testCoerce(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
expect(client.session.status).toHaveBeenCalledTimes(1) expect(client.session.status).toHaveBeenCalledTimes(1)
}) })
@@ -85,7 +86,7 @@ describe("pollSessionUntilIdle", () => {
{ data: {} }, { data: {} },
]) ])
await pollSessionUntilIdle(testCoerce(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 }) await pollSessionUntilIdle(unsafeTestValue(client), "ses_test", { pollIntervalMs: 10, timeoutMs: 5000 })
expect(client.session.status).toHaveBeenCalledTimes(4) expect(client.session.status).toHaveBeenCalledTimes(4)
}) })
@@ -98,7 +99,7 @@ describe("pollSessionUntilIdle", () => {
{ data: {} }, { data: {} },
]) ])
await pollSessionUntilIdle(testCoerce(client), "ses_test") await pollSessionUntilIdle(unsafeTestValue(client), "ses_test")
expect(client.session.status).toHaveBeenCalledTimes(1) expect(client.session.status).toHaveBeenCalledTimes(1)
}) })
+21 -20
View File
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test, mock } from "bun:test"
import type { ToolContext } from "@opencode-ai/plugin/tool" import type { ToolContext } from "@opencode-ai/plugin/tool"
import { clearVisionCapableModelsCache, setVisionCapableModelsCache } from "../../shared/vision-capable-models-cache" import { clearVisionCapableModelsCache, setVisionCapableModelsCache } from "../../shared/vision-capable-models-cache"
import { normalizeArgs, validateArgs, createLookAt } from "./tools" import { normalizeArgs, validateArgs, createLookAt } from "./tools"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("look-at tool", () => { describe("look-at tool", () => {
afterEach(() => { afterEach(() => {
@@ -14,7 +15,7 @@ describe("look-at tool", () => {
// then should normalize to file_path // then should normalize to file_path
test("normalizes path to file_path for LLM compatibility", () => { test("normalizes path to file_path for LLM compatibility", () => {
const args = { path: "/some/file.png", goal: "analyze" } const args = { path: "/some/file.png", goal: "analyze" }
const normalized = normalizeArgs(testCoerce(args)) const normalized = normalizeArgs(unsafeTestValue(args))
expect(normalized.file_path).toBe("/some/file.png") expect(normalized.file_path).toBe("/some/file.png")
expect(normalized.goal).toBe("analyze") expect(normalized.goal).toBe("analyze")
}) })
@@ -33,7 +34,7 @@ describe("look-at tool", () => {
// then prefer file_path // then prefer file_path
test("prefers file_path over path when both provided", () => { test("prefers file_path over path when both provided", () => {
const args = { file_path: "/preferred.png", path: "/fallback.png", goal: "test" } const args = { file_path: "/preferred.png", path: "/fallback.png", goal: "test" }
const normalized = normalizeArgs(testCoerce(args)) const normalized = normalizeArgs(unsafeTestValue(args))
expect(normalized.file_path).toBe("/preferred.png") expect(normalized.file_path).toBe("/preferred.png")
}) })
@@ -42,7 +43,7 @@ describe("look-at tool", () => {
// then preserve image_data in normalized args // then preserve image_data in normalized args
test("preserves image_data when provided", () => { test("preserves image_data when provided", () => {
const args = { image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "analyze" } const args = { image_data: "data:image/png;base64,iVBORw0KGgo=", goal: "analyze" }
const normalized = normalizeArgs(testCoerce(args)) const normalized = normalizeArgs(unsafeTestValue(args))
expect(normalized.image_data).toBe("data:image/png;base64,iVBORw0KGgo=") expect(normalized.image_data).toBe("data:image/png;base64,iVBORw0KGgo=")
expect(normalized.file_path).toBeUndefined() expect(normalized.file_path).toBeUndefined()
}) })
@@ -69,7 +70,7 @@ describe("look-at tool", () => {
// when validated // when validated
// then clear error message // then clear error message
test("returns error when neither file_path nor image_data provided", () => { test("returns error when neither file_path nor image_data provided", () => {
const args = testCoerce({ goal: "analyze" }) const args = unsafeTestValue({ goal: "analyze" })
const error = validateArgs(args) const error = validateArgs(args)
expect(error).toContain("file_path") expect(error).toContain("file_path")
expect(error).toContain("image_data") expect(error).toContain("image_data")
@@ -88,7 +89,7 @@ describe("look-at tool", () => {
// when validated // when validated
// then clear error message // then clear error message
test("returns error when goal is missing", () => { test("returns error when goal is missing", () => {
const args = testCoerce({ file_path: "/some/path.png" }) const args = unsafeTestValue({ file_path: "/some/path.png" })
const error = validateArgs(args) const error = validateArgs(args)
expect(error).toContain("goal") expect(error).toContain("goal")
expect(error).toContain("required") expect(error).toContain("required")
@@ -156,7 +157,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -193,7 +194,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -230,7 +231,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -291,7 +292,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -346,7 +347,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -395,7 +396,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -437,7 +438,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -486,7 +487,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -515,7 +516,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -539,7 +540,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -579,7 +580,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -632,7 +633,7 @@ describe("look-at tool", () => {
}, },
} }
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -701,7 +702,7 @@ describe("look-at tool", () => {
test("instructs agent to analyze attached file when Read is disabled (file_path mode)", async () => { test("instructs agent to analyze attached file when Read is disabled (file_path mode)", async () => {
const { mockClient, captured } = captureLastPromptBody() const { mockClient, captured } = captureLastPromptBody()
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -726,7 +727,7 @@ describe("look-at tool", () => {
test("instructs agent to analyze attached image when image_data is provided", async () => { test("instructs agent to analyze attached image when image_data is provided", async () => {
const { mockClient, captured } = captureLastPromptBody() const { mockClient, captured } = captureLastPromptBody()
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
@@ -751,7 +752,7 @@ describe("look-at tool", () => {
test("explicitly warns the agent not to attempt Read when Read is disabled", async () => { test("explicitly warns the agent not to attempt Read when Read is disabled", async () => {
const { mockClient, captured } = captureLastPromptBody() const { mockClient, captured } = captureLastPromptBody()
const tool = createLookAt(testCoerce({ const tool = createLookAt(unsafeTestValue({
client: mockClient, client: mockClient,
directory: "/project", directory: "/project",
})) }))
+3 -2
View File
@@ -16,6 +16,7 @@ afterAll(() => { mock.restore() })
import { LSPClient, lspManager, validateCwd } from "./client" import { LSPClient, lspManager, validateCwd } from "./client"
import type { ResolvedServer } from "./types" import type { ResolvedServer } from "./types"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
describe("LSPClient", () => { describe("LSPClient", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -36,7 +37,7 @@ describe("LSPClient", () => {
const originalSetTimeout = globalThis.setTimeout const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => { globalThis.setTimeout = ((fn: (...args: unknown[]) => void, _ms?: number) => {
fn() fn()
return testCoerce<ReturnType<typeof setTimeout>>(0) return unsafeTestValue<ReturnType<typeof setTimeout>>(0)
}) as typeof setTimeout }) as typeof setTimeout
const server: ResolvedServer = { const server: ResolvedServer = {
@@ -50,7 +51,7 @@ describe("LSPClient", () => {
// Stub protocol output: we only want to assert notifications. // Stub protocol output: we only want to assert notifications.
const sendNotificationSpy = spyOn( const sendNotificationSpy = spyOn(
testCoerce<{ sendNotification: (m: string, p?: unknown) => void }>(client), unsafeTestValue<{ sendNotification: (m: string, p?: unknown) => void }>(client),
"sendNotification" "sendNotification"
) )
+6 -5
View File
@@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from "node:
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { randomUUID } from "node:crypto" 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_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`)
const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message") 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 // Re-import to get fresh module with mocked isSqliteBackend
const { setStorageClient, getMainSessions } = await import("./storage") const { setStorageClient, getMainSessions } = await import("./storage")
setStorageClient(testCoerce<Parameters<typeof setStorageClient>[0]>(mockClient)) setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
// when // when
const sessions = await getMainSessions({ directory: "/test" }) const sessions = await getMainSessions({ directory: "/test" })
@@ -473,7 +474,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
})) }))
const { setStorageClient, getAllSessions } = await import("./storage") const { setStorageClient, getAllSessions } = await import("./storage")
setStorageClient(testCoerce<Parameters<typeof setStorageClient>[0]>(mockClient)) setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
// when // when
const sessionIDs = await getAllSessions() const sessionIDs = await getAllSessions()
@@ -503,7 +504,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
})) }))
const { setStorageClient, readSessionMessages } = await import("./storage") const { setStorageClient, readSessionMessages } = await import("./storage")
setStorageClient(testCoerce<Parameters<typeof setStorageClient>[0]>(mockClient)) setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
// when // when
const messages = await readSessionMessages("ses_test") const messages = await readSessionMessages("ses_test")
@@ -531,7 +532,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
})) }))
const { setStorageClient, readSessionTodos } = await import("./storage") const { setStorageClient, readSessionTodos } = await import("./storage")
setStorageClient(testCoerce<Parameters<typeof setStorageClient>[0]>(mockClient)) setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
// when // when
const todos = await readSessionTodos("ses_test") const todos = await readSessionTodos("ses_test")
@@ -555,7 +556,7 @@ describe("session-manager storage - SDK path (beta mode)", () => {
})) }))
const { setStorageClient, readSessionMessages } = await import("./storage") const { setStorageClient, readSessionMessages } = await import("./storage")
setStorageClient(testCoerce<Parameters<typeof setStorageClient>[0]>(mockClient)) setStorageClient(unsafeTestValue<Parameters<typeof setStorageClient>[0]>(mockClient))
await expect(readSessionMessages("ses_test")).rejects.toThrow("API error") await expect(readSessionMessages("ses_test")).rejects.toThrow("API error")
}) })
@@ -12,6 +12,7 @@ import { clearSkillCache } from "../../../features/opencode-skill-loader/skill-c
import type { LoadedSkill } from "../../../features/opencode-skill-loader/types" import type { LoadedSkill } from "../../../features/opencode-skill-loader/types"
import type { CommandInfo } from "../../slashcommand/types" import type { CommandInfo } from "../../slashcommand/types"
import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js" import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
import { unsafeTestValue } from "../../../../test-support/unsafe-test-value"
const originalReadFileSync = fs.readFileSync.bind(fs) const originalReadFileSync = fs.readFileSync.bind(fs)
@@ -205,7 +206,7 @@ describe("skill tool - agent restriction", () => {
// given // given
const loadedSkills = [createMockSkill("sisyphus-only-skill", { agent: "sisyphus" })] const loadedSkills = [createMockSkill("sisyphus-only-skill", { agent: "sisyphus" })]
const tool = createSkillTool({ skills: loadedSkills }) const tool = createSkillTool({ skills: loadedSkills })
const contextWithoutAgent = { ...mockContext, agent: testCoerce<string>(undefined) } const contextWithoutAgent = { ...mockContext, agent: unsafeTestValue<string>(undefined) }
// when / #then // when / #then
return expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow( return expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow(
-2
View File
@@ -12,8 +12,6 @@ const { restoreModuleMocks } = installModuleMockLifecycle(mock)
let environmentSnapshot: NodeJS.ProcessEnv = { ...process.env } let environmentSnapshot: NodeJS.ProcessEnv = { ...process.env }
let workingDirectorySnapshot = process.cwd() let workingDirectorySnapshot = process.cwd()
globalThis.testCoerce = <TValue>(value: TValue): TValue => value
function cleanupOmoCacheDir(cacheDir: string): void { function cleanupOmoCacheDir(cacheDir: string): void {
rmSync(cacheDir, { recursive: true, force: true }) rmSync(cacheDir, { recursive: true, force: true })
} }

Some files were not shown because too many files have changed in this diff Show More