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

335 lines
13 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: "krisolo",
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;
}
const buildSingleThreadProject = (projectId: string, threadDisplayName: string) => ({
id: projectId,
name: threadDisplayName,
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: `${threadDisplayName} 等待主 Agent 汇总阻塞点。`,
updatedAt: "2026-03-30T10:00:00+08:00",
lastMessageAt: "2026-03-30T10:00:00+08:00",
isGroup: false,
threadMeta: {
projectId,
threadId: `${projectId}-thread`,
threadDisplayName,
folderName: "阻塞梳理",
activityIconCount: 0,
updatedAt: "2026-03-30T10:00:00+08:00",
codexThreadRef: `${projectId}-thread`,
codexFolderRef: `/Users/kris/code/${projectId}`,
},
groupMembers: [],
messages: [
{
id: `msg-${projectId}`,
sender: "device" as const,
senderLabel: "Win GPU / Codex",
body: "这里还在等待视觉链路复核。",
sentAt: "2026-03-30T10:00:00+08:00",
kind: "text" as const,
},
],
goals: [],
versions: [],
createdByAgent: true,
collaborationMode: "development" as const,
approvalState: "not_required" as const,
unreadCount: 0,
riskLevel: "low" as const,
});
const missingProjects = [
!singles[0] ? buildSingleThreadProject("dispatch-thread-a", "北区试产线回归") : null,
!singles[1] ? buildSingleThreadProject("dispatch-thread-b", "南区试产线回归") : null,
].filter(Boolean);
await writeState({
...state,
projects: [...state.projects, ...missingProjects],
});
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: "krisolo",
});
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");
assert.ok(executionTask?.executionPrompt?.includes("请主 Agent 推荐要先同步的线程"));
assert.ok(executionTask?.executionPrompt?.includes(executionTask?.targetThreadDisplayName ?? ""));
assert.ok(!executionTask?.executionPrompt?.includes("groupProjectId:"), "dispatch prompt should not include group project id labels");
assert.ok(!executionTask?.executionPrompt?.includes("threadProjectId:"), "dispatch prompt should not include thread project id labels");
assert.ok(!executionTask?.executionPrompt?.includes("threadId:"), "dispatch prompt should not include raw thread id labels");
assert.ok(!executionTask?.executionPrompt?.includes("folderName:"), "dispatch prompt should not include folder labels");
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");
const targetThreadMessages =
nextState.projects.find((project) => project.id === execution.targetProjectId)?.messages ?? [];
const mirroredThreadReply = targetThreadMessages.find(
(message) =>
message.sender === "device" &&
message.body.includes("线程A已经完成阻塞点整理"),
);
assert.ok(
mirroredThreadReply,
"expected raw thread reply to also be mirrored back to the target single-thread conversation",
);
});
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);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete blocks leaked thread environment diagnostics from group dispatch results", 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:
"我不能直接把当前会话环境从只读改回可写。cwd 我可以在命令里指向 /Users/kris/code/gptpluscontrol但现在真正卡住的是只读权限。",
},
),
{ params: Promise.resolve({ taskId: executionTask.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const groupMessages = nextState.projects.find((project) => project.id === groupProject.id)?.messages ?? [];
const leakedReply = groupMessages.find((message) =>
message.body.includes("当前会话环境从只读改回可写"),
);
assert.equal(leakedReply, undefined);
const opsNotice = groupMessages.find((message) =>
message.body.includes("线程环境异常,请重新绑定到正确项目或工作目录后再试。"),
);
assert.ok(opsNotice, "expected a system notice instead of raw leaked diagnostics");
});