Files
boss/tests/dispatch-execution-result.test.ts

273 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
let runtimeRoot = "";
let postMessageRoute: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route"))["POST"];
let confirmDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route"))["POST"];
let completeMasterTaskRoute: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let createProjectGroupChat: (typeof import("../src/lib/boss-data"))["createProjectGroupChat"];
let isDispatchableThreadProject: (typeof import("../src/lib/boss-data"))["isDispatchableThreadProject"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let AUTH_SESSION_COOKIE = "";
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-task5-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [messageModule, confirmModule, completeModule, data, auth] = await Promise.all([
import("../src/app/api/v1/projects/[projectId]/messages/route.ts"),
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route.ts"),
import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
postMessageRoute = messageModule.POST;
confirmDispatchPlanRoute = confirmModule.POST;
completeMasterTaskRoute = completeModule.POST;
createAuthSession = data.createAuthSession;
createProjectGroupChat = data.createProjectGroupChat;
isDispatchableThreadProject = data.isDispatchableThreadProject;
readState = data.readState;
writeState = data.writeState;
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
async function createAuthedRequest(url: string, method: "POST", body: unknown) {
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return new NextRequest(url, {
method,
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify(body),
});
}
async function ensureTwoSingleThreadProjects() {
const state = await readState();
const singles = state.projects.filter((project) => isDispatchableThreadProject(project));
if (singles.length >= 2) {
return singles;
}
assert.ok(singles[0], "expected at least one seeded single-thread project");
const seed = singles[0];
const clonedProject = {
...seed,
id: "boss-console-clone",
name: "Boss 移动控制台副线程",
deviceIds: [...seed.deviceIds],
updatedAt: "2026-03-30T10:00:00+08:00",
lastMessageAt: "2026-03-30T10:00:00+08:00",
preview: "副线程等待主 Agent 汇总阻塞点。",
threadMeta: {
...seed.threadMeta,
projectId: "boss-console-clone",
threadId: "thread-boss-ui-clone",
threadDisplayName: "南区试产线回归",
folderName: "阻塞梳理",
updatedAt: "2026-03-30T10:00:00+08:00",
codexThreadRef: "thread-boss-ui-clone",
codexFolderRef: "boss-console-clone",
},
groupMembers: [],
messages: [
{
id: "msg-boss-console-clone",
sender: "device" as const,
senderLabel: "Win GPU / Codex",
body: "这里还在等待视觉链路复核。",
sentAt: "2026-03-30T10:00:00+08:00",
kind: "text" as const,
},
],
goals: [],
versions: [],
};
await writeState({
...state,
projects: [...state.projects, clonedProject],
});
const nextState = await readState();
return nextState.projects.filter((project) => isDispatchableThreadProject(project));
}
async function createConfirmedDispatchExecution() {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
const groupProject = await createProjectGroupChat({
sourceProjectId: memberProjects[0].id,
memberProjectIds: [memberProjects[1].id],
createdBy: "17600003315",
});
const messageResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/messages`,
"POST",
{ body: "请主 Agent 推荐要先同步的线程" },
),
{ params: Promise.resolve({ projectId: groupProject.id }) },
);
assert.equal(messageResponse.status, 200);
const messagePayload = (await messageResponse.json()) as {
dispatchPlan: { planId: string; targets: Array<{ projectId: string }> } | null;
};
assert.ok(messagePayload.dispatchPlan, "expected seeded dispatch plan");
const approvedTargetProjectId = messagePayload.dispatchPlan.targets[0]?.projectId;
assert.ok(approvedTargetProjectId, "expected approved target");
const confirmResponse = await confirmDispatchPlanRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-plans/${messagePayload.dispatchPlan.planId}/confirm`,
"POST",
{ approvedTargetProjectIds: [approvedTargetProjectId] },
),
{
params: Promise.resolve({
projectId: groupProject.id,
planId: messagePayload.dispatchPlan.planId,
}),
},
);
assert.equal(confirmResponse.status, 200);
const state = await readState();
const execution = state.dispatchExecutions.find(
(item) =>
item.planId === messagePayload.dispatchPlan?.planId &&
item.targetProjectId === approvedTargetProjectId,
);
assert.ok(execution, "expected queued dispatch execution");
const executionTask = state.masterAgentTasks.find(
(task) =>
task.taskType === "dispatch_execution" &&
task.projectId === groupProject.id &&
task.requestMessageId === messagePayload.dispatchPlan?.planId,
);
assert.ok(executionTask, "expected a queued dispatch execution master-agent task");
return { groupProject, execution, executionTask };
}
test("POST /api/v1/master-agent/tasks/[taskId]/complete mirrors raw thread replies to the group chat and appends a master-agent summary", async () => {
const { groupProject, execution, executionTask } = await createConfirmedDispatchExecution();
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${executionTask.taskId}/complete`,
"POST",
{
deviceId: execution.deviceId,
status: "completed",
dispatchExecutionId: execution.executionId,
targetProjectId: execution.targetProjectId,
targetThreadId: execution.targetThreadId,
rawThreadReply: "线程A已经完成阻塞点整理待你确认最终回滚窗口。",
replyBody: "主 Agent 汇总线程A已返回阻塞点整理下一步建议安排回滚窗口确认。",
},
),
{ params: Promise.resolve({ taskId: executionTask.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const completedExecution = nextState.dispatchExecutions.find(
(item) => item.executionId === execution.executionId,
);
assert.equal(completedExecution?.status, "completed");
assert.ok(completedExecution?.resultMessageId, "expected raw result message id to be recorded");
const groupMessages = nextState.projects.find((project) => project.id === groupProject.id)?.messages ?? [];
const mirroredDeviceReply = groupMessages.find(
(message) =>
message.sender === "device" &&
message.body.includes("线程A已经完成阻塞点整理"),
);
assert.ok(mirroredDeviceReply, "expected raw thread reply to be mirrored back to the group chat");
const masterSummary = groupMessages.find(
(message) =>
message.sender === "master" &&
message.body.includes("主 Agent 汇总线程A已返回阻塞点整理"),
);
assert.ok(masterSummary, "expected master-agent summary to be appended after the raw thread reply");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete is idempotent for repeated dispatch execution completions", async () => {
const { groupProject, execution, executionTask } = await createConfirmedDispatchExecution();
const completionBody = {
deviceId: execution.deviceId,
status: "completed" as const,
dispatchExecutionId: execution.executionId,
targetProjectId: execution.targetProjectId,
targetThreadId: execution.targetThreadId,
rawThreadReply: "线程A已经完成阻塞点整理待你确认最终回滚窗口。",
replyBody: "主 Agent 汇总线程A已返回阻塞点整理下一步建议安排回滚窗口确认。",
};
const firstResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${executionTask.taskId}/complete`,
"POST",
completionBody,
),
{ params: Promise.resolve({ taskId: executionTask.taskId }) },
);
assert.equal(firstResponse.status, 200);
const secondResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${executionTask.taskId}/complete`,
"POST",
completionBody,
),
{ params: Promise.resolve({ taskId: executionTask.taskId }) },
);
assert.equal(secondResponse.status, 200);
const nextState = await readState();
const groupMessages = nextState.projects.find((project) => project.id === groupProject.id)?.messages ?? [];
const mirroredReplies = groupMessages.filter(
(message) =>
message.sender === "device" &&
message.body.includes("线程A已经完成阻塞点整理"),
);
const masterSummaries = groupMessages.filter(
(message) =>
message.sender === "master" &&
message.body.includes("主 Agent 汇总线程A已返回阻塞点整理"),
);
assert.equal(mirroredReplies.length, 1);
assert.equal(masterSummaries.length, 1);
});