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>; 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 { 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); });