Merge pull request #3967 from code-yeongyu/refactor/typescript-no-excuse-slops
Remove unsafe TypeScript test assertions
This commit is contained in:
@@ -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,11 +17,11 @@ function makeHangingClient(): {
|
|||||||
hangCount.value += 1
|
hangCount.value += 1
|
||||||
return new Promise<never>(() => {})
|
return new Promise<never>(() => {})
|
||||||
}
|
}
|
||||||
const client = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: sessionGet,
|
get: sessionGet,
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
return { hangCount, client }
|
return { hangCount, client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,12 +14,12 @@ describe("fetchNpmDistTags", () => {
|
|||||||
|
|
||||||
test("returns dist-tags on success", async () => {
|
test("returns dist-tags on success", async () => {
|
||||||
//#given
|
//#given
|
||||||
globalThis.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" }),
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = await fetchNpmDistTags("oh-my-openagent")
|
const result = await fetchNpmDistTags("oh-my-openagent")
|
||||||
@@ -29,7 +30,7 @@ describe("fetchNpmDistTags", () => {
|
|||||||
|
|
||||||
test("returns null on network failure", async () => {
|
test("returns null on network failure", async () => {
|
||||||
//#given
|
//#given
|
||||||
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
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,12 +41,12 @@ describe("fetchNpmDistTags", () => {
|
|||||||
|
|
||||||
test("returns null on non-ok response", async () => {
|
test("returns null on non-ok response", async () => {
|
||||||
//#given
|
//#given
|
||||||
globalThis.fetch = mock(() =>
|
globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 404,
|
status: 404,
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = await fetchNpmDistTags("oh-my-openagent")
|
const result = await fetchNpmDistTags("oh-my-openagent")
|
||||||
|
|||||||
@@ -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,12 +93,12 @@ describe("getOpenCodeVersion (installer)", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const immediateSetTimeout = ((handler: TimerHandler) => {
|
const immediateSetTimeout = unsafeTestValue<typeof globalThis.setTimeout>(((handler: TimerHandler) => {
|
||||||
if (typeof handler === "function") {
|
if (typeof handler === "function") {
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
return 1 as unknown as ReturnType<typeof setTimeout>
|
return unsafeTestValue<ReturnType<typeof setTimeout>>(1)
|
||||||
}) as unknown as typeof globalThis.setTimeout
|
}))
|
||||||
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
|
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
|
||||||
|
|
||||||
const result = await getOpenCodeVersion()
|
const result = await getOpenCodeVersion()
|
||||||
@@ -124,12 +125,12 @@ describe("getOpenCodeVersion (installer)", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const immediateSetTimeout = ((handler: TimerHandler) => {
|
const immediateSetTimeout = unsafeTestValue<typeof globalThis.setTimeout>(((handler: TimerHandler) => {
|
||||||
if (typeof handler === "function") {
|
if (typeof handler === "function") {
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
return 1 as unknown as ReturnType<typeof setTimeout>
|
return unsafeTestValue<ReturnType<typeof setTimeout>>(1)
|
||||||
}) as unknown as typeof globalThis.setTimeout
|
}))
|
||||||
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
|
const setTimeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(immediateSetTimeout)
|
||||||
|
|
||||||
const result = await getOpenCodeVersion()
|
const result = await getOpenCodeVersion()
|
||||||
|
|||||||
@@ -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,12 +14,12 @@ 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 = 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" }),
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = await getPluginNameWithVersion("3.13.1")
|
const result = await getPluginNameWithVersion("3.13.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 = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
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,12 +41,12 @@ 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 = mock(() =>
|
globalThis.fetch = unsafeTestValue<typeof fetch>(mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 404,
|
status: 404,
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = await getPluginNameWithVersion("3.13.1")
|
const result = await getPluginNameWithVersion("3.13.1")
|
||||||
|
|||||||
@@ -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,12 +58,12 @@ 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 = 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" }),
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
const args: InstallArgs = {
|
const args: InstallArgs = {
|
||||||
tui: false,
|
tui: false,
|
||||||
@@ -92,12 +93,12 @@ 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 = 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" }),
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
const args: InstallArgs = {
|
const args: InstallArgs = {
|
||||||
tui: false,
|
tui: false,
|
||||||
@@ -131,12 +132,12 @@ 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 = 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" }),
|
||||||
} as Response)
|
} as Response)
|
||||||
) as unknown as typeof fetch
|
))
|
||||||
|
|
||||||
const args: InstallArgs = {
|
const args: InstallArgs = {
|
||||||
tui: false,
|
tui: false,
|
||||||
|
|||||||
@@ -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: {
|
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: [] })),
|
||||||
@@ -39,7 +40,7 @@ function createMockContext(directory: string): RunContext {
|
|||||||
})),
|
})),
|
||||||
messages: mock(async () => ({ data: [] })),
|
messages: mock(async () => ({ data: [] })),
|
||||||
},
|
},
|
||||||
} as unknown as RunContext["client"],
|
}),
|
||||||
sessionID: "test-session",
|
sessionID: "test-session",
|
||||||
directory,
|
directory,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
@@ -155,17 +156,17 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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" } }]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.client.session.messages = mock(async () => ({ data: [] })) as unknown as RunContext["client"]["session"]["messages"]
|
ctx.client.session.messages = unsafeTestValue<RunContext["client"]["session"]["messages"]>(mock(async () => ({ data: [] })))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -218,17 +219,17 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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" } }]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -253,17 +254,17 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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" } }]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -288,14 +289,14 @@ 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 = 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")
|
||||||
}) as unknown as RunContext["client"]["session"]["get"]
|
}))
|
||||||
ctx.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" } }]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -317,12 +318,12 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -347,12 +348,12 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -374,9 +375,9 @@ 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 = mock(async () => {
|
ctx.client.session.get = unsafeTestValue<RunContext["client"]["session"]["get"]>(mock(async () => {
|
||||||
throw new Error("lineage unavailable")
|
throw new Error("lineage unavailable")
|
||||||
}) as unknown as RunContext["client"]["session"]["get"]
|
}))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -401,17 +402,17 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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" } }]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -437,20 +438,20 @@ 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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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" } },
|
||||||
{ info: { agent: "compaction", providerID: "openai", modelID: "gpt-5.4" } },
|
{ info: { agent: "compaction", providerID: "openai", modelID: "gpt-5.4" } },
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.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 } } },
|
||||||
@@ -486,7 +487,7 @@ describe("checkCompletionConditions continuation coverage", () => {
|
|||||||
{ id: "msg_0002", info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } },
|
{ id: "msg_0002", info: { agent: "sisyphus-junior", providerID: "openai", modelID: "gpt-5.4", time: { created: 100 } } },
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
})) as unknown as RunContext["client"]["session"]["messages"]
|
})))
|
||||||
|
|
||||||
const { checkCompletionConditions } = await import("./completion")
|
const { checkCompletionConditions } = await import("./completion")
|
||||||
|
|
||||||
@@ -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 = 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,
|
||||||
},
|
},
|
||||||
})) as unknown as RunContext["client"]["session"]["get"]
|
})))
|
||||||
ctx.client.session.messages = mock(async () => ({ data: [] })) as unknown as RunContext["client"]["session"]["messages"]
|
ctx.client.session.messages = unsafeTestValue<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: {
|
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 } }) =>
|
||||||
@@ -21,7 +22,7 @@ const createMockContext = (overrides: {
|
|||||||
),
|
),
|
||||||
status: mock(() => Promise.resolve({ data: statuses })),
|
status: mock(() => Promise.resolve({ data: statuses })),
|
||||||
},
|
},
|
||||||
} as unknown as RunContext["client"],
|
}),
|
||||||
sessionID: "test-session",
|
sessionID: "test-session",
|
||||||
directory: "/test",
|
directory: "/test",
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
|
|||||||
@@ -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: {
|
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 } }) =>
|
||||||
@@ -21,7 +22,7 @@ const createMockContext = (overrides: {
|
|||||||
),
|
),
|
||||||
status: mock(() => Promise.resolve({ data: statuses })),
|
status: mock(() => Promise.resolve({ data: statuses })),
|
||||||
},
|
},
|
||||||
} as unknown as RunContext["client"],
|
}),
|
||||||
sessionID: "test-session",
|
sessionID: "test-session",
|
||||||
directory: "/test",
|
directory: "/test",
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
|
|||||||
@@ -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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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, payload as any, 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,
|
||||||
{
|
unsafeTestValue({
|
||||||
type: "message.updated",
|
type: "message.updated",
|
||||||
properties: {
|
properties: {
|
||||||
info: {
|
info: {
|
||||||
@@ -254,7 +255,7 @@ describe("handleMessagePartUpdated", () => {
|
|||||||
modelID: "claude-sonnet-4-6",
|
modelID: "claude-sonnet-4-6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
state.messageStartedAtById["msg_1"] = 1000
|
state.messageStartedAtById["msg_1"] = 1000
|
||||||
@@ -262,7 +263,7 @@ describe("handleMessagePartUpdated", () => {
|
|||||||
// when
|
// when
|
||||||
handleMessagePartUpdated(
|
handleMessagePartUpdated(
|
||||||
ctx,
|
ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
type: "message.part.updated",
|
type: "message.part.updated",
|
||||||
properties: {
|
properties: {
|
||||||
part: {
|
part: {
|
||||||
@@ -274,13 +275,13 @@ describe("handleMessagePartUpdated", () => {
|
|||||||
time: { end: 1 },
|
time: { end: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
|
|
||||||
handleMessagePartUpdated(
|
handleMessagePartUpdated(
|
||||||
ctx,
|
ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
type: "message.part.updated",
|
type: "message.part.updated",
|
||||||
properties: {
|
properties: {
|
||||||
part: {
|
part: {
|
||||||
@@ -292,7 +293,7 @@ describe("handleMessagePartUpdated", () => {
|
|||||||
time: { end: 2 },
|
time: { end: 2 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,7 +324,7 @@ describe("handleTuiToast", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
handleTuiToast(ctx, payload as any, 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, payload as any, state)
|
handleTuiToast(ctx, unsafeTestValue(payload), state)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(state.mainSessionError).toBe(false)
|
expect(state.mainSessionError).toBe(false)
|
||||||
|
|||||||
@@ -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,14 +57,14 @@ function createMockWriteStream(): MockWriteStream {
|
|||||||
|
|
||||||
const createMockClient = (
|
const createMockClient = (
|
||||||
getResult?: { error?: unknown; data?: { id: string } }
|
getResult?: { error?: unknown; data?: { id: string } }
|
||||||
): 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 } })
|
||||||
),
|
),
|
||||||
create: mock(() => Promise.resolve({ data: { id: "new-session-id" } })),
|
create: mock(() => Promise.resolve({ data: { id: "new-session-id" } })),
|
||||||
},
|
},
|
||||||
} as unknown as OpencodeClient)
|
}))
|
||||||
|
|
||||||
describe("integration: --json mode", () => {
|
describe("integration: --json mode", () => {
|
||||||
it("emits valid RunResult JSON to stdout", () => {
|
it("emits valid RunResult JSON to stdout", () => {
|
||||||
@@ -78,8 +79,8 @@ describe("integration: --json mode", () => {
|
|||||||
summary: "Test summary",
|
summary: "Test summary",
|
||||||
}
|
}
|
||||||
const manager = createJsonOutputManager({
|
const manager = createJsonOutputManager({
|
||||||
stdout: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
|
||||||
})
|
})
|
||||||
jsonManager.redirectToStderr()
|
jsonManager.redirectToStderr()
|
||||||
spawnSpy.mockClear()
|
spawnSpy.mockClear()
|
||||||
|
|||||||
@@ -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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -126,8 +127,8 @@ describe("createJsonOutputManager", () => {
|
|||||||
summary: "Test",
|
summary: "Test",
|
||||||
}
|
}
|
||||||
const manager = createJsonOutputManager({
|
const manager = createJsonOutputManager({
|
||||||
stdout: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
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: mockStdout as unknown as NodeJS.WriteStream,
|
stdout: unsafeTestValue<NodeJS.WriteStream>(mockStdout),
|
||||||
stderr: mockStderr as unknown as NodeJS.WriteStream,
|
stderr: unsafeTestValue<NodeJS.WriteStream>(mockStderr),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
|||||||
@@ -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: {
|
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 } }) =>
|
||||||
@@ -23,7 +24,7 @@ const createMockContext = (overrides: {
|
|||||||
),
|
),
|
||||||
status: mock(() => Promise.resolve({ data: statuses })),
|
status: mock(() => Promise.resolve({ data: statuses })),
|
||||||
},
|
},
|
||||||
} as unknown as RunContext["client"],
|
}),
|
||||||
sessionID: "test-session",
|
sessionID: "test-session",
|
||||||
directory: "/test",
|
directory: "/test",
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
@@ -124,7 +125,7 @@ describe("pollForCompletion", () => {
|
|||||||
let todoCallCount = 0
|
let todoCallCount = 0
|
||||||
let busyInserted = false
|
let busyInserted = false
|
||||||
|
|
||||||
;(ctx.client.session as any).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: [] }
|
||||||
})
|
})
|
||||||
;(ctx.client.session as any).children = mock(() =>
|
;(unsafeTestValue(ctx.client.session)).children = mock(() =>
|
||||||
Promise.resolve({ data: [] })
|
Promise.resolve({ data: [] })
|
||||||
)
|
)
|
||||||
;(ctx.client.session as any).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
|
||||||
|
|
||||||
;(ctx.client.session as any).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: [] }
|
||||||
})
|
})
|
||||||
;(ctx.client.session as any).children = mock(() =>
|
;(unsafeTestValue(ctx.client.session)).children = mock(() =>
|
||||||
Promise.resolve({ data: [] })
|
Promise.resolve({ data: [] })
|
||||||
)
|
)
|
||||||
;(ctx.client.session as any).status = mock(() =>
|
;(unsafeTestValue(ctx.client.session)).status = mock(() =>
|
||||||
Promise.resolve({ data: {} })
|
Promise.resolve({ data: {} })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
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 } })
|
||||||
@@ -22,7 +23,7 @@ const createMockClient = (overrides: {
|
|||||||
return Promise.resolve(result)
|
return Promise.resolve(result)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as unknown as OpencodeClient
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("resolveSession", () => {
|
describe("resolveSession", () => {
|
||||||
|
|||||||
@@ -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(stdout as unknown as NodeJS.WriteStream)
|
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(stdout as unknown as NodeJS.WriteStream)
|
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(stdout as unknown as NodeJS.WriteStream)
|
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: undefined as unknown as string,
|
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: undefined as unknown as string, status: "completed" },
|
{ id: "bg_abc123", description: unsafeTestValue<string>(undefined), status: "completed" },
|
||||||
{ id: "bg_def456", description: undefined as unknown as string, 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: undefined as unknown as string,
|
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(null as unknown as string)
|
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,12 +17,12 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const manager = new BackgroundManager({ pluginContext: { client, directory: tmpdir() } as unknown as PluginInput, config: config })
|
const manager = new BackgroundManager({ pluginContext: unsafeTestValue<PluginInput>({ client, directory: tmpdir() }), config: config })
|
||||||
const testManager = manager as unknown as {
|
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>
|
||||||
}
|
}>(manager)
|
||||||
|
|
||||||
testManager.enqueueNotificationForParent = async (_sessionId: string, fn) => {
|
testManager.enqueueNotificationForParent = async (_sessionId: string, fn) => {
|
||||||
await fn()
|
await fn()
|
||||||
@@ -32,7 +33,7 @@ function createManager(config?: BackgroundTaskConfig): BackgroundManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> {
|
function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> {
|
||||||
return (manager as unknown as { tasks: Map<string, BackgroundTask> }).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: { client, directory } as unknown as PluginInput })
|
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: { client, directory: tmpdir() } as unknown as PluginInput })
|
const manager = new BackgroundManager({ pluginContext: unsafeTestValue<PluginInput>({ client, directory: tmpdir() }) })
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await manager.launch({
|
await manager.launch({
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ 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 = {
|
const client = unsafeTestValue<OpencodeClient>({
|
||||||
session: {
|
session: {
|
||||||
get,
|
get,
|
||||||
},
|
},
|
||||||
} as unknown as OpencodeClient
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await verifySessionExists(client, "session-123", "/project/root")
|
const result = await verifySessionExists(client, "session-123", "/project/root")
|
||||||
|
|||||||
@@ -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,14 +21,14 @@ 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((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" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: { id: "root-session", parentID: undefined } }
|
return { data: { id: "root-session", parentID: undefined } }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await resolveSubagentSpawnContext(client, "child-session", "/project/root")
|
const result = await resolveSubagentSpawnContext(client, "child-session", "/project/root")
|
||||||
@@ -50,10 +51,10 @@ 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((async () => ({
|
const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async () => ({
|
||||||
error: "lookup failed",
|
error: "lookup failed",
|
||||||
data: undefined,
|
data: undefined,
|
||||||
})) as unknown as OpencodeClient["session"]["get"])
|
}))))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = resolveSubagentSpawnContext(client, "parent-session")
|
const result = resolveSubagentSpawnContext(client, "parent-session")
|
||||||
@@ -66,9 +67,9 @@ 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((async () => ({
|
const client = createMockClient(unsafeTestValue<OpencodeClient["session"]["get"]>((async () => ({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
})) as unknown as OpencodeClient["session"]["get"])
|
}))))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = resolveSubagentSpawnContext(client, "parent-session")
|
const result = resolveSubagentSpawnContext(client, "parent-session")
|
||||||
@@ -81,12 +82,12 @@ 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((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 } }
|
||||||
}
|
}
|
||||||
return { error: "not found", data: undefined }
|
return { error: "not found", data: undefined }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await resolveSubagentSpawnContext(client, "root-session")
|
const result = await resolveSubagentSpawnContext(client, "root-session")
|
||||||
@@ -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((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" } }
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,7 @@ describe("resolveSubagentSpawnContext", () => {
|
|||||||
return { data: { id: "root-session", parentID: undefined } }
|
return { data: { id: "root-session", parentID: undefined } }
|
||||||
}
|
}
|
||||||
return { error: "not found", data: undefined }
|
return { error: "not found", data: undefined }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await resolveSubagentSpawnContext(client, "child-1")
|
const result = await resolveSubagentSpawnContext(client, "child-1")
|
||||||
@@ -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((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" },
|
||||||
@@ -129,7 +130,7 @@ describe("resolveSubagentSpawnContext", () => {
|
|||||||
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 }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await resolveSubagentSpawnContext(client, "grandchild")
|
const result = await resolveSubagentSpawnContext(client, "grandchild")
|
||||||
@@ -153,11 +154,11 @@ describe("resolveSubagentSpawnContext", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createMockClient((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 }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when - resolve from the deepest session
|
// when - resolve from the deepest session
|
||||||
const deepest = `session-${DEFAULT_MAX_SUBAGENT_DEPTH}`
|
const deepest = `session-${DEFAULT_MAX_SUBAGENT_DEPTH}`
|
||||||
@@ -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((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" },
|
||||||
@@ -178,7 +179,7 @@ describe("resolveSubagentSpawnContext", () => {
|
|||||||
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 }
|
||||||
}) as unknown as OpencodeClient["session"]["get"])
|
})))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = resolveSubagentSpawnContext(client, "session-a")
|
const result = resolveSubagentSpawnContext(client, "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 = { messages } as any
|
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 = { messages } as any
|
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 = { messages } as any
|
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 = { messages } as any
|
const output = unsafeTestValue({ messages })
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await hook["experimental.chat.messages.transform"]!({}, output)
|
await hook["experimental.chat.messages.transform"]!({}, output)
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ type MessagesTransformHook = {
|
|||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSessionIDFromMessageInfo(info: Message): string | undefined {
|
||||||
|
return "sessionID" in info && typeof info.sessionID === "string" ? info.sessionID : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasText(part: Part): boolean {
|
||||||
|
return "text" in part && typeof part.text === "string" && part.text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export function createContextInjectorMessagesTransformHook(
|
export function createContextInjectorMessagesTransformHook(
|
||||||
collector: ContextCollector
|
collector: ContextCollector
|
||||||
): MessagesTransformHook {
|
): MessagesTransformHook {
|
||||||
@@ -106,8 +114,7 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastUserMessage = messages[lastUserMessageIndex]
|
const lastUserMessage = messages[lastUserMessageIndex]
|
||||||
// Try message.info.sessionID first, fallback to mainSessionID
|
const messageSessionID = getSessionIDFromMessageInfo(lastUserMessage.info)
|
||||||
const messageSessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID
|
|
||||||
const sessionID = messageSessionID ?? getMainSessionID()
|
const sessionID = messageSessionID ?? getMainSessionID()
|
||||||
log("[DEBUG] Extracted sessionID", {
|
log("[DEBUG] Extracted sessionID", {
|
||||||
messageSessionID,
|
messageSessionID,
|
||||||
@@ -135,7 +142,7 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const textPartIndex = lastUserMessage.parts.findIndex(
|
const textPartIndex = lastUserMessage.parts.findIndex(
|
||||||
(p) => p.type === "text" && (p as { text?: string }).text
|
(p) => p.type === "text" && hasText(p)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (textPartIndex === -1) {
|
if (textPartIndex === -1) {
|
||||||
@@ -150,7 +157,7 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
const syntheticPart = {
|
const syntheticPart = {
|
||||||
id: `synthetic_hook_${sessionID}`,
|
id: `synthetic_hook_${sessionID}`,
|
||||||
messageID: lastUserMessage.info.id,
|
messageID: lastUserMessage.info.id,
|
||||||
sessionID: (lastUserMessage.info as { sessionID?: string }).sessionID ?? "",
|
sessionID: messageSessionID ?? "",
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: pending.merged,
|
text: pending.merged,
|
||||||
synthetic: true, // hidden in UI
|
synthetic: true, // hidden in UI
|
||||||
|
|||||||
@@ -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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(mockClient as any, "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(manager as any, "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(manager as any, "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(manager as any, "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(manager as any, "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(manager as any, "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,15 +21,15 @@ describe("TaskToastManager", () => {
|
|||||||
showToast: mock(() => Promise.resolve()),
|
showToast: mock(() => Promise.resolve()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mockConcurrencyManager = {
|
mockConcurrencyManager = unsafeTestValue<ConcurrencyManager>({
|
||||||
getConcurrencyLimit: mock(() => 5),
|
getConcurrencyLimit: mock(() => 5),
|
||||||
} as unknown as ConcurrencyManager
|
})
|
||||||
|
|
||||||
const mod = await import("./manager")
|
const mod = await import("./manager")
|
||||||
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(mockClient as any, 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 = {
|
const mockConcurrencyWithCounts = unsafeTestValue<ConcurrencyManager>({
|
||||||
getConcurrencyLimit: mock(() => 5),
|
getConcurrencyLimit: mock(() => 5),
|
||||||
getRunningCount: mock(() => 2),
|
getRunningCount: mock(() => 2),
|
||||||
getQueuedCount: mock(() => 1),
|
getQueuedCount: mock(() => 1),
|
||||||
} as unknown as ConcurrencyManager
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const managerWithConcurrency = new TaskToastManager(mockClient as any, mockConcurrencyWithCounts)
|
const managerWithConcurrency = new TaskToastManager(unsafeTestValue(mockClient), mockConcurrencyWithCounts)
|
||||||
|
|
||||||
// when - a task is added
|
// 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 = {
|
const limitedConcurrency = unsafeTestValue<ConcurrencyManager>({
|
||||||
getConcurrencyLimit: mock(() => 1),
|
getConcurrencyLimit: mock(() => 1),
|
||||||
} as unknown as ConcurrencyManager
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const limitedManager = new TaskToastManager(mockClient as any, limitedConcurrency)
|
const limitedManager = new TaskToastManager(unsafeTestValue(mockClient), limitedConcurrency)
|
||||||
|
|
||||||
limitedManager.addTask({
|
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,9 +42,9 @@ function createRuntimeState(teamRunId: string): RuntimeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createStubBgMgr(): BackgroundManager {
|
function createStubBgMgr(): BackgroundManager {
|
||||||
return {
|
return unsafeTestValue<BackgroundManager>({
|
||||||
cancelTask: async () => undefined,
|
cancelTask: async () => undefined,
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("cleanupTeamRunResources", () => {
|
describe("cleanupTeamRunResources", () => {
|
||||||
|
|||||||
@@ -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(
|
||||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
|
||||||
sessions,
|
sessions,
|
||||||
async () => {},
|
async () => {},
|
||||||
)
|
)
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const firstPoll = (manager as unknown as { pollSessions: () => Promise<void> }).pollSessions()
|
const firstPoll = (unsafeTestValue<{ pollSessions: () => Promise<void> }>(manager)).pollSessions()
|
||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
const secondPoll = (manager as unknown as { pollSessions: () => Promise<void> }).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(
|
||||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
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 = (manager as unknown as { pollSessions: () => Promise<void> }).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(
|
||||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
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 = (manager as unknown as { pollSessions: () => Promise<void> }).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(
|
||||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
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 = (manager as unknown as { pollSessions: () => Promise<void> }).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(
|
||||||
client as unknown as import("../../tools/delegate-task/types").OpencodeClient,
|
unsafeTestValue<import("../../tools/delegate-task/types").OpencodeClient>(client),
|
||||||
sessions,
|
sessions,
|
||||||
async (sessionId) => {
|
async (sessionId) => {
|
||||||
closedSessionIds.push(sessionId)
|
closedSessionIds.push(sessionId)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const pollSessions = (manager as unknown as { pollSessions: () => Promise<void> }).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 id as unknown as ReturnType<typeof setTimeout>
|
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 = (mockClient.tui.showToast as any).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 = (mockClient.tui.showToast as any).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 0 as unknown as ReturnType<typeof setTimeout>
|
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 = timeoutCalls.length + 1 as unknown as ReturnType<typeof setTimeout>
|
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 1 as unknown as ReturnType<typeof setTimeout>
|
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,
|
||||||
1 as unknown as ReturnType<typeof setTimeout>,
|
unsafeTestValue<ReturnType<typeof setTimeout>>(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
|
|||||||
@@ -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 id as unknown as ReturnType<typeof setTimeout>
|
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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -128,13 +129,13 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
},
|
}>({
|
||||||
|
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -169,13 +170,13 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
},
|
}>({
|
||||||
|
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -212,9 +213,11 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
|
}>({
|
||||||
getTasksByParentSession: () => {
|
getTasksByParentSession: () => {
|
||||||
if (remainingRunningRetries > 0) {
|
if (remainingRunningRetries > 0) {
|
||||||
remainingRunningRetries -= 1
|
remainingRunningRetries -= 1
|
||||||
@@ -223,9 +226,7 @@ describe("atlas background task retry", () => {
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
}),
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -266,9 +267,11 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
|
}>({
|
||||||
getTasksByParentSession: () => {
|
getTasksByParentSession: () => {
|
||||||
backgroundCheckCount += 1
|
backgroundCheckCount += 1
|
||||||
if (backgroundCheckCount === 1) {
|
if (backgroundCheckCount === 1) {
|
||||||
@@ -281,9 +284,7 @@ describe("atlas background task retry", () => {
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
}),
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -321,13 +322,13 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
},
|
}>({
|
||||||
|
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -384,18 +385,18 @@ describe("atlas background task retry", () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
|
}>({
|
||||||
getTasksByParentSession: (currentSessionID: string) => {
|
getTasksByParentSession: (currentSessionID: string) => {
|
||||||
if (currentSessionID !== descendantSessionID) {
|
if (currentSessionID !== descendantSessionID) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return backgroundRunning ? [{ status: "running" }] : []
|
return backgroundRunning ? [{ status: "running" }] : []
|
||||||
},
|
},
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
}),
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -432,7 +433,7 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput)
|
}))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const firstIdle = hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
const firstIdle = hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
@@ -462,7 +463,7 @@ describe("atlas background task retry", () => {
|
|||||||
promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise)
|
promptAsyncMock.mockImplementationOnce(() => deferredPrompt.promise)
|
||||||
promptAsyncMock.mockImplementationOnce(async () => ({}))
|
promptAsyncMock.mockImplementationOnce(async () => ({}))
|
||||||
|
|
||||||
const hook = createAtlasHook({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -470,13 +471,13 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
getTasksByParentSession: () => [],
|
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
},
|
}>({
|
||||||
|
getTasksByParentSession: () => [],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -515,7 +516,7 @@ describe("atlas background task retry", () => {
|
|||||||
})
|
})
|
||||||
promptAsyncMock.mockImplementationOnce(async () => ({}))
|
promptAsyncMock.mockImplementationOnce(async () => ({}))
|
||||||
|
|
||||||
const hook = createAtlasHook({
|
const hook = createAtlasHook(unsafeTestValue<PluginInput>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -523,13 +524,13 @@ describe("atlas background task retry", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput, {
|
}), {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
||||||
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"] & {
|
|
||||||
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
getTasksByParentSession: (sessionID: string) => Array<{ status: string }>
|
||||||
},
|
}>({
|
||||||
|
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
|||||||
@@ -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 = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -28,7 +29,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await injectBoulderContinuation({
|
const result = await injectBoulderContinuation({
|
||||||
@@ -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 = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -68,7 +69,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await injectBoulderContinuation({
|
const result = await injectBoulderContinuation({
|
||||||
@@ -78,9 +79,9 @@ describe("injectBoulderContinuation", () => {
|
|||||||
remaining: 1,
|
remaining: 1,
|
||||||
total: 2,
|
total: 2,
|
||||||
agent: "atlas",
|
agent: "atlas",
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({
|
||||||
getTasksByParentSession: () => [{ status: "running" }],
|
getTasksByParentSession: () => [{ status: "running" }],
|
||||||
} as unknown as Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"],
|
}),
|
||||||
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 = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -106,7 +107,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await injectBoulderContinuation({
|
const result = await injectBoulderContinuation({
|
||||||
@@ -116,9 +117,9 @@ describe("injectBoulderContinuation", () => {
|
|||||||
remaining: 1,
|
remaining: 1,
|
||||||
total: 2,
|
total: 2,
|
||||||
agent: "atlas",
|
agent: "atlas",
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"]>({
|
||||||
getTasksByParentSession: () => [{ status: "pending" }],
|
getTasksByParentSession: () => [{ status: "pending" }],
|
||||||
} as unknown as Parameters<typeof injectBoulderContinuation>[0]["backgroundManager"],
|
}),
|
||||||
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 = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -142,7 +143,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await injectBoulderContinuation({
|
const result = await injectBoulderContinuation({
|
||||||
@@ -189,7 +190,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
}],
|
}],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const ctx = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -197,7 +198,7 @@ describe("injectBoulderContinuation", () => {
|
|||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await injectBoulderContinuation({
|
const result = await injectBoulderContinuation({
|
||||||
|
|||||||
@@ -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({
|
const hook = createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDirectory,
|
directory: testDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -59,7 +60,7 @@ describe("atlas hook idle-event complete boulder", () => {
|
|||||||
promptAsync: async () => ({ data: {} }),
|
promptAsync: async () => ({ data: {} }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0])
|
}))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await hook.handler({
|
await hook.handler({
|
||||||
|
|||||||
@@ -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({
|
return createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDirectory,
|
directory: testDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -52,7 +53,7 @@ describe("atlas hook idle-event session lineage", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0])
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -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({
|
return createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDirectory,
|
directory: testDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -79,7 +80,7 @@ describe("atlas hook idle-event persisted lineage", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0])
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -173,7 +174,7 @@ describe("atlas hook idle-event persisted lineage", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const hook = createAtlasHook({
|
const hook = createAtlasHook(unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDirectory,
|
directory: testDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -193,7 +194,7 @@ describe("atlas hook idle-event persisted lineage", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0])
|
}))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await hook.handler({
|
await hook.handler({
|
||||||
|
|||||||
@@ -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,14 +77,14 @@ describe("handleAtlasSessionIdle completion nudge", () => {
|
|||||||
return { data: {} }
|
return { data: {} }
|
||||||
})
|
})
|
||||||
|
|
||||||
const ctx = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
directory: testDirectory,
|
directory: testDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
promptAsync: promptAsyncMock,
|
promptAsync: promptAsyncMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
const sessionStateById = new Map<string, SessionState>()
|
const sessionStateById = new Map<string, SessionState>()
|
||||||
const getState = (sessionId: string): SessionState => {
|
const getState = (sessionId: string): SessionState => {
|
||||||
|
|||||||
@@ -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 = {
|
const ctx = unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: mock(async () => ({
|
messages: mock(async () => ({
|
||||||
@@ -32,7 +33,7 @@ describe("resolveRecentPromptContextForSession", () => {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await resolveRecentPromptContextForSession(ctx, "ses_123")
|
const result = await resolveRecentPromptContextForSession(ctx, "ses_123")
|
||||||
|
|||||||
@@ -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,11 +81,11 @@ 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 = {
|
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]),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
if (parentSessionIDs) {
|
if (parentSessionIDs) {
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
@@ -141,11 +142,11 @@ 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 = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: async () => createSessionGetResult(undefined),
|
get: async () => createSessionGetResult(undefined),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined),
|
createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined),
|
||||||
@@ -215,11 +216,11 @@ 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 = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: async () => createSessionGetResult(undefined),
|
get: async () => createSessionGetResult(undefined),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
spyOn(client.session, "get").mockImplementation((input) => {
|
spyOn(client.session, "get").mockImplementation((input) => {
|
||||||
if (input?.path?.id === childSessionID) {
|
if (input?.path?.id === childSessionID) {
|
||||||
@@ -288,11 +289,11 @@ 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 = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: async () => createSessionGetResult(undefined),
|
get: async () => createSessionGetResult(undefined),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
createSessionGetResult(input?.path?.id === childSessionID ? "ses_unrelated_parent" : undefined),
|
createSessionGetResult(input?.path?.id === childSessionID ? "ses_unrelated_parent" : undefined),
|
||||||
@@ -358,11 +359,11 @@ 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 = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: async () => createSessionGetResult(undefined),
|
get: async () => createSessionGetResult(undefined),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined),
|
createSessionGetResult(input?.path?.id === childSessionID ? sessionID : undefined),
|
||||||
@@ -431,11 +432,11 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
|
|||||||
const planPathA = join(testDirectory, "background-launch-work-a.md")
|
const 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 = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
get: async () => createSessionGetResult(undefined),
|
get: async () => createSessionGetResult(undefined),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
createSessionGetResult(input?.path?.id === childSessionID ? parentSessionID : undefined),
|
createSessionGetResult(input?.path?.id === childSessionID ? parentSessionID : 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 = (current as unknown as Record<PropertyKey, unknown>)[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,13 +22,13 @@ describe("category-skill-reminder hook", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createMockPluginInput() {
|
function createMockPluginInput() {
|
||||||
return {
|
return unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => {},
|
showToast: async () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHook(availableSkills: AvailableSkill[] = []) {
|
function createHook(availableSkills: AvailableSkill[] = []) {
|
||||||
|
|||||||
@@ -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 = mockFetch as unknown as typeof fetch
|
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 = mockFetch as unknown as typeof fetch
|
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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
const [url, options] = unsafeTestValue<[string, RequestInit]>(mockFetch.mock.calls[0])
|
||||||
expect(url).toBe("http://localhost:8080/hooks/pre-tool-use")
|
expect(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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
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] = mockFetch.mock.calls[0] as unknown as [string, RequestInit]
|
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 = { unref: mock(() => {}) } as unknown as ReturnType<typeof setInterval>
|
const intervalHandle = unsafeTestValue<ReturnType<typeof setInterval>>({ unref: mock(() => {}) })
|
||||||
const setIntervalMock = mock(() => intervalHandle)
|
const setIntervalMock = mock(() => intervalHandle)
|
||||||
const clearIntervalMock = mock(() => {})
|
const clearIntervalMock = mock(() => {})
|
||||||
globalThis.setInterval = setIntervalMock as unknown as typeof setInterval
|
globalThis.setInterval = unsafeTestValue<typeof setInterval>(setIntervalMock)
|
||||||
globalThis.clearInterval = clearIntervalMock as unknown as typeof clearInterval
|
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`)
|
||||||
|
|||||||
@@ -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 0 as unknown as ReturnType<typeof setTimeout>
|
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 0 as unknown as ReturnType<typeof setTimeout>
|
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,18 +8,18 @@ describe("pending-calls cleanup interval", () => {
|
|||||||
const setIntervalCalls: number[] = []
|
const setIntervalCalls: number[] = []
|
||||||
let unrefCalled = 0
|
let unrefCalled = 0
|
||||||
|
|
||||||
globalThis.setInterval = ((
|
globalThis.setInterval = unsafeTestValue<typeof setInterval>(((
|
||||||
_handler: TimerHandler,
|
_handler: TimerHandler,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
..._args: any[]
|
..._args: unknown[]
|
||||||
) => {
|
) => {
|
||||||
setIntervalCalls.push(timeout as number)
|
setIntervalCalls.push(timeout as number)
|
||||||
return {
|
return unsafeTestValue<ReturnType<typeof setInterval>>({
|
||||||
unref: () => {
|
unref: () => {
|
||||||
unrefCalled += 1
|
unrefCalled += 1
|
||||||
},
|
},
|
||||||
} as unknown as ReturnType<typeof setInterval>
|
})
|
||||||
}) as unknown as typeof setInterval
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname
|
const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname
|
||||||
@@ -43,20 +44,20 @@ 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 = ((
|
globalThis.setInterval = unsafeTestValue<typeof setInterval>(((
|
||||||
_handler: TimerHandler,
|
_handler: TimerHandler,
|
||||||
_timeout?: number,
|
_timeout?: number,
|
||||||
..._args: any[]
|
..._args: unknown[]
|
||||||
) => {
|
) => {
|
||||||
intervalHandle = { unref: () => {} } as unknown as ReturnType<typeof setInterval>
|
intervalHandle = unsafeTestValue<ReturnType<typeof setInterval>>({ unref: () => {} })
|
||||||
return intervalHandle
|
return intervalHandle
|
||||||
}) as unknown as typeof setInterval
|
}))
|
||||||
|
|
||||||
globalThis.clearInterval = ((handle?: ReturnType<typeof setInterval>) => {
|
globalThis.clearInterval = unsafeTestValue<typeof clearInterval>(((handle?: ReturnType<typeof setInterval>) => {
|
||||||
if (handle === intervalHandle) {
|
if (handle === intervalHandle) {
|
||||||
clearCalls += 1
|
clearCalls += 1
|
||||||
}
|
}
|
||||||
}) as unknown as typeof clearInterval
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname
|
const modulePath = new URL("./pending-calls.ts", import.meta.url).pathname
|
||||||
|
|||||||
@@ -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({} as any)
|
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: undefined as unknown as string,
|
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,13 +12,13 @@ type StartLoopCall = {
|
|||||||
type CancelLoopCall = { sessionID: string }
|
type CancelLoopCall = { sessionID: string }
|
||||||
|
|
||||||
function createMockPluginInput() {
|
function createMockPluginInput() {
|
||||||
return {
|
return unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => {},
|
showToast: async () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockRalphLoop(startLoopCalls: StartLoopCall[], cancelLoopCalls: CancelLoopCall[] = []) {
|
function createMockRalphLoop(startLoopCalls: StartLoopCall[], cancelLoopCalls: CancelLoopCall[] = []) {
|
||||||
|
|||||||
@@ -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 {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async (opts: { body: { title: string } }) => {
|
showToast: async (opts: { body: { title: string } }) => {
|
||||||
@@ -30,7 +31,7 @@ describe("keyword-detector hyperplan-ultrawork combo", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should inject combo message when user types 'hpp ulw' (forward order)", async () => {
|
test("should inject combo message when user types 'hpp ulw' (forward order)", async () => {
|
||||||
|
|||||||
@@ -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,13 +882,13 @@ describe("keyword-detector team mode", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createMockPluginInput() {
|
function createMockPluginInput() {
|
||||||
return {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => {},
|
showToast: async () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should inject team-mode message when user types 'team mode'", async () => {
|
test("should inject team-mode message when user types 'team mode'", async () => {
|
||||||
@@ -1063,7 +1064,7 @@ describe("keyword-detector disabled_keywords config", () => {
|
|||||||
|
|
||||||
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
|
function createMockPluginInput(options: { toastCalls?: string[] } = {}) {
|
||||||
const toastCalls = options.toastCalls ?? []
|
const toastCalls = options.toastCalls ?? []
|
||||||
return {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async (opts: { body: { title: string } }) => {
|
showToast: async (opts: { body: { title: string } }) => {
|
||||||
@@ -1071,7 +1072,7 @@ describe("keyword-detector disabled_keywords config", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should NOT inject search-mode when disabled_keywords includes 'search'", async () => {
|
test("should NOT inject search-mode when disabled_keywords includes 'search'", async () => {
|
||||||
|
|||||||
@@ -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 {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async (opts: { body: { title: string } }) => {
|
showToast: async (opts: { body: { title: string } }) => {
|
||||||
@@ -19,7 +20,7 @@ function createMockPluginInput(toastCalls: string[] = []) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockRalphLoop(startLoopCalls: StartLoopCall[]) {
|
function createMockRalphLoop(startLoopCalls: StartLoopCall[]) {
|
||||||
|
|||||||
@@ -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 {
|
return unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async (opts: { body: { message: string } }) => {
|
showToast: async (opts: { body: { message: string } }) => {
|
||||||
@@ -11,7 +12,7 @@ function createMockPluginInput(toastMessages: string[]) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("keyword-detector ultrawork runtime variant gating", () => {
|
describe("keyword-detector ultrawork runtime variant gating", () => {
|
||||||
|
|||||||
@@ -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,12 +87,12 @@ 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 = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
const set = setPendingModelFallback(
|
const set = setPendingModelFallback(
|
||||||
modelFallback,
|
modelFallback,
|
||||||
@@ -122,12 +123,12 @@ 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 = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
const sessionID = "ses_model_fallback_main"
|
const sessionID = "ses_model_fallback_main"
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -212,12 +213,12 @@ 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 = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
setSessionFallbackChain(modelFallback, sessionID, [
|
setSessionFallbackChain(modelFallback, sessionID, [
|
||||||
{ providers: ["anthropic"], model: "claude-opus-4-7" },
|
{ providers: ["anthropic"], model: "claude-opus-4-7" },
|
||||||
@@ -254,12 +255,12 @@ 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 = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
setSessionFallbackChain(modelFallback, sessionID, [
|
setSessionFallbackChain(modelFallback, sessionID, [
|
||||||
{ providers: ["quotio"], model: "claude-opus-4-7", variant: "max" },
|
{ providers: ["quotio"], model: "claude-opus-4-7", variant: "max" },
|
||||||
@@ -299,12 +300,12 @@ describe("model fallback hook", () => {
|
|||||||
clearPendingModelFallback(modelFallback, sessionID)
|
clearPendingModelFallback(modelFallback, sessionID)
|
||||||
readConnectedProvidersCacheMock.mockReturnValue(["provider-x"])
|
readConnectedProvidersCacheMock.mockReturnValue(["provider-x"])
|
||||||
|
|
||||||
const hook = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
setSessionFallbackChain(modelFallback, sessionID, [
|
setSessionFallbackChain(modelFallback, sessionID, [
|
||||||
{ providers: ["provider-y"], model: "fallback-model" },
|
{ providers: ["provider-y"], model: "fallback-model" },
|
||||||
@@ -355,16 +356,16 @@ 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 = createModelFallbackHook({
|
const hook = unsafeTestValue<{
|
||||||
toast: async ({ title, message }) => {
|
|
||||||
toastCalls.push({ title, message })
|
|
||||||
},
|
|
||||||
}) as unknown as {
|
|
||||||
"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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(createModelFallbackHook({
|
||||||
|
toast: async ({ title, message }) => {
|
||||||
|
toastCalls.push({ title, message })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
const set = setPendingModelFallback(
|
const set = setPendingModelFallback(
|
||||||
hook,
|
hook,
|
||||||
@@ -393,12 +394,12 @@ describe("model fallback hook", () => {
|
|||||||
const sessionID = "ses_model_fallback_ghcp"
|
const sessionID = "ses_model_fallback_ghcp"
|
||||||
clearPendingModelFallback(modelFallback, sessionID)
|
clearPendingModelFallback(modelFallback, sessionID)
|
||||||
|
|
||||||
const hook = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
setSessionFallbackChain(modelFallback, sessionID, [
|
setSessionFallbackChain(modelFallback, sessionID, [
|
||||||
{ providers: ["github-copilot"], model: "claude-sonnet-4-6" },
|
{ providers: ["github-copilot"], model: "claude-sonnet-4-6" },
|
||||||
@@ -434,12 +435,12 @@ describe("model fallback hook", () => {
|
|||||||
const sessionID = "ses_model_fallback_google"
|
const sessionID = "ses_model_fallback_google"
|
||||||
clearPendingModelFallback(modelFallback, sessionID)
|
clearPendingModelFallback(modelFallback, sessionID)
|
||||||
|
|
||||||
const hook = modelFallback as unknown as {
|
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 }> },
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}>(modelFallback)
|
||||||
|
|
||||||
setSessionFallbackChain(modelFallback, sessionID, [
|
setSessionFallbackChain(modelFallback, sessionID, [
|
||||||
{ providers: ["google"], model: "gemini-3.1-pro-preview" },
|
{ providers: ["google"], model: "gemini-3.1-pro-preview" },
|
||||||
|
|||||||
@@ -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,9 +20,9 @@ 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({
|
const hook = createNoHephaestusNonGptHook(unsafeTestValue({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as any)
|
}))
|
||||||
|
|
||||||
const output1 = createOutput()
|
const output1 = createOutput()
|
||||||
const output2 = createOutput()
|
const output2 = createOutput()
|
||||||
@@ -54,9 +55,9 @@ describe("no-hephaestus-non-gpt hook", () => {
|
|||||||
test("shows warning and does not switch agent when allow_non_gpt_model is enabled", async () => {
|
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({
|
const hook = createNoHephaestusNonGptHook(unsafeTestValue({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as any, {
|
}), {
|
||||||
allowNonGptModel: true,
|
allowNonGptModel: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,9 +84,9 @@ 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({
|
const hook = createNoHephaestusNonGptHook(unsafeTestValue({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as any)
|
}))
|
||||||
|
|
||||||
const output = createOutput()
|
const output = createOutput()
|
||||||
|
|
||||||
@@ -104,9 +105,9 @@ 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({
|
const hook = createNoHephaestusNonGptHook(unsafeTestValue({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as any)
|
}))
|
||||||
|
|
||||||
const output = createOutput()
|
const output = createOutput()
|
||||||
|
|
||||||
@@ -127,9 +128,9 @@ 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({
|
const hook = createNoHephaestusNonGptHook(unsafeTestValue({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as any)
|
}))
|
||||||
|
|
||||||
const output = createOutput()
|
const output = createOutput()
|
||||||
|
|
||||||
|
|||||||
@@ -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,9 +23,9 @@ function createOutput(): HookOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createHookContext(showToast: (input: unknown) => Promise<unknown>): PluginInput {
|
function createHookContext(showToast: (input: unknown) => Promise<unknown>): PluginInput {
|
||||||
return {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: { tui: { showToast } },
|
client: { tui: { showToast } },
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("no-sisyphus-gpt hook", () => {
|
describe("no-sisyphus-gpt hook", () => {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ function truncateQuestionLabels(args: AskUserQuestionArgs): AskUserQuestionArgs
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasQuestions(args: Record<string, unknown>): args is Record<string, unknown> & AskUserQuestionArgs {
|
||||||
|
return Array.isArray(args.questions);
|
||||||
|
}
|
||||||
|
|
||||||
export function createQuestionLabelTruncatorHook() {
|
export function createQuestionLabelTruncatorHook() {
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (
|
"tool.execute.before": async (
|
||||||
@@ -50,10 +54,8 @@ export function createQuestionLabelTruncatorHook() {
|
|||||||
const toolName = input.tool?.toLowerCase();
|
const toolName = input.tool?.toLowerCase();
|
||||||
|
|
||||||
if (toolName === "askuserquestion" || toolName === "ask_user_question") {
|
if (toolName === "askuserquestion" || toolName === "ask_user_question") {
|
||||||
const args = output.args as unknown as AskUserQuestionArgs | undefined;
|
if (hasQuestions(output.args)) {
|
||||||
|
const truncatedArgs = truncateQuestionLabels(output.args);
|
||||||
if (args?.questions) {
|
|
||||||
const truncatedArgs = truncateQuestionLabels(args);
|
|
||||||
Object.assign(output.args, truncatedArgs);
|
Object.assign(output.args, truncatedArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]?.(input as any, output as any);
|
await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const truncatedLabel = (output.args as any).questions[0].options[0].label;
|
const truncatedLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
|
||||||
expect(truncatedLabel.length).toBeLessThanOrEqual(30);
|
expect(truncatedLabel.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"]?.(input as any, output as any);
|
await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const resultLabel = (output.args as any).questions[0].options[0].label;
|
const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
|
||||||
expect(resultLabel).toBe(shortLabel);
|
expect(resultLabel).toBe(shortLabel);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,10 +75,10 @@ describe("createQuestionLabelTruncatorHook", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const resultLabel = (output.args as any).questions[0].options[0].label;
|
const resultLabel = (unsafeTestValue(output.args)).questions[0].options[0].label;
|
||||||
expect(resultLabel).toBe(exactLabel);
|
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"]?.(input as any, output as any);
|
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"]?.(input as any, output as any);
|
await hook["tool.execute.before"]?.(unsafeTestValue(input), unsafeTestValue(output));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const q1opts = (output.args as any).questions[0].options;
|
const q1opts = (unsafeTestValue(output.args)).questions[0].options;
|
||||||
const q2opts = (output.args as any).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);
|
||||||
|
|||||||
+3
-2
@@ -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,8 +17,8 @@ export function createPluginInput(messages: SessionMessage[]): PluginInput {
|
|||||||
$: {} as PluginInput["$"],
|
$: {} as PluginInput["$"],
|
||||||
} as PluginInput
|
} as PluginInput
|
||||||
|
|
||||||
pluginInput.client.session.messages =
|
const messagesFunction = unsafeTestValue<PluginInput["client"]["session"]["messages"]>(async () => ({ data: messages }))
|
||||||
(async () => ({ data: messages })) as unknown as PluginInput["client"]["session"]["messages"]
|
pluginInput.client.session.messages = messagesFunction
|
||||||
|
|
||||||
return pluginInput
|
return pluginInput
|
||||||
}
|
}
|
||||||
@@ -1,7 +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 { detectCompletionInSessionMessages } from "./completion-promise-detector"
|
import { detectCompletionInSessionMessages } from "./completion-promise-detector"
|
||||||
import { createPluginInput } from "./completion-promise-detector-test-input"
|
import { createPluginInput } from "./completion-promise-detector-test-input.test"
|
||||||
|
|
||||||
describe("detectCompletionInSessionMessages", () => {
|
describe("detectCompletionInSessionMessages", () => {
|
||||||
describe("#given session with prior DONE and new messages", () => {
|
describe("#given session with prior DONE and new messages", () => {
|
||||||
|
|||||||
@@ -1,7 +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 { detectCompletionInSessionMessages } from "./completion-promise-detector"
|
import { detectCompletionInSessionMessages } from "./completion-promise-detector"
|
||||||
import { createPluginInput } from "./completion-promise-detector-test-input"
|
import { createPluginInput } from "./completion-promise-detector-test-input.test"
|
||||||
|
|
||||||
describe("detectCompletionInSessionMessages negative cases", () => {
|
describe("detectCompletionInSessionMessages negative cases", () => {
|
||||||
describe("#given natural language completion text without explicit promise", () => {
|
describe("#given natural language completion text without explicit promise", () => {
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
unsafeTestValue<Parameters<typeof createRalphLoopHook>[0]>({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -86,7 +87,7 @@ describe("ralph-loop reset strategy race condition", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createRalphLoopHook>[0],
|
}),
|
||||||
{ idleSettleMs: 0 },
|
{ idleSettleMs: 0 },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
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 }> } }) => {
|
||||||
@@ -39,7 +40,7 @@ describe("ulw-loop verification", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
} as unknown as Parameters<typeof createRalphLoopHook>[0]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -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,13 +13,13 @@ 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 = {
|
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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = getFallbackModelsForSession(sessionID, undefined, pluginConfig)
|
const result = getFallbackModelsForSession(sessionID, undefined, pluginConfig)
|
||||||
@@ -29,13 +30,13 @@ describe("runtime-fallback fallback-models", () => {
|
|||||||
|
|
||||||
test("uses agent-specific fallback_models when agent is resolved", () => {
|
test("uses agent-specific fallback_models when agent is resolved", () => {
|
||||||
//#given
|
//#given
|
||||||
const pluginConfig = {
|
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"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = getFallbackModelsForSession("ses_runtime_fallback_agent", "oracle", pluginConfig)
|
const result = getFallbackModelsForSession("ses_runtime_fallback_agent", "oracle", pluginConfig)
|
||||||
@@ -46,7 +47,7 @@ describe("runtime-fallback fallback-models", () => {
|
|||||||
|
|
||||||
test("does not fall back to another agent chain when agent cannot be resolved", () => {
|
test("does not fall back to another agent chain when agent cannot be resolved", () => {
|
||||||
//#given
|
//#given
|
||||||
const pluginConfig = {
|
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"],
|
||||||
@@ -55,7 +56,7 @@ describe("runtime-fallback fallback-models", () => {
|
|||||||
fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"],
|
fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-7"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = getFallbackModelsForSession("ses_runtime_fallback_unknown", undefined, pluginConfig)
|
const result = getFallbackModelsForSession("ses_runtime_fallback_unknown", undefined, pluginConfig)
|
||||||
|
|||||||
@@ -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 {
|
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 } }) => {
|
||||||
@@ -59,7 +60,7 @@ describe("runtime-fallback", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
directory: "/test/dir",
|
directory: "/test/dir",
|
||||||
} as any
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockConfig(overrides?: Partial<RuntimeFallbackConfig>): RuntimeFallbackConfig {
|
function createMockConfig(overrides?: Partial<RuntimeFallbackConfig>): RuntimeFallbackConfig {
|
||||||
|
|||||||
@@ -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 = {
|
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 }
|
||||||
@@ -95,7 +96,7 @@ describe("session-notification-sender", () => {
|
|||||||
promise.nothrow = () => promise
|
promise.nothrow = () => promise
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
||||||
|
|
||||||
@@ -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 = {
|
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 }
|
||||||
@@ -130,7 +131,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
||||||
|
|
||||||
@@ -142,9 +143,9 @@ 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 = {
|
const mockCtx = unsafeTestValue<PluginInput>({
|
||||||
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
|
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
||||||
|
|
||||||
@@ -157,9 +158,9 @@ describe("session-notification-sender", () => {
|
|||||||
test("#then should fall back to terminal-notifier when cmux fails", async () => {
|
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 = {
|
const mockCtx = unsafeTestValue<PluginInput>({
|
||||||
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")),
|
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify")),
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
const originalFactory = mockCtx.$
|
const originalFactory = mockCtx.$
|
||||||
const trackingCalls: string[] = []
|
const trackingCalls: string[] = []
|
||||||
@@ -180,9 +181,9 @@ 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 = {
|
const mockCtx = unsafeTestValue<PluginInput>({
|
||||||
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")),
|
$: createThrowingShellPromise((cmdStr) => cmdStr.includes("cmux notify") || cmdStr.includes("terminal-notifier")),
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
const originalFactory = mockCtx.$
|
const originalFactory = mockCtx.$
|
||||||
mockCtx.$ = ((cmd: TemplateStringsArray, ...values: unknown[]) => {
|
mockCtx.$ = ((cmd: TemplateStringsArray, ...values: unknown[]) => {
|
||||||
@@ -200,9 +201,9 @@ describe("session-notification-sender", () => {
|
|||||||
|
|
||||||
test("#then should skip cmux when not available and use terminal-notifier", async () => {
|
test("#then should skip cmux when not available and use terminal-notifier", async () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const mockCtx = {
|
const mockCtx = unsafeTestValue<PluginInput>({
|
||||||
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
|
$: createShellPromise((cmdStr) => { calls.push(cmdStr) }),
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "darwin", "Test", "Message")
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ describe("session-notification-sender", () => {
|
|||||||
|
|
||||||
test("#then should call .quiet() on linux notify-send", async () => {
|
test("#then should call .quiet() on linux notify-send", async () => {
|
||||||
const quietCalls: string[] = []
|
const quietCalls: string[] = []
|
||||||
const mockCtx = {
|
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 }
|
||||||
@@ -236,7 +237,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "linux", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "linux", "Test", "Message")
|
||||||
|
|
||||||
@@ -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 = {
|
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 }
|
||||||
@@ -269,7 +270,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.sendSessionNotification(mockCtx, "win32", "Test", "Message")
|
await sender.sendSessionNotification(mockCtx, "win32", "Test", "Message")
|
||||||
|
|
||||||
@@ -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 = {
|
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 }
|
||||||
@@ -306,7 +307,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.playSessionNotificationSound(mockCtx, "darwin", "/sound.aiff")
|
await sender.playSessionNotificationSound(mockCtx, "darwin", "/sound.aiff")
|
||||||
|
|
||||||
@@ -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 = {
|
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 }
|
||||||
@@ -339,7 +340,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga")
|
await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga")
|
||||||
|
|
||||||
@@ -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 = {
|
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 }
|
||||||
@@ -374,7 +375,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga")
|
await sender.playSessionNotificationSound(mockCtx, "linux", "/sound.oga")
|
||||||
|
|
||||||
@@ -384,7 +385,7 @@ describe("session-notification-sender", () => {
|
|||||||
|
|
||||||
test("#then should call .quiet() on win32 powershell sound", async () => {
|
test("#then should call .quiet() on win32 powershell sound", async () => {
|
||||||
const quietCalls: string[] = []
|
const quietCalls: string[] = []
|
||||||
const mockCtx = {
|
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 }
|
||||||
@@ -407,7 +408,7 @@ describe("session-notification-sender", () => {
|
|||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput
|
})
|
||||||
|
|
||||||
await sender.playSessionNotificationSound(mockCtx, "win32", "C:\\sound.wav")
|
await sender.playSessionNotificationSound(mockCtx, "win32", "C:\\sound.wav")
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ type ClientWithPromptAsync = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasPromptAsync(client: Client): client is Client & ClientWithPromptAsync {
|
||||||
|
return "promptAsync" in client.session && typeof client.session.promptAsync === "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ToolUsePart {
|
interface ToolUsePart {
|
||||||
type: "tool_use"
|
type: "tool_use"
|
||||||
@@ -111,7 +115,11 @@ export async function recoverToolResultMissing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (client as unknown as ClientWithPromptAsync).session.promptAsync(promptInput)
|
if (!hasPromptAsync(client)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.session.promptAsync(promptInput)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -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 {
|
return unsafeTestValue({
|
||||||
session: {
|
session: {
|
||||||
messages: async (opts: { path: { id: string } }) => {
|
messages: async (opts: { path: { id: string } }) => {
|
||||||
if (handlers.messages) {
|
if (handlers.messages) {
|
||||||
@@ -28,7 +29,7 @@ function createMockClient(handlers: {
|
|||||||
throw new Error("not implemented")
|
throw new Error("not implemented")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("session-recovery storage SDK readers", () => {
|
describe("session-recovery storage SDK readers", () => {
|
||||||
|
|||||||
@@ -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 = {
|
const ctx = unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -747,7 +748,7 @@ You are starting a Sisyphus work session.
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0]
|
})
|
||||||
const startWorkHook = createStartWorkHook(ctx)
|
const startWorkHook = createStartWorkHook(ctx)
|
||||||
const atlasHook = createAtlasHook(ctx)
|
const atlasHook = createAtlasHook(ctx)
|
||||||
const output = {
|
const output = {
|
||||||
@@ -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 = ((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 id as unknown as ReturnType<typeof setTimeout>
|
return unsafeTestValue<ReturnType<typeof setTimeout>>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalSetTimeout(callback as Parameters<typeof originalSetTimeout>[0], delay)
|
return originalSetTimeout(callback as Parameters<typeof originalSetTimeout>[0], delay)
|
||||||
}) as unknown as typeof setTimeout
|
}))
|
||||||
|
|
||||||
globalThis.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)
|
||||||
@@ -803,11 +804,11 @@ You are starting a Sisyphus work session.
|
|||||||
}
|
}
|
||||||
|
|
||||||
originalClearTimeout(id as Parameters<typeof originalClearTimeout>[0])
|
originalClearTimeout(id as Parameters<typeof originalClearTimeout>[0])
|
||||||
}) as unknown as typeof clearTimeout
|
}))
|
||||||
|
|
||||||
Date.now = () => fakeNow
|
Date.now = () => fakeNow
|
||||||
|
|
||||||
const ctx = {
|
const ctx = unsafeTestValue<Parameters<typeof createAtlasHook>[0]>({
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -816,13 +817,13 @@ You are starting a Sisyphus work session.
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createAtlasHook>[0]
|
})
|
||||||
const startWorkHook = createStartWorkHook(ctx)
|
const startWorkHook = createStartWorkHook(ctx)
|
||||||
const atlasHook = createAtlasHook(ctx, {
|
const atlasHook = createAtlasHook(ctx, {
|
||||||
directory: testDir,
|
directory: testDir,
|
||||||
backgroundManager: {
|
backgroundManager: unsafeTestValue<NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"]>({
|
||||||
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
getTasksByParentSession: () => backgroundRunning ? [{ status: "running" }] : [],
|
||||||
} as unknown as NonNullable<Parameters<typeof createAtlasHook>[1]>["backgroundManager"],
|
}),
|
||||||
})
|
})
|
||||||
const output = {
|
const output = {
|
||||||
message: {} as Record<string, unknown>,
|
message: {} as Record<string, unknown>,
|
||||||
|
|||||||
@@ -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,14 +32,14 @@ describe("stop-continuation-guard", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createMockPluginInput() {
|
function createMockPluginInput() {
|
||||||
return {
|
return unsafeTestValue<PluginInput>({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => ({}),
|
showToast: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
directory: createTempDir(),
|
directory: createTempDir(),
|
||||||
} as unknown as PluginInput
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackgroundTask(status: BackgroundTask["status"], id: string): BackgroundTask {
|
function createBackgroundTask(status: BackgroundTask["status"], id: string): BackgroundTask {
|
||||||
|
|||||||
@@ -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: undefined as unknown as string,
|
output: unsafeTestValue<string>(undefined),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
const config: OpenClawConfig = unsafeTestValue({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
gateways: {
|
gateways: {
|
||||||
discord: {
|
discord: {
|
||||||
@@ -20,7 +21,7 @@ describe("OpenClaw Config", () => {
|
|||||||
instruction: "Started session {{sessionId}}",
|
instruction: "Started session {{sessionId}}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
|
|
||||||
const resolved = resolveGateway(config, "session-start")
|
const resolved = resolveGateway(config, "session-start")
|
||||||
expect(resolved).not.toBeNull()
|
expect(resolved).not.toBeNull()
|
||||||
@@ -30,31 +31,31 @@ describe("OpenClaw Config", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("resolveGateway returns null for disabled config", () => {
|
test("resolveGateway returns null for disabled config", () => {
|
||||||
const config: OpenClawConfig = {
|
const config: OpenClawConfig = unsafeTestValue({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
gateways: {},
|
gateways: {},
|
||||||
hooks: {},
|
hooks: {},
|
||||||
} as any
|
})
|
||||||
expect(resolveGateway(config, "session-start")).toBeNull()
|
expect(resolveGateway(config, "session-start")).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("resolveGateway returns null for unknown hook", () => {
|
test("resolveGateway returns null for unknown hook", () => {
|
||||||
const config: OpenClawConfig = {
|
const config: OpenClawConfig = unsafeTestValue({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
gateways: {},
|
gateways: {},
|
||||||
hooks: {},
|
hooks: {},
|
||||||
} as any
|
})
|
||||||
expect(resolveGateway(config, "unknown")).toBeNull()
|
expect(resolveGateway(config, "unknown")).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("resolveGateway returns null for disabled hook", () => {
|
test("resolveGateway returns null for disabled hook", () => {
|
||||||
const config: OpenClawConfig = {
|
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: {
|
||||||
event: { enabled: false, gateway: "g", instruction: "i" },
|
event: { enabled: false, gateway: "g", instruction: "i" },
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
expect(resolveGateway(config, "event")).toBeNull()
|
expect(resolveGateway(config, "event")).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 = fetchMock as unknown as typeof fetch
|
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 = fetchMock as unknown as typeof fetch
|
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",
|
||||||
|
|||||||
@@ -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, "createBuiltinAgents" as any).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, "loadUserCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, unsafeTestValue("loadUserCommands")).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, unsafeTestValue("loadProjectCommands")).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, unsafeTestValue("loadOpencodeGlobalCommands")).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadOpencodeProjectCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, unsafeTestValue("loadOpencodeProjectCommands")).mockResolvedValue({})
|
||||||
|
|
||||||
spyOn(builtinCommands, "loadBuiltinCommands" as any).mockReturnValue({})
|
spyOn(builtinCommands, unsafeTestValue("loadBuiltinCommands")).mockReturnValue({})
|
||||||
|
|
||||||
spyOn(skillLoader, "loadUserSkills" as any).mockResolvedValue({})
|
spyOn(skillLoader, unsafeTestValue("loadUserSkills")).mockResolvedValue({})
|
||||||
spyOn(skillLoader, "loadProjectSkills" as any).mockResolvedValue({})
|
spyOn(skillLoader, unsafeTestValue("loadProjectSkills")).mockResolvedValue({})
|
||||||
spyOn(skillLoader, "loadOpencodeGlobalSkills" as any).mockResolvedValue({})
|
spyOn(skillLoader, unsafeTestValue("loadOpencodeGlobalSkills")).mockResolvedValue({})
|
||||||
spyOn(skillLoader, "loadOpencodeProjectSkills" as any).mockResolvedValue({})
|
spyOn(skillLoader, unsafeTestValue("loadOpencodeProjectSkills")).mockResolvedValue({})
|
||||||
spyOn(skillLoader, "discoverUserClaudeSkills" as any).mockResolvedValue([])
|
spyOn(skillLoader, unsafeTestValue("discoverUserClaudeSkills")).mockResolvedValue([])
|
||||||
spyOn(skillLoader, "discoverProjectClaudeSkills" as any).mockResolvedValue([])
|
spyOn(skillLoader, unsafeTestValue("discoverProjectClaudeSkills")).mockResolvedValue([])
|
||||||
spyOn(skillLoader, "discoverOpencodeGlobalSkills" as any).mockResolvedValue([])
|
spyOn(skillLoader, unsafeTestValue("discoverOpencodeGlobalSkills")).mockResolvedValue([])
|
||||||
spyOn(skillLoader, "discoverOpencodeProjectSkills" as any).mockResolvedValue([])
|
spyOn(skillLoader, unsafeTestValue("discoverOpencodeProjectSkills")).mockResolvedValue([])
|
||||||
|
|
||||||
spyOn(agentLoader, "loadUserAgents" as any).mockReturnValue({})
|
spyOn(agentLoader, unsafeTestValue("loadUserAgents")).mockReturnValue({})
|
||||||
spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({})
|
spyOn(agentLoader, unsafeTestValue("loadProjectAgents")).mockReturnValue({})
|
||||||
spyOn(agentLoader, "loadOpencodeGlobalAgents" as any).mockReturnValue({})
|
spyOn(agentLoader, unsafeTestValue("loadOpencodeGlobalAgents")).mockReturnValue({})
|
||||||
spyOn(agentLoader, "loadOpencodeProjectAgents" as any).mockReturnValue({})
|
spyOn(agentLoader, unsafeTestValue("loadOpencodeProjectAgents")).mockReturnValue({})
|
||||||
|
|
||||||
spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} })
|
spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({ servers: {}, loadedServers: [] })
|
||||||
setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {})
|
setAdditionalAllowedMcpEnvVarsSpy = spyOn(mcpLoader, "setAdditionalAllowedMcpEnvVars").mockImplementation(() => {})
|
||||||
|
|
||||||
spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({
|
spyOn(pluginLoader, unsafeTestValue("loadAllPluginComponents")).mockResolvedValue({
|
||||||
commands: {},
|
commands: {},
|
||||||
skills: {},
|
skills: {},
|
||||||
agents: {},
|
agents: {},
|
||||||
@@ -85,54 +86,57 @@ beforeEach(async () => {
|
|||||||
errors: [],
|
errors: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({})
|
spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({})
|
||||||
|
|
||||||
spyOn(shared, "log" as any).mockImplementation(() => {})
|
spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {})
|
||||||
spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"]))
|
spyOn(shared, unsafeTestValue("fetchAvailableModels")).mockResolvedValue(new Set(["anthropic/claude-opus-4-7"]))
|
||||||
spyOn(shared, "readConnectedProvidersCache" as any).mockReturnValue(null)
|
spyOn(shared, unsafeTestValue("readConnectedProvidersCache")).mockReturnValue(null)
|
||||||
|
|
||||||
spyOn(configDir, "getOpenCodeConfigPaths" as any).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, "migrateAgentConfig" as any).mockImplementation((config: Record<string, unknown>) => config)
|
spyOn(permissionCompat, unsafeTestValue("migrateAgentConfig")).mockImplementation((config: Record<string, unknown>) => config)
|
||||||
|
|
||||||
spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-7" })
|
spyOn(modelResolver, unsafeTestValue("resolveModelWithFallback")).mockReturnValue({ model: "anthropic/claude-opus-4-7", source: "provider-fallback" })
|
||||||
;({ createConfigHandler } = await importFreshConfigHandlerModule())
|
;({ createConfigHandler } = await importFreshConfigHandlerModule())
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
(agents.createBuiltinAgents as any)?.mockRestore?.()
|
(unsafeTestValue(agents.createBuiltinAgents))?.mockRestore?.()
|
||||||
;(sisyphusJunior.createSisyphusJuniorAgentWithOverrides as any)?.mockRestore?.()
|
;(unsafeTestValue(sisyphusJunior.createSisyphusJuniorAgentWithOverrides))?.mockRestore?.()
|
||||||
;(commandLoader.loadUserCommands as any)?.mockRestore?.()
|
;(unsafeTestValue(commandLoader.loadUserCommands))?.mockRestore?.()
|
||||||
;(commandLoader.loadProjectCommands as any)?.mockRestore?.()
|
;(unsafeTestValue(commandLoader.loadProjectCommands))?.mockRestore?.()
|
||||||
;(commandLoader.loadOpencodeGlobalCommands as any)?.mockRestore?.()
|
;(unsafeTestValue(commandLoader.loadOpencodeGlobalCommands))?.mockRestore?.()
|
||||||
;(commandLoader.loadOpencodeProjectCommands as any)?.mockRestore?.()
|
;(unsafeTestValue(commandLoader.loadOpencodeProjectCommands))?.mockRestore?.()
|
||||||
;(builtinCommands.loadBuiltinCommands as any)?.mockRestore?.()
|
;(unsafeTestValue(builtinCommands.loadBuiltinCommands))?.mockRestore?.()
|
||||||
;(skillLoader.loadUserSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.loadUserSkills))?.mockRestore?.()
|
||||||
;(skillLoader.loadProjectSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.loadProjectSkills))?.mockRestore?.()
|
||||||
;(skillLoader.loadOpencodeGlobalSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.loadOpencodeGlobalSkills))?.mockRestore?.()
|
||||||
;(skillLoader.loadOpencodeProjectSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.loadOpencodeProjectSkills))?.mockRestore?.()
|
||||||
;(skillLoader.discoverUserClaudeSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.discoverUserClaudeSkills))?.mockRestore?.()
|
||||||
;(skillLoader.discoverProjectClaudeSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.discoverProjectClaudeSkills))?.mockRestore?.()
|
||||||
;(skillLoader.discoverOpencodeGlobalSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.discoverOpencodeGlobalSkills))?.mockRestore?.()
|
||||||
;(skillLoader.discoverOpencodeProjectSkills as any)?.mockRestore?.()
|
;(unsafeTestValue(skillLoader.discoverOpencodeProjectSkills))?.mockRestore?.()
|
||||||
;(agentLoader.loadUserAgents as any)?.mockRestore?.()
|
;(unsafeTestValue(agentLoader.loadUserAgents))?.mockRestore?.()
|
||||||
;(agentLoader.loadProjectAgents as any)?.mockRestore?.()
|
;(unsafeTestValue(agentLoader.loadProjectAgents))?.mockRestore?.()
|
||||||
;(agentLoader.loadOpencodeGlobalAgents as any)?.mockRestore?.()
|
;(unsafeTestValue(agentLoader.loadOpencodeGlobalAgents))?.mockRestore?.()
|
||||||
;(agentLoader.loadOpencodeProjectAgents as any)?.mockRestore?.()
|
;(unsafeTestValue(agentLoader.loadOpencodeProjectAgents))?.mockRestore?.()
|
||||||
;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.()
|
;(unsafeTestValue(mcpLoader.loadMcpConfigs))?.mockRestore?.()
|
||||||
setAdditionalAllowedMcpEnvVarsSpy?.mockRestore()
|
setAdditionalAllowedMcpEnvVarsSpy?.mockRestore()
|
||||||
;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.()
|
;(unsafeTestValue(pluginLoader.loadAllPluginComponents))?.mockRestore?.()
|
||||||
;(mcpModule.createBuiltinMcps as any)?.mockRestore?.()
|
;(unsafeTestValue(mcpModule.createBuiltinMcps))?.mockRestore?.()
|
||||||
;(shared.log as any)?.mockRestore?.()
|
;(unsafeTestValue(shared.log))?.mockRestore?.()
|
||||||
;(shared.fetchAvailableModels as any)?.mockRestore?.()
|
;(unsafeTestValue(shared.fetchAvailableModels))?.mockRestore?.()
|
||||||
;(shared.readConnectedProvidersCache as any)?.mockRestore?.()
|
;(unsafeTestValue(shared.readConnectedProvidersCache))?.mockRestore?.()
|
||||||
;(configDir.getOpenCodeConfigPaths as any)?.mockRestore?.()
|
;(unsafeTestValue(configDir.getOpenCodeConfigPaths))?.mockRestore?.()
|
||||||
;(permissionCompat.migrateAgentConfig as any)?.mockRestore?.()
|
;(unsafeTestValue(permissionCompat.migrateAgentConfig))?.mockRestore?.()
|
||||||
;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.()
|
;(unsafeTestValue(modelResolver.resolveModelWithFallback))?.mockRestore?.()
|
||||||
;(agentPriorityOrder.reorderAgentsByPriority as any)?.mockRestore?.()
|
;(unsafeTestValue(agentPriorityOrder.reorderAgentsByPriority))?.mockRestore?.()
|
||||||
configErrors.clearConfigLoadErrors()
|
configErrors.clearConfigLoadErrors()
|
||||||
mock.restore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
@@ -230,10 +234,10 @@ 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 = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||||
@@ -275,17 +279,17 @@ describe("Plan agent demote behavior", () => {
|
|||||||
|
|
||||||
test("assembles core agents first before priority reorder runs", async () => {
|
test("assembles core agents first before priority reorder runs", async () => {
|
||||||
// #given
|
// #given
|
||||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||||
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 = spyOn(agentPriorityOrder, "reorderAgentsByPriority") as any
|
const reorderSpy = unsafeTestValue(spyOn(agentPriorityOrder, "reorderAgentsByPriority"))
|
||||||
const pluginConfig = createPluginConfig({
|
const pluginConfig = createPluginConfig({
|
||||||
sisyphus_agent: {
|
sisyphus_agent: {
|
||||||
planner_enabled: true,
|
planner_enabled: true,
|
||||||
@@ -321,9 +325,9 @@ 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 = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { prompt: "test", mode: "primary" },
|
sisyphus: { prompt: "test", mode: "primary" },
|
||||||
hephaestus: { prompt: "test", mode: "primary" },
|
hephaestus: { prompt: "test", mode: "primary" },
|
||||||
@@ -485,9 +489,9 @@ 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 = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||||
@@ -1054,7 +1058,7 @@ describe("Plan agent model inheritance from prometheus", () => {
|
|||||||
|
|
||||||
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
|
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, "resolveModelPipeline" as any).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, "resolveModelPipeline" as any).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, "resolveModelPipeline" as any).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
|
||||||
;(pluginLoader.loadAllPluginComponents as any).mockRestore?.()
|
;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
|
||||||
spyOn(pluginLoader, "loadAllPluginComponents" as any).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
|
||||||
;(pluginLoader.loadAllPluginComponents as any).mockRestore?.()
|
;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
|
||||||
spyOn(pluginLoader, "loadAllPluginComponents" as any).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
|
||||||
;(pluginLoader.loadAllPluginComponents as any).mockRestore?.()
|
;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
|
||||||
spyOn(pluginLoader, "loadAllPluginComponents" as any).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
|
||||||
;(pluginLoader.loadAllPluginComponents as any).mockRestore?.()
|
;(unsafeTestValue(pluginLoader.loadAllPluginComponents)).mockRestore?.()
|
||||||
spyOn(pluginLoader, "loadAllPluginComponents" as any).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,16 +1359,16 @@ 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 = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
}
|
}>(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" },
|
||||||
})
|
})
|
||||||
;(builtinCommands.loadBuiltinCommands as unknown as {
|
;(unsafeTestValue<{
|
||||||
mockReturnValue: (value: Record<string, unknown>) => void
|
mockReturnValue: (value: Record<string, unknown>) => void
|
||||||
}).mockReturnValue({
|
}>(builtinCommands.loadBuiltinCommands)).mockReturnValue({
|
||||||
"start-work": {
|
"start-work": {
|
||||||
name: "start-work",
|
name: "start-work",
|
||||||
description: "(builtin) Start work",
|
description: "(builtin) Start work",
|
||||||
@@ -1404,9 +1412,9 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
|
|||||||
|
|
||||||
test("denies todowrite and todoread for primary agents when task_system is enabled", async () => {
|
test("denies todowrite and todoread for primary agents when task_system is enabled", async () => {
|
||||||
//#given
|
//#given
|
||||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||||
@@ -1445,10 +1453,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
|
|||||||
|
|
||||||
test("does not deny todowrite/todoread when task_system is disabled", async () => {
|
test("does not deny todowrite/todoread when task_system is disabled", async () => {
|
||||||
//#given
|
//#given
|
||||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||||
@@ -1487,10 +1495,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
|
|||||||
|
|
||||||
test("does not deny todowrite/todoread when task_system is undefined", async () => {
|
test("does not deny todowrite/todoread when task_system is undefined", async () => {
|
||||||
//#given
|
//#given
|
||||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||||
})
|
})
|
||||||
@@ -1526,10 +1534,10 @@ describe("per-agent todowrite/todoread deny when task_system enabled", () => {
|
|||||||
describe("disable_omo_env pass-through", () => {
|
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 = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "without-env", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "without-env", mode: "primary" },
|
||||||
})
|
})
|
||||||
@@ -1563,10 +1571,10 @@ describe("disable_omo_env pass-through", () => {
|
|||||||
|
|
||||||
test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => {
|
test("passes disable_omo_env=false to createBuiltinAgents when omitted", async () => {
|
||||||
//#given
|
//#given
|
||||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
const createBuiltinAgentsMock = unsafeTestValue<{
|
||||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||||
mock: { calls: unknown[][] }
|
mock: { calls: unknown[][] }
|
||||||
}
|
}>(agents.createBuiltinAgents)
|
||||||
createBuiltinAgentsMock.mockResolvedValue({
|
createBuiltinAgentsMock.mockResolvedValue({
|
||||||
sisyphus: { name: "sisyphus", prompt: "with-env", mode: "primary" },
|
sisyphus: { name: "sisyphus", prompt: "with-env", mode: "primary" },
|
||||||
})
|
})
|
||||||
@@ -1600,14 +1608,14 @@ describe("disable_omo_env pass-through", () => {
|
|||||||
describe("Agent merge priority — project-local overrides global", () => {
|
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
|
||||||
;(agentLoader.loadUserAgents as any).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",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
;(agentLoader.loadProjectAgents as any).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
|
||||||
;(agentLoader.loadOpencodeGlobalAgents as any).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",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
;(agentLoader.loadOpencodeProjectAgents as any).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
|
||||||
;(agentLoader.loadOpencodeGlobalAgents as any).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",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
;(agentLoader.loadProjectAgents as any).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
|
||||||
;(pluginLoader.loadAllPluginComponents as any).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: [],
|
||||||
})
|
})
|
||||||
;(agentLoader.loadUserAgents as any).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, "loadMcpConfigs" as any).mockResolvedValue({
|
loadMcpConfigsSpy = spyOn(mcpLoader, unsafeTestValue("loadMcpConfigs")).mockResolvedValue({
|
||||||
servers: {},
|
servers: {},
|
||||||
})
|
})
|
||||||
createBuiltinMcpsSpy = spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({})
|
createBuiltinMcpsSpy = spyOn(mcpModule, unsafeTestValue("createBuiltinMcps")).mockReturnValue({})
|
||||||
spyOn(shared, "log" as any).mockImplementation(() => {})
|
spyOn(shared, unsafeTestValue("log")).mockImplementation(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
loadMcpConfigsSpy.mockRestore()
|
loadMcpConfigsSpy.mockRestore()
|
||||||
createBuiltinMcpsSpy.mockRestore()
|
createBuiltinMcpsSpy.mockRestore()
|
||||||
;(shared.log as any)?.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: ["playwright"] as any })
|
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: ["firecrawl", "exa"] as any })
|
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: ["plugin:custom"] as any })
|
const pluginConfig = createPluginConfig({ disabled_mcps: unsafeTestValue(["plugin:custom"]) })
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const { applyMcpConfig } = await import("./mcp-config-handler")
|
const { applyMcpConfig } = await import("./mcp-config-handler")
|
||||||
|
|||||||
@@ -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: { client: { tui: { showToast: async () => {} } } } as any,
|
ctx: unsafeTestValue({ client: { tui: { showToast: async () => {} } } }),
|
||||||
pluginConfig: (overrides?.pluginConfig ?? {}) as any,
|
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: {
|
hooks: unsafeTestValue({
|
||||||
stopContinuationGuard: null,
|
stopContinuationGuard: null,
|
||||||
backgroundNotificationHook: null,
|
backgroundNotificationHook: null,
|
||||||
keywordDetector: null,
|
keywordDetector: null,
|
||||||
@@ -70,7 +71,7 @@ function createMockHandlerArgs(overrides?: {
|
|||||||
autoSlashCommand: null,
|
autoSlashCommand: null,
|
||||||
startWork: null,
|
startWork: null,
|
||||||
ralphLoop: null,
|
ralphLoop: null,
|
||||||
} as any,
|
}),
|
||||||
_appliedSessions: appliedSessions,
|
_appliedSessions: appliedSessions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 input as unknown as EventHandlerInput
|
return unsafeTestValue<EventHandlerInput>(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] {
|
function asEventHandlerContext(ctx: unknown): EventHandlerArgs["ctx"] {
|
||||||
return ctx as unknown as EventHandlerArgs["ctx"]
|
return unsafeTestValue<EventHandlerArgs["ctx"]>(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] {
|
function asPluginConfig(config: unknown): EventHandlerArgs["pluginConfig"] {
|
||||||
return config as unknown as EventHandlerArgs["pluginConfig"]
|
return unsafeTestValue<EventHandlerArgs["pluginConfig"]>(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] {
|
function asChatMessageHandlerContext(ctx: unknown): ChatMessageHandlerArgs["ctx"] {
|
||||||
return ctx as unknown as ChatMessageHandlerArgs["ctx"]
|
return unsafeTestValue<ChatMessageHandlerArgs["ctx"]>(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] {
|
function asChatPluginConfig(config: unknown): ChatMessageHandlerArgs["pluginConfig"] {
|
||||||
return config as unknown as ChatMessageHandlerArgs["pluginConfig"]
|
return unsafeTestValue<ChatMessageHandlerArgs["pluginConfig"]>(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerManagers(): EventHandlerArgs["managers"] {
|
function createEventHandlerManagers(): EventHandlerArgs["managers"] {
|
||||||
return {
|
return unsafeTestValue<EventHandlerArgs["managers"]>({
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
@@ -41,17 +42,17 @@ function createEventHandlerManagers(): EventHandlerArgs["managers"] {
|
|||||||
skillMcpManager: {
|
skillMcpManager: {
|
||||||
disconnectSession: async () => {},
|
disconnectSession: async () => {},
|
||||||
},
|
},
|
||||||
} as unknown as EventHandlerArgs["managers"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): EventHandlerArgs["hooks"] {
|
function createEventHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): EventHandlerArgs["hooks"] {
|
||||||
return {
|
return unsafeTestValue<EventHandlerArgs["hooks"]>({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
} as unknown as EventHandlerArgs["hooks"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createChatMessageHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): ChatMessageHandlerArgs["hooks"] {
|
function createChatMessageHandlerHooks(modelFallback: ReturnType<typeof createModelFallbackHook>): ChatMessageHandlerArgs["hooks"] {
|
||||||
return {
|
return unsafeTestValue<ChatMessageHandlerArgs["hooks"]>({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
stopContinuationGuard: null,
|
stopContinuationGuard: null,
|
||||||
keywordDetector: null,
|
keywordDetector: null,
|
||||||
@@ -59,7 +60,7 @@ function createChatMessageHandlerHooks(modelFallback: ReturnType<typeof createMo
|
|||||||
autoSlashCommand: null,
|
autoSlashCommand: null,
|
||||||
startWork: null,
|
startWork: null,
|
||||||
ralphLoop: null,
|
ralphLoop: null,
|
||||||
} as unknown as ChatMessageHandlerArgs["hooks"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
|
let readConnectedProvidersCacheSpy: { mockRestore: () => void } | undefined
|
||||||
|
|||||||
@@ -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: {
|
ctx: unsafeTestValue({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: { session: sessionClient },
|
client: { session: sessionClient },
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: (args?.pluginConfig ?? {}) as any,
|
pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
markSessionCreated: () => {},
|
markSessionCreated: () => {},
|
||||||
clear: () => {},
|
clear: () => {},
|
||||||
},
|
},
|
||||||
managers: {
|
managers: unsafeTestValue({
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
@@ -67,8 +68,8 @@ describe("createEventHandler - model-fallback auto-continuation pins agent/model
|
|||||||
skillMcpManager: {
|
skillMcpManager: {
|
||||||
disconnectSession: async () => {},
|
disconnectSession: async () => {},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
hooks: args?.hooks ?? ({} as any),
|
hooks: args?.hooks ?? (unsafeTestValue({})),
|
||||||
})
|
})
|
||||||
|
|
||||||
return { handler, promptAsyncBodies, promptBodies }
|
return { handler, promptAsyncBodies, promptBodies }
|
||||||
|
|||||||
@@ -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: {
|
ctx: unsafeTestValue({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -36,13 +37,13 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: (args?.pluginConfig ?? {}) as any,
|
pluginConfig: unsafeTestValue((args?.pluginConfig ?? {})),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
markSessionCreated: () => {},
|
markSessionCreated: () => {},
|
||||||
clear: () => {},
|
clear: () => {},
|
||||||
},
|
},
|
||||||
managers: {
|
managers: unsafeTestValue({
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
@@ -50,8 +51,8 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
skillMcpManager: {
|
skillMcpManager: {
|
||||||
disconnectSession: async () => {},
|
disconnectSession: async () => {},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
hooks: args?.hooks ?? ({} as any),
|
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: {
|
ctx: unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => ({}),
|
showToast: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: {} as any,
|
pluginConfig: unsafeTestValue({}),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
shouldOverride: () => false,
|
shouldOverride: () => false,
|
||||||
markApplied: () => {},
|
markApplied: () => {},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: unsafeTestValue({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
stopContinuationGuard: null,
|
stopContinuationGuard: null,
|
||||||
keywordDetector: null,
|
keywordDetector: null,
|
||||||
@@ -168,7 +169,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
autoSlashCommand: null,
|
autoSlashCommand: null,
|
||||||
startWork: null,
|
startWork: null,
|
||||||
ralphLoop: null,
|
ralphLoop: null,
|
||||||
} as any,
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await handler({
|
await handler({
|
||||||
@@ -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: {
|
ctx: unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async () => ({}),
|
showToast: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: {} as any,
|
pluginConfig: unsafeTestValue({}),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
shouldOverride: () => false,
|
shouldOverride: () => false,
|
||||||
markApplied: () => {},
|
markApplied: () => {},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: unsafeTestValue({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
stopContinuationGuard: null,
|
stopContinuationGuard: null,
|
||||||
keywordDetector: null,
|
keywordDetector: null,
|
||||||
@@ -378,7 +379,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
autoSlashCommand: null,
|
autoSlashCommand: null,
|
||||||
startWork: null,
|
startWork: null,
|
||||||
ralphLoop: null,
|
ralphLoop: null,
|
||||||
} as any,
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await handler({
|
await handler({
|
||||||
@@ -449,7 +450,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
|
|
||||||
setupConnectedProviderCacheMocks()
|
setupConnectedProviderCacheMocks()
|
||||||
const eventHandler = createEventHandler({
|
const eventHandler = createEventHandler({
|
||||||
ctx: {
|
ctx: unsafeTestValue({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -463,13 +464,13 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: {} as any,
|
pluginConfig: unsafeTestValue({}),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
markSessionCreated: () => {},
|
markSessionCreated: () => {},
|
||||||
clear: () => {},
|
clear: () => {},
|
||||||
},
|
},
|
||||||
managers: {
|
managers: unsafeTestValue({
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
@@ -477,14 +478,14 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
skillMcpManager: {
|
skillMcpManager: {
|
||||||
disconnectSession: async () => {},
|
disconnectSession: async () => {},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
hooks: {
|
hooks: unsafeTestValue({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
} as any,
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const chatMessageHandler = createChatMessageHandler({
|
const chatMessageHandler = createChatMessageHandler({
|
||||||
ctx: {
|
ctx: unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
showToast: async ({ body }: { body: { title?: string } }) => {
|
showToast: async ({ body }: { body: { title?: string } }) => {
|
||||||
@@ -493,13 +494,13 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any,
|
}),
|
||||||
pluginConfig: {} as any,
|
pluginConfig: unsafeTestValue({}),
|
||||||
firstMessageVariantGate: {
|
firstMessageVariantGate: {
|
||||||
shouldOverride: () => false,
|
shouldOverride: () => false,
|
||||||
markApplied: () => {},
|
markApplied: () => {},
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: unsafeTestValue({
|
||||||
modelFallback,
|
modelFallback,
|
||||||
stopContinuationGuard: null,
|
stopContinuationGuard: null,
|
||||||
keywordDetector: null,
|
keywordDetector: null,
|
||||||
@@ -507,7 +508,7 @@ describe("createEventHandler - model fallback", () => {
|
|||||||
autoSlashCommand: null,
|
autoSlashCommand: null,
|
||||||
startWork: null,
|
startWork: null,
|
||||||
ralphLoop: null,
|
ralphLoop: null,
|
||||||
} as any,
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const triggerRetryCycle = async () => {
|
const triggerRetryCycle = async () => {
|
||||||
|
|||||||
@@ -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,42 +19,42 @@ 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 input as unknown as HarnessEventInput
|
return unsafeTestValue<HarnessEventInput>(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
function asHarnessContext(ctx: unknown): HarnessContext {
|
function asHarnessContext(ctx: unknown): HarnessContext {
|
||||||
return ctx as unknown as HarnessContext
|
return unsafeTestValue<HarnessContext>(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerManagers(
|
function createEventHandlerManagers(
|
||||||
overrides: Record<string, unknown> = {},
|
overrides: Record<string, unknown> = {},
|
||||||
): EventHandlerArgs["managers"] {
|
): EventHandlerArgs["managers"] {
|
||||||
return {
|
return unsafeTestValue<EventHandlerArgs["managers"]>({
|
||||||
...({} as EventHandlerArgs["managers"]),
|
...({} as EventHandlerArgs["managers"]),
|
||||||
tmuxSessionManager: {
|
tmuxSessionManager: {
|
||||||
onSessionCreated: async () => {},
|
onSessionCreated: async () => {},
|
||||||
onSessionDeleted: async () => {},
|
onSessionDeleted: async () => {},
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as EventHandlerArgs["managers"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandlerHooks(
|
function createEventHandlerHooks(
|
||||||
overrides: Record<string, unknown>,
|
overrides: Record<string, unknown>,
|
||||||
): EventHandlerArgs["hooks"] {
|
): EventHandlerArgs["hooks"] {
|
||||||
return {
|
return unsafeTestValue<EventHandlerArgs["hooks"]>({
|
||||||
...({} as EventHandlerArgs["hooks"]),
|
...({} as EventHandlerArgs["hooks"]),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as EventHandlerArgs["hooks"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createChatMessageHandlerHooks(
|
function createChatMessageHandlerHooks(
|
||||||
overrides: Record<string, unknown>,
|
overrides: Record<string, unknown>,
|
||||||
): ChatMessageHandlerArgs["hooks"] {
|
): ChatMessageHandlerArgs["hooks"] {
|
||||||
return {
|
return unsafeTestValue<ChatMessageHandlerArgs["hooks"]>({
|
||||||
...({} as ChatMessageHandlerArgs["hooks"]),
|
...({} as ChatMessageHandlerArgs["hooks"]),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as ChatMessageHandlerArgs["hooks"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIMARY_MODEL = {
|
const PRIMARY_MODEL = {
|
||||||
@@ -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 {
|
return unsafeTestValue<EventHandlerArgs["pluginConfig"]>({
|
||||||
agents: {
|
agents: {
|
||||||
sisyphus: {
|
sisyphus: {
|
||||||
fallback_models: CLIPROXYAPI_FALLBACKS,
|
fallback_models: CLIPROXYAPI_FALLBACKS,
|
||||||
@@ -100,7 +101,7 @@ function createPluginConfig(mode: HarnessMode) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
} as unknown as EventHandlerArgs["pluginConfig"]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHarness(args: {
|
function createHarness(args: {
|
||||||
@@ -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: pluginConfig as unknown as EventHandlerArgs["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: pluginConfig as unknown as EventHandlerArgs["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: pluginConfig as unknown as ChatMessageHandlerArgs["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 = {
|
const mockContext = unsafeTestValue<PluginContext>({
|
||||||
directory: "/tmp",
|
directory: "/tmp",
|
||||||
client: {
|
client: {
|
||||||
tui: {
|
tui: {
|
||||||
@@ -15,7 +16,7 @@ const mockContext = {
|
|||||||
update: async () => ({}),
|
update: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as PluginContext
|
})
|
||||||
|
|
||||||
const mockModelCacheState = {} as ModelCacheState
|
const mockModelCacheState = {} as ModelCacheState
|
||||||
|
|
||||||
|
|||||||
@@ -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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"],
|
ctx: unsafeTestValue<Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"]>(createCtx(directory)),
|
||||||
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 ?? {} })
|
||||||
@@ -106,7 +107,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
|
|||||||
cancelLoop: () => true,
|
cancelLoop: () => true,
|
||||||
getState: () => null,
|
getState: () => null,
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["hooks"],
|
}),
|
||||||
})
|
})
|
||||||
const output = {
|
const output = {
|
||||||
args: {
|
args: {
|
||||||
@@ -148,7 +149,7 @@ describe("tool.execute.before ultrawork oracle verification", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const beforeHandler = createToolExecuteBeforeHandler({
|
const beforeHandler = createToolExecuteBeforeHandler({
|
||||||
ctx: createCtx(directory) as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteBeforeHandler>[0]["ctx"],
|
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: createCtx(directory) as unknown as Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"],
|
ctx: unsafeTestValue<Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"]>(createCtx(directory)),
|
||||||
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
|
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +71,11 @@ describe("resolveUltraworkOverride", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
||||||
return {
|
return unsafeTestValue<Parameters<typeof resolveUltraworkOverride>[0]>({
|
||||||
agents: {
|
agents: {
|
||||||
[agentName]: { ultrawork },
|
[agentName]: { ultrawork },
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof resolveUltraworkOverride>[0]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should resolve override when ultrawork keyword detected", () => {
|
test("should resolve override when ultrawork keyword detected", () => {
|
||||||
@@ -139,9 +140,9 @@ 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 = {
|
const config = unsafeTestValue<Parameters<typeof resolveUltraworkOverride>[0]>({
|
||||||
agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } },
|
agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } },
|
||||||
} as unknown as Parameters<typeof resolveUltraworkOverride>[0]
|
})
|
||||||
const output = createOutput("ultrawork do something")
|
const output = createOutput("ultrawork do something")
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
@@ -278,11 +279,11 @@ describe("applyUltraworkModelOverrideOnMessage", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
||||||
return {
|
return unsafeTestValue<Parameters<typeof applyUltraworkModelOverrideOnMessage>[0]>({
|
||||||
agents: {
|
agents: {
|
||||||
[agentName]: { ultrawork },
|
[agentName]: { ultrawork },
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof applyUltraworkModelOverrideOnMessage>[0]
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should schedule deferred DB override without variant when SDK unavailable", () => {
|
test("should schedule deferred DB override without variant when SDK unavailable", () => {
|
||||||
|
|||||||
@@ -216,20 +216,23 @@ Body content`
|
|||||||
agent: string
|
agent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FrontmatterWithExtras extends MinimalMeta {
|
||||||
|
extra_field: string
|
||||||
|
another_extra: { nested: string; array: string[] }
|
||||||
|
custom_boolean: boolean
|
||||||
|
custom_number: number
|
||||||
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = parseFrontmatter<MinimalMeta>(content)
|
const result = parseFrontmatter<FrontmatterWithExtras>(content)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result.data.description).toBe("Test command")
|
expect(result.data.description).toBe("Test command")
|
||||||
expect(result.data.agent).toBe("build")
|
expect(result.data.agent).toBe("build")
|
||||||
expect(result.body).toBe("Body content")
|
expect(result.body).toBe("Body content")
|
||||||
// @ts-expect-error - accessing extra field not in MinimalMeta
|
|
||||||
expect(result.data.extra_field).toBe("should not fail")
|
expect(result.data.extra_field).toBe("should not fail")
|
||||||
// @ts-expect-error - accessing extra field not in MinimalMeta
|
|
||||||
expect(result.data.another_extra).toEqual({ nested: "value", array: ["item1", "item2"] })
|
expect(result.data.another_extra).toEqual({ nested: "value", array: ["item1", "item2"] })
|
||||||
// @ts-expect-error - accessing extra field not in MinimalMeta
|
|
||||||
expect(result.data.custom_boolean).toBe(true)
|
expect(result.data.custom_boolean).toBe(true)
|
||||||
// @ts-expect-error - accessing extra field not in MinimalMeta
|
|
||||||
expect(result.data.custom_number).toBe(42)
|
expect(result.data.custom_number).toBe(42)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 = execFileSyncSpy.mock.calls as unknown as Array<[string, string[], { cwd?: string }]>
|
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")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as fs from "node:fs"
|
import * as fs from "node:fs"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import { log } from "../logger"
|
import { log } from "../logger"
|
||||||
|
import { isRecord } from "../record-type-guard"
|
||||||
import { writeFileAtomically } from "../write-file-atomically"
|
import { writeFileAtomically } from "../write-file-atomically"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,14 +49,9 @@ export function readAppliedMigrations(configPath: string): Set<string> {
|
|||||||
return new Set()
|
return new Set()
|
||||||
}
|
}
|
||||||
const content = fs.readFileSync(sidecarPath, "utf-8")
|
const content = fs.readFileSync(sidecarPath, "utf-8")
|
||||||
const parsed = JSON.parse(content) as unknown
|
const parsed: unknown = JSON.parse(content)
|
||||||
if (
|
if (isRecord(parsed) && Array.isArray(parsed.appliedMigrations)) {
|
||||||
parsed &&
|
return new Set(parsed.appliedMigrations.filter((migration): migration is string => typeof migration === "string"))
|
||||||
typeof parsed === "object" &&
|
|
||||||
!Array.isArray(parsed) &&
|
|
||||||
Array.isArray((parsed as MigrationsSidecar).appliedMigrations)
|
|
||||||
) {
|
|
||||||
return new Set((parsed as MigrationsSidecar).appliedMigrations.filter((m): m is string => typeof m === "string"))
|
|
||||||
}
|
}
|
||||||
return new Set()
|
return new Set()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
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(client as any, {
|
await promptSyncWithModelSuggestionRetry(unsafeTestValue(client), {
|
||||||
path: { id: "session-1" },
|
path: { id: "session-1" },
|
||||||
body: {
|
body: {
|
||||||
agent: "multimodal-looker",
|
agent: "multimodal-looker",
|
||||||
|
|||||||
@@ -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 = {
|
const manager = unsafeTestValue<BackgroundManager>({
|
||||||
launch: mock(() => Promise.resolve({
|
launch: mock(() => Promise.resolve({
|
||||||
id: "task-1",
|
id: "task-1",
|
||||||
sessionID: null,
|
sessionID: null,
|
||||||
@@ -27,12 +28,12 @@ describe("createBackgroundTask metadata", () => {
|
|||||||
status: "pending",
|
status: "pending",
|
||||||
})),
|
})),
|
||||||
getTask: mock(() => undefined),
|
getTask: mock(() => undefined),
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = {
|
const client = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
messages: mock(() => Promise.resolve({ data: [] })),
|
messages: mock(() => Promise.resolve({ data: [] })),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
let capturedMetadata: { title?: string; metadata?: Record<string, unknown> } | undefined
|
let capturedMetadata: { title?: string; metadata?: Record<string, unknown> } | undefined
|
||||||
const tool = createBackgroundTask(manager, client)
|
const tool = createBackgroundTask(manager, client)
|
||||||
|
|||||||
@@ -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,16 +22,16 @@ describe("createBackgroundTask", () => {
|
|||||||
}))
|
}))
|
||||||
const getTaskMock = mock()
|
const getTaskMock = mock()
|
||||||
|
|
||||||
const mockManager = {
|
const mockManager = unsafeTestValue<BackgroundManager>({
|
||||||
launch: launchMock,
|
launch: launchMock,
|
||||||
getTask: getTaskMock,
|
getTask: getTaskMock,
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
|
|
||||||
const mockClient = {
|
const mockClient = unsafeTestValue<PluginInput["client"]>({
|
||||||
session: {
|
session: {
|
||||||
messages: mock(() => Promise.resolve({ data: [] })),
|
messages: mock(() => Promise.resolve({ data: [] })),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
})
|
||||||
|
|
||||||
const tool = createBackgroundTask(mockManager, mockClient)
|
const tool = createBackgroundTask(mockManager, mockClient)
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +67,10 @@ 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 = {
|
const ctxWithCallId = unsafeTestValue<ToolContext>({
|
||||||
...mockContext,
|
...mockContext,
|
||||||
callID: "call-1",
|
callID: "call-1",
|
||||||
} as unknown as ToolContext
|
})
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
||||||
@@ -93,10 +94,10 @@ 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 = {
|
const ctxWithCallId = unsafeTestValue<ToolContext>({
|
||||||
...mockContext,
|
...mockContext,
|
||||||
callID: "call-1",
|
callID: "call-1",
|
||||||
} as unknown as ToolContext
|
})
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
await tool.execute({ task_id: "task-1" }, ctxWithCallId)
|
||||||
@@ -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 = {
|
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) => {
|
||||||
@@ -395,7 +396,7 @@ describe("background_cancel", () => {
|
|||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||||
const tool = createBackgroundCancel(manager, client)
|
const tool = createBackgroundCancel(manager, client)
|
||||||
|
|
||||||
@@ -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 = {
|
const manager = unsafeTestValue<BackgroundManager>({
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
getAllDescendantTasks: () => [taskA, taskB],
|
getAllDescendantTasks: () => [taskA, taskB],
|
||||||
cancelTask: async (taskId: string) => {
|
cancelTask: async (taskId: string) => {
|
||||||
@@ -421,7 +422,7 @@ describe("background_cancel", () => {
|
|||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||||
const tool = createBackgroundCancel(manager, client)
|
const tool = createBackgroundCancel(manager, client)
|
||||||
|
|
||||||
@@ -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 = {
|
const manager = unsafeTestValue<BackgroundManager>({
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
getAllDescendantTasks: () => [taskA, taskB],
|
getAllDescendantTasks: () => [taskA, taskB],
|
||||||
cancelTask: async (taskId: string) => {
|
cancelTask: async (taskId: string) => {
|
||||||
@@ -445,7 +446,7 @@ describe("background_cancel", () => {
|
|||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||||
const tool = createBackgroundCancel(manager, client)
|
const tool = createBackgroundCancel(manager, client)
|
||||||
|
|
||||||
@@ -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 = {
|
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) => {
|
||||||
@@ -469,7 +470,7 @@ describe("background_cancel", () => {
|
|||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||||
const tool = createBackgroundCancel(manager, client)
|
const tool = createBackgroundCancel(manager, client)
|
||||||
|
|
||||||
@@ -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 = {
|
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) => {
|
||||||
@@ -495,7 +496,7 @@ describe("background_cancel", () => {
|
|||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
} as unknown as BackgroundManager
|
})
|
||||||
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
const client = { session: { abort: async () => ({}) } } as BackgroundCancelClient
|
||||||
const tool = createBackgroundCancel(manager, client)
|
const tool = createBackgroundCancel(manager, client)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const { describe, test, expect, mock, beforeEach } = require("bun:test")
|
|||||||
const { resolveCallableAgents, clearCallableAgentsCache } = require("./agent-resolver")
|
const { resolveCallableAgents, clearCallableAgentsCache } = require("./agent-resolver")
|
||||||
const { ALLOWED_AGENTS } = require("./constants")
|
const { ALLOWED_AGENTS } = require("./constants")
|
||||||
|
|
||||||
function createMockClient(agents = []) {
|
function createMockClient(agents: Array<Record<string, string>> = []) {
|
||||||
return {
|
return {
|
||||||
app: {
|
app: {
|
||||||
agents: mock(() => Promise.resolve({ data: agents })),
|
agents: mock(() => Promise.resolve({ data: agents })),
|
||||||
|
|||||||
@@ -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(args as any, toolContext as any, ctx as any)
|
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 = (createCalls[0] as any)?.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 = {
|
const ctx = unsafeTestValue<Parameters<typeof resolveOrCreateSessionId>[0]>({
|
||||||
directory: contextDirectory,
|
directory: contextDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -31,7 +32,7 @@ describe("call-omo-agent resolveOrCreateSessionId", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof resolveOrCreateSessionId>[0]
|
})
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
description: "sync test",
|
description: "sync test",
|
||||||
|
|||||||
@@ -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, ctx as any, deps, undefined, spawnReservation)
|
await executeSync(args, toolContext, unsafeTestValue(ctx), deps, undefined, spawnReservation)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(spawnReservation.commit).toHaveBeenCalledTimes(1)
|
expect(spawnReservation.commit).toHaveBeenCalledTimes(1)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ type SessionWithPromptAsync = {
|
|||||||
promptAsync: (opts: { path: { id: string }; body: Record<string, unknown> }) => Promise<unknown>
|
promptAsync: (opts: { path: { id: string }; body: Record<string, unknown> }) => Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasPromptAsync(session: PluginInput["client"]["session"]): session is PluginInput["client"]["session"] & SessionWithPromptAsync {
|
||||||
|
return "promptAsync" in session && typeof session.promptAsync === "function"
|
||||||
|
}
|
||||||
|
|
||||||
type ExecuteSyncDeps = {
|
type ExecuteSyncDeps = {
|
||||||
createOrGetSession: typeof createOrGetSession
|
createOrGetSession: typeof createOrGetSession
|
||||||
waitForCompletion: typeof waitForCompletion
|
waitForCompletion: typeof waitForCompletion
|
||||||
@@ -102,7 +106,11 @@ export async function executeSync(
|
|||||||
const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type)
|
const normalizedSubagentType = stripAgentListSortPrefix(args.subagent_type)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (ctx.client.session as unknown as SessionWithPromptAsync).promptAsync({
|
if (!hasPromptAsync(ctx.client.session)) {
|
||||||
|
return `Error: Failed to send prompt: promptAsync is not available on this OpenCode client.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.client.session.promptAsync({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: {
|
||||||
agent: normalizedSubagentType,
|
agent: normalizedSubagentType,
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
import type { OpencodeClient } from "./types"
|
import type { OpencodeClient } from "./types"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
import { isRecord } from "../../shared/record-type-guard"
|
||||||
import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache"
|
import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache"
|
||||||
|
|
||||||
|
type ModelListClient = OpencodeClient & {
|
||||||
|
model: { list: () => Promise<unknown> }
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasModelList(client: OpencodeClient): client is ModelListClient {
|
||||||
|
return "model" in client && isRecord(client.model) && typeof client.model.list === "function"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isModelRow(value: unknown): value is { provider: string; id: string } {
|
||||||
|
return isRecord(value) && typeof value.provider === "string" && typeof value.id === "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractModelRows(result: unknown): Array<{ provider: string; id: string }> {
|
||||||
|
const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.data) ? result.data : []
|
||||||
|
return rows.filter(isModelRow)
|
||||||
|
}
|
||||||
|
|
||||||
function addFromProviderModels(
|
function addFromProviderModels(
|
||||||
out: Set<string>,
|
out: Set<string>,
|
||||||
providerID: string,
|
providerID: string,
|
||||||
@@ -35,24 +53,17 @@ export async function getAvailableModelsForDelegateTask(client: OpencodeClient):
|
|||||||
return new Set()
|
return new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelList = (client as unknown as { model?: { list?: () => Promise<unknown> } })
|
if (!hasModelList(client)) {
|
||||||
?.model
|
|
||||||
?.list
|
|
||||||
|
|
||||||
if (!modelList) {
|
|
||||||
return new Set()
|
return new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await modelList()
|
const result = await client.model.list()
|
||||||
const rows = Array.isArray(result)
|
const rows = extractModelRows(result)
|
||||||
? result
|
|
||||||
: ((result as { data?: unknown }).data as Array<{ provider?: string; id?: string }> | undefined) ?? []
|
|
||||||
|
|
||||||
const connected = new Set(connectedProviders)
|
const connected = new Set(connectedProviders)
|
||||||
const out = new Set<string>()
|
const out = new Set<string>()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (!row?.provider || !row?.id) continue
|
|
||||||
if (!connected.has(row.provider)) continue
|
if (!connected.has(row.provider)) continue
|
||||||
out.add(`${row.provider}/${row.id}`)
|
out.add(`${row.provider}/${row.id}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {} as any,
|
client: unsafeTestValue({}),
|
||||||
manager: {} as any,
|
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 = {
|
const executorCtx = unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => ({
|
launch: async () => ({
|
||||||
id: "task_1",
|
id: "task_1",
|
||||||
@@ -40,7 +41,7 @@ describe("task tool metadata awaiting", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any
|
})
|
||||||
|
|
||||||
const parentContext = {
|
const parentContext = {
|
||||||
sessionID: "ses_parent",
|
sessionID: "ses_parent",
|
||||||
|
|||||||
@@ -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, {
|
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",
|
||||||
@@ -71,7 +72,7 @@ describe("metadata model unification", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any, parentContext, "explore", MODEL, undefined)
|
}), parentContext, "explore", MODEL, undefined)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -92,7 +93,7 @@ describe("metadata model unification", () => {
|
|||||||
}
|
}
|
||||||
await executeUnstableAgentTask(
|
await executeUnstableAgentTask(
|
||||||
args, ctx,
|
args, ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => launchedTask,
|
launch: async () => launchedTask,
|
||||||
getTask: () => launchedTask,
|
getTask: () => launchedTask,
|
||||||
@@ -109,7 +110,7 @@ describe("metadata model unification", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncPollTimeoutMs: 100,
|
syncPollTimeoutMs: 100,
|
||||||
} as any,
|
}),
|
||||||
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,14 +127,14 @@ describe("metadata model unification", () => {
|
|||||||
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed", model: MODEL,
|
status: "running", sessionId: "ses_resumed", model: MODEL,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -162,7 +163,7 @@ describe("metadata model unification", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
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",
|
||||||
@@ -214,7 +215,7 @@ describe("metadata model unification", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any, parentContext, "explore", undefined, undefined)
|
}), parentContext, "explore", undefined, undefined)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -236,7 +237,7 @@ describe("metadata model unification", () => {
|
|||||||
|
|
||||||
await executeUnstableAgentTask(
|
await executeUnstableAgentTask(
|
||||||
args, ctx,
|
args, ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => launchedTask,
|
launch: async () => launchedTask,
|
||||||
getTask: () => launchedTask,
|
getTask: () => launchedTask,
|
||||||
@@ -253,7 +254,7 @@ describe("metadata model unification", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncPollTimeoutMs: 100,
|
syncPollTimeoutMs: 100,
|
||||||
} as any,
|
}),
|
||||||
parentContext, "explore", undefined, undefined, "anthropic/claude-sonnet-4-6",
|
parentContext, "explore", undefined, undefined, "anthropic/claude-sonnet-4-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -270,14 +271,14 @@ describe("metadata model unification", () => {
|
|||||||
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
load_skills: [], run_in_background: true, task_id: "ses_resumed",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed",
|
status: "running", sessionId: "ses_resumed",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -297,14 +298,14 @@ describe("metadata model unification", () => {
|
|||||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeSyncContinuation(args, ctx, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
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",
|
||||||
@@ -389,7 +390,7 @@ describe("metadata model unification", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any, parentContext, "explore", MODEL_WITH_VARIANT, undefined)
|
}), parentContext, "explore", MODEL_WITH_VARIANT, undefined)
|
||||||
|
|
||||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -411,7 +412,7 @@ describe("metadata model unification", () => {
|
|||||||
|
|
||||||
await executeUnstableAgentTask(
|
await executeUnstableAgentTask(
|
||||||
args, ctx,
|
args, ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => launchedTask,
|
launch: async () => launchedTask,
|
||||||
getTask: () => launchedTask,
|
getTask: () => launchedTask,
|
||||||
@@ -428,7 +429,7 @@ describe("metadata model unification", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncPollTimeoutMs: 100,
|
syncPollTimeoutMs: 100,
|
||||||
} as any,
|
}),
|
||||||
parentContext, "explore", MODEL_WITH_VARIANT, undefined, "google/gemini-3.1-pro high",
|
parentContext, "explore", MODEL_WITH_VARIANT, undefined, "google/gemini-3.1-pro high",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -445,14 +446,14 @@ describe("metadata model unification", () => {
|
|||||||
load_skills: [], run_in_background: true, task_id: "ses_resumed_variant",
|
load_skills: [], run_in_background: true, task_id: "ses_resumed_variant",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed_variant", model: MODEL_WITH_VARIANT,
|
status: "running", sessionId: "ses_resumed_variant", model: MODEL_WITH_VARIANT,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -481,7 +482,7 @@ describe("metadata model unification", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
const meta = ctx.captured.find((metadataEvent: any) => metadataEvent.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
|
|||||||
@@ -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, {
|
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",
|
||||||
@@ -72,7 +73,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any, parentContext, "explore", MODEL, undefined)
|
}), parentContext, "explore", MODEL, undefined)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -98,7 +99,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
|
|
||||||
await executeUnstableAgentTask(
|
await executeUnstableAgentTask(
|
||||||
args, ctx,
|
args, ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => launchedTask,
|
launch: async () => launchedTask,
|
||||||
getTask: () => launchedTask,
|
getTask: () => launchedTask,
|
||||||
@@ -115,7 +116,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncPollTimeoutMs: 100,
|
syncPollTimeoutMs: 100,
|
||||||
} as any,
|
}),
|
||||||
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,14 +137,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
|
load_skills: [], run_in_background: true, task_id: "ses_resumed_x",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -160,14 +161,14 @@ 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, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed_x", model: MODEL, category: "deep",
|
status: "running", sessionId: "ses_resumed_x", model: MODEL, category: "deep",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -187,14 +188,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
task_id: "ses_resumed_x",
|
task_id: "ses_resumed_x",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
status: "running", sessionId: "ses_resumed_x", model: MODEL,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -225,7 +226,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -255,7 +256,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -284,7 +285,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -318,7 +319,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, deps)
|
}), parentContext, deps)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -368,7 +369,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
run_in_background: true,
|
run_in_background: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundTask(args, ctx, {
|
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",
|
||||||
@@ -376,7 +377,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
}),
|
}),
|
||||||
getTask: () => undefined,
|
getTask: () => undefined,
|
||||||
},
|
},
|
||||||
} as any, parentContext, "Sisyphus-Junior", MODEL, undefined)
|
}), parentContext, "Sisyphus-Junior", MODEL, undefined)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -402,7 +403,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
|
|
||||||
await executeUnstableAgentTask(
|
await executeUnstableAgentTask(
|
||||||
args, ctx,
|
args, ctx,
|
||||||
{
|
unsafeTestValue({
|
||||||
manager: {
|
manager: {
|
||||||
launch: async () => launchedTask,
|
launch: async () => launchedTask,
|
||||||
getTask: () => launchedTask,
|
getTask: () => launchedTask,
|
||||||
@@ -419,7 +420,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
syncPollTimeoutMs: 100,
|
syncPollTimeoutMs: 100,
|
||||||
} as any,
|
}),
|
||||||
parentContext, "Sisyphus-Junior", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
parentContext, "Sisyphus-Junior", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -438,14 +439,14 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
load_skills: [], run_in_background: true, task_id: "ses_resume_title",
|
load_skills: [], run_in_background: true, task_id: "ses_resume_title",
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeBackgroundContinuation(args, ctx, {
|
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",
|
||||||
status: "running", sessionId: "ses_resume_title", model: MODEL,
|
status: "running", sessionId: "ses_resume_title", model: MODEL,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
} as any, parentContext)
|
}), parentContext)
|
||||||
|
|
||||||
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
const meta = ctx.captured.find((item: any) => item.metadata?.sessionId)
|
||||||
expect(meta).toBeDefined()
|
expect(meta).toBeDefined()
|
||||||
@@ -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, {
|
await executeSyncContinuation(args, ctx, unsafeTestValue({
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
messages: async () => ({
|
messages: async () => ({
|
||||||
@@ -469,7 +470,7 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
prompt: async () => ({}),
|
prompt: async () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any, parentContext, {
|
}), parentContext, {
|
||||||
pollSyncSession: async () => null,
|
pollSyncSession: async () => null,
|
||||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||||
})
|
})
|
||||||
@@ -500,8 +501,8 @@ describe("taskId and backgroundTaskId metadata consistency", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgOutput = createBackgroundOutput(manager as any, client as any)
|
const bgOutput = createBackgroundOutput(unsafeTestValue(manager), unsafeTestValue(client))
|
||||||
await bgOutput.execute({ task_id: "bg_output_xyz" } as any, ctx as any)
|
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()
|
||||||
|
|||||||
@@ -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,14 +19,14 @@ 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 = toolDefinition.args.category as unknown as {
|
const categorySchema = unsafeTestValue<{
|
||||||
def: {
|
def: {
|
||||||
type: string
|
type: string
|
||||||
innerType: {
|
innerType: {
|
||||||
def: { type: string }
|
def: { type: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}>(toolDefinition.args.category)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(categorySchema.def.type).toBe("optional")
|
expect(categorySchema.def.type).toBe("optional")
|
||||||
|
|||||||
@@ -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 = {
|
const executorContext = unsafeTestValue<Parameters<typeof executeUnstableAgentTask>[2]>({
|
||||||
manager: mockManager,
|
manager: mockManager,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
@@ -41,7 +42,7 @@ describe("executeUnstableAgentTask session permission", () => {
|
|||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof executeUnstableAgentTask>[2]
|
})
|
||||||
const parentContext = {
|
const parentContext = {
|
||||||
sessionID: "parent-session",
|
sessionID: "parent-session",
|
||||||
messageID: "msg_parent",
|
messageID: "msg_parent",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user