Files
oh-my-opencode/src/hooks/unstable-agent-babysitter/index.test.ts
T
YeonGyu-Kim aa528e42c0 fix: propagate variant field in all promptAsync continuation paths (#3081)
All 5 continuation paths now send variant as top-level body field:
- boulder-continuation-injector.ts
- ralph-loop/continuation-prompt-injector.ts
- todo-continuation-enforcer/continuation-injection.ts
- unstable-agent-babysitter-hook.ts
- session-recovery/resume.ts

Plus type/helper updates in atlas, todo-continuation-enforcer,
unstable-agent-babysitter, and session-recovery.

TDD: 18 regression tests added, all pass. tsc clean.
2026-04-07 15:10:38 +09:00

259 lines
8.3 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test"
import { _resetForTesting, setMainSession } from "../../features/claude-code-session-state"
import type { BackgroundTask } from "../../features/background-agent"
import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker"
import { createUnstableAgentBabysitterHook } from "./index"
const projectDir = process.cwd()
type BabysitterContext = Parameters<typeof createUnstableAgentBabysitterHook>[0]
function createMockPluginInput(options: {
messagesBySession: Record<string, unknown[]>
promptCalls: Array<{ input: unknown }>
}): BabysitterContext {
const { messagesBySession, promptCalls } = options
return {
directory: projectDir,
client: {
session: {
messages: async ({ path }: { path: { id: string } }) => ({
data: messagesBySession[path.id] ?? [],
}),
prompt: async (input: unknown) => {
promptCalls.push({ input })
},
promptAsync: async (input: unknown) => {
promptCalls.push({ input })
},
},
},
}
}
function createBackgroundManager(tasks: BackgroundTask[]) {
return {
getTasksByParentSession: () => tasks,
}
}
function createTask(overrides: Partial<BackgroundTask> = {}): BackgroundTask {
return {
id: "task-1",
sessionID: "bg-1",
parentSessionID: "main-1",
parentMessageID: "msg-1",
description: "unstable task",
prompt: "run work",
agent: "test-agent",
status: "running",
progress: {
toolCalls: 1,
lastUpdate: new Date(),
lastMessage: "still working",
lastMessageAt: new Date(Date.now() - 121000),
},
model: { providerID: "google", modelID: "gemini-1.5" },
...overrides,
}
}
describe("unstable-agent-babysitter hook", () => {
afterEach(() => {
_resetForTesting()
})
test("fires reminder for hung gemini task", async () => {
// #given
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const ctx = createMockPluginInput({
messagesBySession: {
"main-1": [
{ info: { agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-4" } } },
],
"bg-1": [
{ info: { role: "assistant" }, parts: [{ type: "thinking", thinking: "deep thought" }] },
],
},
promptCalls,
})
const backgroundManager = createBackgroundManager([createTask()])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
// #when
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
// #then
expect(promptCalls.length).toBe(1)
const payload = promptCalls[0].input as { body?: { parts?: Array<{ text?: string }> } }
const text = payload.body?.parts?.[0]?.text ?? ""
expect(text).toContain("background_output")
expect(text).toContain("background_cancel")
expect(text).toContain("deep thought")
expect(text).toContain(OMO_INTERNAL_INITIATOR_MARKER)
})
test("fires reminder for hung minimax task", async () => {
// #given
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const ctx = createMockPluginInput({
messagesBySession: {
"main-1": [
{ info: { agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-4" } } },
],
"bg-1": [
{ info: { role: "assistant" }, parts: [{ type: "thinking", thinking: "minimax thought" }] },
],
},
promptCalls,
})
const backgroundManager = createBackgroundManager([
createTask({ model: { providerID: "minimax", modelID: "minimax-1" } }),
])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
// #when
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
// #then
expect(promptCalls.length).toBe(1)
const payload = promptCalls[0].input as { body?: { parts?: Array<{ text?: string }> } }
const text = payload.body?.parts?.[0]?.text ?? ""
expect(text).toContain("background_output")
expect(text).toContain("background_cancel")
expect(text).toContain("minimax thought")
expect(text).toContain(OMO_INTERNAL_INITIATOR_MARKER)
})
test("does not remind stable model tasks", async () => {
// #given
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const ctx = createMockPluginInput({
messagesBySession: { "main-1": [] },
promptCalls,
})
const backgroundManager = createBackgroundManager([
createTask({ model: { providerID: "openai", modelID: "gpt-4" } }),
])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
// #when
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
// #then
expect(promptCalls.length).toBe(0)
})
test("respects per-task cooldown", async () => {
// #given
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const ctx = createMockPluginInput({
messagesBySession: { "main-1": [], "bg-1": [] },
promptCalls,
})
const backgroundManager = createBackgroundManager([createTask()])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
const now = Date.now()
const originalNow = Date.now
Date.now = () => now
// #when
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
// #then
expect(promptCalls.length).toBe(1)
Date.now = originalNow
})
test("skips follow-up reminder after the main session is cancelled", async () => {
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const ctx = createMockPluginInput({
messagesBySession: {
"main-1": [
{ info: { agent: "sisyphus", model: { providerID: "openai", modelID: "gpt-4" } } },
],
"bg-1": [
{ info: { role: "assistant" }, parts: [{ type: "thinking", thinking: "deep thought" }] },
],
},
promptCalls,
})
const backgroundManager = createBackgroundManager([createTask()])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
const firstNow = Date.now()
const originalNow = Date.now
let currentNow = firstNow
Date.now = () => currentNow
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
await hook.event({ event: { type: "session.error", properties: { sessionID: "main-1", error: { name: "AbortError" } } } })
currentNow += 5 * 60 * 1000 + 1
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
expect(promptCalls.length).toBe(1)
Date.now = originalNow
})
test("#given the main session model includes variant #when injecting a babysitter reminder #then promptAsync receives variant as a top-level field", async () => {
// given
setMainSession("main-1")
const promptCalls: Array<{ input: unknown }> = []
const mainModel = {
providerID: "openai",
modelID: "gpt-4",
variant: "max",
}
const ctx = createMockPluginInput({
messagesBySession: {
"main-1": [
{ info: { agent: "sisyphus", model: mainModel } },
],
"bg-1": [
{ info: { role: "assistant" }, parts: [{ type: "thinking", thinking: "deep thought" }] },
],
},
promptCalls,
})
const backgroundManager = createBackgroundManager([createTask()])
const hook = createUnstableAgentBabysitterHook(ctx, {
backgroundManager,
config: { timeout_ms: 120000 },
})
// when
await hook.event({ event: { type: "session.idle", properties: { sessionID: "main-1" } } })
// then
expect(promptCalls.length).toBe(1)
const payload = promptCalls[0].input as {
body?: {
model?: { providerID: string; modelID: string }
variant?: string
}
}
expect(payload.body?.model).toEqual({ providerID: "openai", modelID: "gpt-4" })
expect(payload.body?.variant).toBe("max")
})
})