Files
oh-my-opencode/src/features/background-agent/task-poller.test.ts
T

193 lines
5.5 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, mock } from "bun:test"
import { checkAndInterruptStaleTasks, pruneStaleTasksAndNotifications } from "./task-poller"
import type { BackgroundTask } from "./types"
describe("checkAndInterruptStaleTasks", () => {
const mockClient = {
session: {
abort: mock(() => Promise.resolve()),
},
}
const mockConcurrencyManager = {
release: mock(() => {}),
}
const mockNotify = mock(() => Promise.resolve())
function createRunningTask(overrides: Partial<BackgroundTask> = {}): BackgroundTask {
return {
id: "task-1",
sessionID: "ses-1",
parentSessionID: "parent-ses-1",
parentMessageID: "msg-1",
description: "test",
prompt: "test",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 120_000),
...overrides,
}
}
it("should interrupt tasks with lastUpdate exceeding stale timeout", async () => {
//#given
const task = createRunningTask({
progress: {
toolCalls: 1,
lastUpdate: new Date(Date.now() - 200_000),
},
})
//#when
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { staleTimeoutMs: 180_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
})
//#then
expect(task.status).toBe("cancelled")
expect(task.error).toContain("Stale timeout")
})
it("should NOT interrupt tasks with recent lastUpdate", async () => {
//#given
const task = createRunningTask({
progress: {
toolCalls: 1,
lastUpdate: new Date(Date.now() - 10_000),
},
})
//#when
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { staleTimeoutMs: 180_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
})
//#then
expect(task.status).toBe("running")
})
it("should interrupt tasks with NO progress.lastUpdate that exceeded messageStalenessTimeoutMs since startedAt", async () => {
//#given — task started 15 minutes ago, never received any progress update
const task = createRunningTask({
startedAt: new Date(Date.now() - 15 * 60 * 1000),
progress: undefined,
})
//#when
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { messageStalenessTimeoutMs: 600_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
})
//#then
expect(task.status).toBe("cancelled")
expect(task.error).toContain("no activity")
})
it("should NOT interrupt tasks with NO progress.lastUpdate that are within messageStalenessTimeoutMs", async () => {
//#given — task started 5 minutes ago, default timeout is 10 minutes
const task = createRunningTask({
startedAt: new Date(Date.now() - 5 * 60 * 1000),
progress: undefined,
})
//#when
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { messageStalenessTimeoutMs: 600_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
})
//#then
expect(task.status).toBe("running")
})
it("should use DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS when messageStalenessTimeoutMs is not configured", async () => {
//#given — task started 15 minutes ago, no config for messageStalenessTimeoutMs
const task = createRunningTask({
startedAt: new Date(Date.now() - 15 * 60 * 1000),
progress: undefined,
})
//#when — default is 10 minutes (600_000ms)
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: undefined,
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
})
//#then
expect(task.status).toBe("cancelled")
expect(task.error).toContain("no activity")
})
it("should release concurrency key when interrupting a never-updated task", async () => {
//#given
const releaseMock = mock(() => {})
const task = createRunningTask({
startedAt: new Date(Date.now() - 15 * 60 * 1000),
progress: undefined,
concurrencyKey: "anthropic/claude-opus-4-6",
})
//#when
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { messageStalenessTimeoutMs: 600_000 },
concurrencyManager: { release: releaseMock } as never,
notifyParentSession: mockNotify,
})
//#then
expect(releaseMock).toHaveBeenCalledWith("anthropic/claude-opus-4-6")
expect(task.concurrencyKey).toBeUndefined()
})
})
describe("pruneStaleTasksAndNotifications", () => {
it("should prune tasks that exceeded TTL", () => {
//#given
const tasks = new Map<string, BackgroundTask>()
const oldTask: BackgroundTask = {
id: "old-task",
parentSessionID: "parent",
parentMessageID: "msg",
description: "old",
prompt: "old",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 31 * 60 * 1000),
}
tasks.set("old-task", oldTask)
const pruned: string[] = []
const notifications = new Map<string, BackgroundTask[]>()
//#when
pruneStaleTasksAndNotifications({
tasks,
notifications,
onTaskPruned: (taskId) => pruned.push(taskId),
})
//#then
expect(pruned).toContain("old-task")
})
})