123 lines
4.2 KiB
TypeScript
123 lines
4.2 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 { NextRequest } from "next/server";
|
|
import type { MasterAgentTask } from "../src/lib/boss-data";
|
|
|
|
let runtimeRoot = "";
|
|
let data: typeof import("../src/lib/boss-data.ts");
|
|
let authCookie = "";
|
|
let getRecovery: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/recovery/route.ts"))["GET"];
|
|
let postRecovery: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/recovery/route.ts"))["POST"];
|
|
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data.ts")["readState"]>>;
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-task-recovery-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
const [dataModule, authModule, routeModule] = await Promise.all([
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
import("../src/app/api/v1/master-agent/tasks/[taskId]/recovery/route.ts"),
|
|
]);
|
|
data = dataModule;
|
|
authCookie = authModule.AUTH_SESSION_COOKIE;
|
|
getRecovery = routeModule.GET;
|
|
postRecovery = routeModule.POST;
|
|
baseState = structuredClone(await data.readState());
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
function task(overrides: Partial<MasterAgentTask>): MasterAgentTask {
|
|
return {
|
|
taskId: "task-recoverable",
|
|
projectId: "project-1",
|
|
taskType: "conversation_reply",
|
|
requestMessageId: "msg-1",
|
|
requestText: "继续执行",
|
|
executionPrompt: "继续执行",
|
|
requestedBy: "Boss",
|
|
requestedByAccount: "owner@boss.com",
|
|
deviceId: "mac-1",
|
|
status: "running",
|
|
phase: "executor_starting",
|
|
requestedAt: "2026-06-06T08:00:00.000Z",
|
|
lastProgressAt: "2026-06-06T08:01:00.000Z",
|
|
attemptCount: 1,
|
|
maxAttempts: 2,
|
|
recoverable: true,
|
|
lastErrorCode: "EXECUTOR_START_FAILED",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function authedRequest(method = "GET", body?: unknown) {
|
|
const session = await data.createAuthSession({
|
|
account: "owner@boss.com",
|
|
role: "highest_admin",
|
|
displayName: "Owner",
|
|
loginMethod: "password",
|
|
});
|
|
return new NextRequest("http://127.0.0.1:3000/api/v1/master-agent/tasks/task-recoverable/recovery", {
|
|
method,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${authCookie}=${session.sessionToken}`,
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await setup();
|
|
const state = structuredClone(baseState);
|
|
state.authAccounts = [
|
|
{
|
|
id: "account-owner",
|
|
account: "owner@boss.com",
|
|
passwordHash: "secret",
|
|
displayName: "Owner",
|
|
role: "highest_admin",
|
|
createdAt: "2026-06-06T08:00:00.000Z",
|
|
updatedAt: "2026-06-06T08:00:00.000Z",
|
|
},
|
|
];
|
|
state.masterAgentTasks = [task({})];
|
|
await data.writeState(state);
|
|
});
|
|
|
|
test("task recovery GET returns safe diagnosis", async () => {
|
|
const response = await getRecovery(
|
|
await authedRequest(),
|
|
{ params: Promise.resolve({ taskId: "task-recoverable" }) },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.recovery.taskId, "task-recoverable");
|
|
assert.equal(payload.recovery.canRetry, true);
|
|
assert.equal(payload.recovery.safeNextAction, "retry");
|
|
assert.equal(payload.recovery.diagnosis.includes("executor_starting"), true);
|
|
});
|
|
|
|
test("task recovery POST retry requeues only recoverable pre-turn task", async () => {
|
|
const response = await postRecovery(
|
|
await authedRequest("POST", { action: "retry", reason: "executor recovered" }),
|
|
{ params: Promise.resolve({ taskId: "task-recoverable" }) },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.task.status, "queued");
|
|
assert.equal(payload.task.phase, "queued");
|
|
|
|
const state = await data.readState();
|
|
assert.equal(state.permissionAuditLogs.some((log) => log.action === "master_agent.task_retried"), true);
|
|
});
|