247 lines
7.9 KiB
TypeScript
247 lines
7.9 KiB
TypeScript
declare const require: (name: string) => any
|
|
const { describe, test, expect, beforeEach, afterEach } = require("bun:test")
|
|
import { __setTimingConfig, __resetTimingConfig, getTimingConfig } from "./timing"
|
|
|
|
function createMockCtx(aborted = false) {
|
|
const controller = new AbortController()
|
|
if (aborted) controller.abort()
|
|
return {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "test-agent",
|
|
abort: controller.signal,
|
|
}
|
|
}
|
|
|
|
function createNeverCompleteClient(sessionID: string, onAbort?: () => void) {
|
|
return {
|
|
session: {
|
|
abort: async () => {
|
|
onAbort?.()
|
|
},
|
|
messages: async () => ({
|
|
data: [{ info: { id: "msg_001", role: "user", time: { created: 1000 } } }],
|
|
}),
|
|
status: async () => ({ data: { [sessionID]: { type: "idle" } } }),
|
|
},
|
|
}
|
|
}
|
|
|
|
async function withMockedDateNow(stepMs: number, run: () => Promise<void>) {
|
|
const originalDateNow = Date.now
|
|
let now = 0
|
|
|
|
Date.now = () => {
|
|
const current = now
|
|
now += stepMs
|
|
return current
|
|
}
|
|
|
|
try {
|
|
await run()
|
|
} finally {
|
|
Date.now = originalDateNow
|
|
}
|
|
}
|
|
|
|
describe("syncPollTimeoutMs threading", () => {
|
|
beforeEach(() => {
|
|
__setTimingConfig({
|
|
POLL_INTERVAL_MS: 10,
|
|
MIN_STABILITY_TIME_MS: 0,
|
|
STABILITY_POLLS_REQUIRED: 1,
|
|
MAX_POLL_TIME_MS: 5000,
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
__resetTimingConfig()
|
|
})
|
|
|
|
describe("#given pollSyncSession timeoutMs input", () => {
|
|
describe("#when custom timeout is provided", () => {
|
|
test("#then custom timeout value is used", async () => {
|
|
const { pollSyncSession } = require("./sync-session-poller")
|
|
let abortCount = 0
|
|
const mockClient = createNeverCompleteClient("ses_custom", () => {
|
|
abortCount++
|
|
})
|
|
|
|
await withMockedDateNow(60_000, async () => {
|
|
const result = await pollSyncSession(createMockCtx(), mockClient, {
|
|
sessionID: "ses_custom",
|
|
agentToUse: "test-agent",
|
|
toastManager: null,
|
|
taskId: undefined,
|
|
}, 120_000)
|
|
|
|
expect(result).toBe("Poll inactivity timeout reached after 120000ms without active OpenCode status for session ses_custom")
|
|
expect(abortCount).toBe(1)
|
|
})
|
|
})
|
|
|
|
test("#then active OpenCode statuses do not consume the inactivity timeout", async () => {
|
|
const { pollSyncSession } = require("./sync-session-poller")
|
|
let abortCount = 0
|
|
let statusCallCount = 0
|
|
let messageCallCount = 0
|
|
const mockClient = {
|
|
session: {
|
|
abort: async () => {
|
|
abortCount++
|
|
},
|
|
messages: async () => {
|
|
messageCallCount++
|
|
return {
|
|
data: [
|
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
|
{
|
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "stop" },
|
|
parts: [{ type: "text", text: "done" }],
|
|
},
|
|
],
|
|
}
|
|
},
|
|
status: async () => {
|
|
statusCallCount++
|
|
if (statusCallCount === 1) return { data: { ses_active: { type: "busy" } } }
|
|
if (statusCallCount === 2) return { data: { ses_active: { type: "retry" } } }
|
|
return { data: { ses_active: { type: "idle" } } }
|
|
},
|
|
},
|
|
}
|
|
|
|
await withMockedDateNow(60_000, async () => {
|
|
const result = await pollSyncSession(createMockCtx(), mockClient, {
|
|
sessionID: "ses_active",
|
|
agentToUse: "oracle",
|
|
toastManager: null,
|
|
taskId: undefined,
|
|
}, 120_000)
|
|
|
|
expect(result).toBeNull()
|
|
expect(abortCount).toBe(0)
|
|
expect(statusCallCount).toBe(3)
|
|
expect(messageCallCount).toBe(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("#when timeoutMs is omitted", () => {
|
|
test("#then default timeout constant is used", async () => {
|
|
const { pollSyncSession } = require("./sync-session-poller")
|
|
const mockClient = createNeverCompleteClient("ses_default")
|
|
const { MAX_POLL_TIME_MS } = getTimingConfig()
|
|
|
|
await withMockedDateNow(300_000, async () => {
|
|
const result = await pollSyncSession(createMockCtx(), mockClient, {
|
|
sessionID: "ses_default",
|
|
agentToUse: "test-agent",
|
|
toastManager: null,
|
|
taskId: undefined,
|
|
})
|
|
|
|
expect(result).toBe(`Poll inactivity timeout reached after ${MAX_POLL_TIME_MS}ms without active OpenCode status for session ses_default`)
|
|
})
|
|
})
|
|
|
|
test("#then MAX_POLL_TIME_MS override is respected for backward compatibility", async () => {
|
|
const { pollSyncSession } = require("./sync-session-poller")
|
|
const mockClient = createNeverCompleteClient("ses_legacy")
|
|
|
|
__setTimingConfig({ MAX_POLL_TIME_MS: 120_000 })
|
|
|
|
await withMockedDateNow(60_000, async () => {
|
|
const result = await pollSyncSession(createMockCtx(), mockClient, {
|
|
sessionID: "ses_legacy",
|
|
agentToUse: "test-agent",
|
|
toastManager: null,
|
|
taskId: undefined,
|
|
})
|
|
|
|
expect(result).toBe("Poll inactivity timeout reached after 120000ms without active OpenCode status for session ses_legacy")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("#when timeoutMs is lower than minimum guard", () => {
|
|
test("#then minimum 50ms timeout is enforced", async () => {
|
|
const { pollSyncSession } = require("./sync-session-poller")
|
|
const mockClient = createNeverCompleteClient("ses_guard")
|
|
|
|
await withMockedDateNow(25, async () => {
|
|
const result = await pollSyncSession(createMockCtx(), mockClient, {
|
|
sessionID: "ses_guard",
|
|
agentToUse: "test-agent",
|
|
toastManager: null,
|
|
taskId: undefined,
|
|
}, 10)
|
|
|
|
expect(result).toBe("Poll inactivity timeout reached after 50ms without active OpenCode status for session ses_guard")
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("#given unstable-agent-task path", () => {
|
|
describe("#when syncPollTimeoutMs is set in executor context", () => {
|
|
test("#then unstable path uses configured timeout budget", async () => {
|
|
const { executeUnstableAgentTask } = require("./unstable-agent-task")
|
|
|
|
let statusCallCount = 0
|
|
const mockClient = {
|
|
session: {
|
|
status: async () => {
|
|
statusCallCount++
|
|
return { data: { ses_unstable: { type: "idle" } } }
|
|
},
|
|
messages: async () => ({
|
|
data: [
|
|
{
|
|
info: { id: "msg_001", role: "assistant", time: { created: 2000 } },
|
|
parts: [{ type: "text", text: "unstable path done" }],
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
}
|
|
|
|
const mockManager = {
|
|
launch: async () => ({ id: "task_001", sessionId: "ses_unstable", status: "running" }),
|
|
getTask: () => ({ id: "task_001", sessionId: "ses_unstable", status: "running" }),
|
|
}
|
|
|
|
const result = await executeUnstableAgentTask(
|
|
{
|
|
description: "unstable timeout threading",
|
|
prompt: "run",
|
|
category: "unspecified-low",
|
|
run_in_background: false,
|
|
load_skills: [],
|
|
command: undefined,
|
|
},
|
|
createMockCtx(),
|
|
{
|
|
manager: mockManager,
|
|
client: mockClient,
|
|
syncPollTimeoutMs: 0,
|
|
},
|
|
{
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
model: "gpt-test",
|
|
agent: "test-agent",
|
|
},
|
|
"test-agent",
|
|
undefined,
|
|
undefined,
|
|
"gpt-test"
|
|
)
|
|
|
|
expect(statusCallCount).toBe(0)
|
|
expect(result).toContain("SUPERVISED TASK TIMED OUT")
|
|
})
|
|
})
|
|
})
|
|
})
|