274 lines
8.4 KiB
TypeScript
274 lines
8.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import type { Device, MasterAgentTask } from "../src/lib/boss-data";
|
|
|
|
let runtimeRoot = "";
|
|
let data: typeof import("../src/lib/boss-data");
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-task-reliability-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
data = await import("../src/lib/boss-data.ts");
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
function makeQueuedTask(taskId: string, overrides: Partial<MasterAgentTask> = {}): MasterAgentTask {
|
|
return {
|
|
taskId,
|
|
projectId: "master-agent",
|
|
taskType: "conversation_reply",
|
|
requestMessageId: `${taskId}-request`,
|
|
requestText: "回复一句收到",
|
|
executionPrompt: "回复一句收到",
|
|
requestedBy: "Boss 测试",
|
|
requestedByAccount: "krisolo",
|
|
deviceId: "mac-studio",
|
|
status: "queued",
|
|
requestedAt: new Date().toISOString(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("master agent task claim, progress, and complete maintain reliability phase", async () => {
|
|
await setup();
|
|
const state = await data.readState();
|
|
state.masterAgentTasks.unshift(makeQueuedTask("task-phase-normal"));
|
|
await data.writeState(state);
|
|
|
|
const claimed = await data.claimNextMasterAgentTask("mac-studio");
|
|
assert.equal(claimed?.taskId, "task-phase-normal");
|
|
assert.equal(claimed?.status, "running");
|
|
assert.equal(claimed?.phase, "claimed");
|
|
assert.ok(claimed?.lastProgressAt);
|
|
|
|
const progressed = await data.updateMasterAgentTaskProgress({
|
|
taskId: "task-phase-normal",
|
|
deviceId: "mac-studio",
|
|
status: "running",
|
|
phase: "awaiting_reply",
|
|
});
|
|
assert.equal(progressed.status, "running");
|
|
assert.equal(progressed.phase, "awaiting_reply");
|
|
assert.ok(progressed.leaseExpiresAt);
|
|
|
|
const completed = await data.completeMasterAgentTask({
|
|
taskId: "task-phase-normal",
|
|
deviceId: "mac-studio",
|
|
status: "completed",
|
|
replyBody: "收到。",
|
|
});
|
|
assert.equal(completed.status, "completed");
|
|
assert.equal(completed.phase, "completed");
|
|
assert.equal(completed.recoverable, false);
|
|
});
|
|
|
|
test("expired pre-turn task is safely requeued and claimed again", async () => {
|
|
await setup();
|
|
const state = await data.readState();
|
|
state.masterAgentTasks.unshift(
|
|
makeQueuedTask("task-phase-retry", {
|
|
status: "running",
|
|
phase: "executor_starting",
|
|
claimedAt: "2020-01-01T08:00:00.000Z",
|
|
lastProgressAt: "2020-01-01T08:00:00.000Z",
|
|
leaseExpiresAt: "2020-01-01T08:01:00.000Z",
|
|
attemptCount: 1,
|
|
maxAttempts: 2,
|
|
}),
|
|
);
|
|
await data.writeState(state);
|
|
|
|
const claimed = await data.claimNextMasterAgentTask("mac-studio");
|
|
assert.equal(claimed?.taskId, "task-phase-retry");
|
|
assert.equal(claimed?.status, "running");
|
|
assert.equal(claimed?.phase, "claimed");
|
|
assert.equal(claimed?.attemptCount, 2);
|
|
assert.equal(claimed?.recoverable, false);
|
|
});
|
|
|
|
test("expired task after turn start is timed out instead of duplicated", async () => {
|
|
await setup();
|
|
const state = await data.readState();
|
|
state.masterAgentTasks.unshift(
|
|
makeQueuedTask("task-phase-no-duplicate", {
|
|
status: "running",
|
|
phase: "turn_started",
|
|
claimedAt: "2020-01-01T08:00:00.000Z",
|
|
lastProgressAt: "2020-01-01T08:00:00.000Z",
|
|
leaseExpiresAt: "2020-01-01T08:01:00.000Z",
|
|
attemptCount: 1,
|
|
maxAttempts: 2,
|
|
}),
|
|
);
|
|
await data.writeState(state);
|
|
|
|
const claimed = await data.claimNextMasterAgentTask("mac-studio");
|
|
assert.notEqual(claimed?.taskId, "task-phase-no-duplicate");
|
|
|
|
const nextState = await data.readState();
|
|
const task = nextState.masterAgentTasks.find((item) => item.taskId === "task-phase-no-duplicate");
|
|
assert.equal(task?.status, "timed_out");
|
|
assert.equal(task?.phase, "timed_out");
|
|
assert.equal(task?.recoverable, false);
|
|
});
|
|
|
|
test("recoverable codex app-server runtime failure requeues conversation reply", async () => {
|
|
await setup();
|
|
const state = await data.readState();
|
|
state.masterAgentTasks.unshift(
|
|
makeQueuedTask("task-runtime-retry", {
|
|
projectId: "project-juyuwan",
|
|
targetProjectId: "project-juyuwan",
|
|
targetThreadId: "thread-juyuwan",
|
|
status: "running",
|
|
phase: "awaiting_reply",
|
|
claimedAt: "2026-06-07T06:05:16.000Z",
|
|
lastProgressAt: "2026-06-07T06:10:00.000Z",
|
|
leaseExpiresAt: "2026-06-07T06:20:16.000Z",
|
|
attemptCount: 1,
|
|
maxAttempts: 2,
|
|
}),
|
|
);
|
|
await data.writeState(state);
|
|
|
|
const completed = await data.completeMasterAgentTask({
|
|
taskId: "task-runtime-retry",
|
|
deviceId: "mac-studio",
|
|
status: "failed",
|
|
errorMessage: "CODEX_APP_SERVER_TURN_INTERRUPTED",
|
|
});
|
|
|
|
assert.equal(completed.status, "queued");
|
|
assert.equal(completed.phase, "recoverable_failed");
|
|
assert.equal(completed.recoverable, true);
|
|
assert.equal(completed.errorMessage, "CODEX_APP_SERVER_TURN_INTERRUPTED");
|
|
assert.equal(completed.attemptCount, 1);
|
|
assert.equal(completed.completedAt, undefined);
|
|
assert.equal(completed.claimedAt, undefined);
|
|
assert.ok(completed.nextRetryAt);
|
|
|
|
const claimed = await data.claimNextMasterAgentTask("mac-studio");
|
|
assert.equal(claimed?.taskId, "task-runtime-retry");
|
|
assert.equal(claimed?.attemptCount, 2);
|
|
});
|
|
|
|
test("recoverable codex app-server runtime failure becomes terminal after max attempts", async () => {
|
|
await setup();
|
|
const state = await data.readState();
|
|
state.masterAgentTasks.unshift(
|
|
makeQueuedTask("task-runtime-terminal", {
|
|
projectId: "project-juyuwan",
|
|
targetProjectId: "project-juyuwan",
|
|
targetThreadId: "thread-juyuwan",
|
|
status: "running",
|
|
phase: "awaiting_reply",
|
|
claimedAt: "2026-06-07T06:05:16.000Z",
|
|
lastProgressAt: "2026-06-07T06:10:00.000Z",
|
|
leaseExpiresAt: "2026-06-07T06:20:16.000Z",
|
|
attemptCount: 2,
|
|
maxAttempts: 2,
|
|
}),
|
|
);
|
|
await data.writeState(state);
|
|
|
|
const completed = await data.completeMasterAgentTask({
|
|
taskId: "task-runtime-terminal",
|
|
deviceId: "mac-studio",
|
|
status: "failed",
|
|
errorMessage: "CODEX_APP_SERVER_TIMEOUT",
|
|
});
|
|
|
|
assert.equal(completed.status, "failed");
|
|
assert.equal(completed.phase, "terminal_failed");
|
|
assert.equal(completed.recoverable, false);
|
|
assert.equal(completed.errorMessage, "CODEX_APP_SERVER_TIMEOUT");
|
|
assert.equal(completed.completedAt !== undefined, true);
|
|
});
|
|
|
|
test("codex app server health distinguishes available, degraded, and unavailable", async () => {
|
|
await setup();
|
|
assert.equal(data.resolveCodexAppServerHealth(undefined), "unavailable");
|
|
assert.equal(
|
|
data.resolveCodexAppServerHealth({
|
|
id: "device-offline",
|
|
name: "离线设备",
|
|
avatar: "D",
|
|
account: "krisolo",
|
|
source: "production",
|
|
status: "offline",
|
|
projects: [],
|
|
quota5h: 0,
|
|
quota7d: 0,
|
|
lastSeenAt: "2026-06-06T08:00:00.000Z",
|
|
endpoint: "",
|
|
token: "",
|
|
note: "",
|
|
capabilities: {
|
|
codexAppServer: {
|
|
connected: false,
|
|
lastSeenAt: "2026-06-06T08:00:00.000Z",
|
|
},
|
|
},
|
|
} satisfies Device),
|
|
"unavailable",
|
|
);
|
|
assert.equal(
|
|
data.resolveCodexAppServerHealth({
|
|
id: "device-degraded",
|
|
name: "降级设备",
|
|
avatar: "D",
|
|
account: "krisolo",
|
|
source: "production",
|
|
status: "online",
|
|
projects: [],
|
|
quota5h: 0,
|
|
quota7d: 0,
|
|
lastSeenAt: new Date().toISOString(),
|
|
endpoint: "",
|
|
token: "",
|
|
note: "",
|
|
capabilities: {
|
|
codexAppServer: {
|
|
connected: true,
|
|
lastSeenAt: new Date().toISOString(),
|
|
metadata: { errors: ["thread/turns/list:STDIN_CLOSED"] },
|
|
},
|
|
},
|
|
} satisfies Device),
|
|
"degraded",
|
|
);
|
|
assert.equal(
|
|
data.resolveCodexAppServerHealth({
|
|
id: "device-available",
|
|
name: "可用设备",
|
|
avatar: "D",
|
|
account: "krisolo",
|
|
source: "production",
|
|
status: "online",
|
|
projects: [],
|
|
quota5h: 0,
|
|
quota7d: 0,
|
|
lastSeenAt: new Date().toISOString(),
|
|
endpoint: "",
|
|
token: "",
|
|
note: "",
|
|
capabilities: {
|
|
codexAppServer: {
|
|
connected: true,
|
|
lastSeenAt: new Date().toISOString(),
|
|
metadata: {},
|
|
},
|
|
},
|
|
} satisfies Device),
|
|
"available",
|
|
);
|
|
});
|