Files
boss/tests/group-message-dispatch-plan.test.ts

585 lines
21 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 POST: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let createIndependentGroupChat: (typeof import("../src/lib/boss-data"))["createIndependentGroupChat"];
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let AUTH_SESSION_COOKIE: string;
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-task3-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [{ POST: routePost }, data, auth] = await Promise.all([
import("../src/app/api/v1/projects/[projectId]/messages/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
POST = routePost;
createAuthSession = data.createAuthSession;
createIndependentGroupChat = data.createIndependentGroupChat;
saveAiAccount = data.saveAiAccount;
updateProjectAgentControls = data.updateProjectAgentControls;
readState = data.readState;
writeState = data.writeState;
baseState = structuredClone(await readState());
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test.beforeEach(async () => {
await setup();
await writeState(structuredClone(baseState));
});
function buildDispatchableThreadProject({
id,
projectName,
threadDisplayName,
body,
}: {
id: string;
projectName: string;
threadDisplayName: string;
body: string;
}) {
return {
id,
name: projectName,
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: body,
updatedAt: "2026-03-30T10:00:00+08:00",
lastMessageAt: "2026-03-30T10:00:00+08:00",
isGroup: false,
threadMeta: {
projectId: id,
threadId: `thread-${id}`,
threadDisplayName,
folderName: "阻塞梳理",
activityIconCount: 0,
updatedAt: "2026-03-30T10:00:00+08:00",
codexThreadRef: `thread-${id}`,
codexFolderRef: `/Users/kris/code/${id}`,
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development" as const,
approvalState: "not_required" as const,
unreadCount: 0,
riskLevel: "low" as const,
messages: [
{
id: `msg-${id}`,
sender: "device" as const,
senderLabel: "Mac Studio / Codex",
body,
sentAt: "2026-03-30T10:00:00+08:00",
kind: "text" as const,
},
],
goals: [],
versions: [],
};
}
async function createAuthedRequest(projectId: string, body: { body: string; kind?: string }) {
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return new NextRequest(`http://127.0.0.1:3000/api/v1/projects/${projectId}/messages`, {
method: "POST",
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify(body),
});
}
async function ensureTwoSingleThreadProjects() {
const state = await readState();
const primaryProject = {
...buildDispatchableThreadProject({
id: "dispatch-thread-a",
projectName: "北区试产线主线程",
threadDisplayName: "北区试产线回归",
body: "主线程还在等待主 Agent 汇总阻塞点。",
}),
};
const secondaryProject = {
...buildDispatchableThreadProject({
id: "dispatch-thread-b",
projectName: "南区试产线主线程",
threadDisplayName: "南区试产线回归",
body: "副线程还在等待视觉链路复核。",
}),
};
await writeState({
...state,
projects: state.projects
.filter((project) => !["dispatch-thread-a", "dispatch-thread-b"].includes(project.id))
.concat(primaryProject, secondaryProject),
});
return [primaryProject, secondaryProject];
}
test("POST /api/v1/projects/[projectId]/messages returns a dispatch plan for group text messages", async () => {
await setup();
await saveAiAccount({
accountId: "master-codex-smart-policy",
label: "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: "Mac 上的 Master Codex Node",
nodeId: "local-codex-node",
nodeLabel: "本机 Codex",
model: "gpt-5.4-mini",
enabled: true,
setActive: true,
loginStatusNote: "用于深度任务模型策略测试。",
});
await updateProjectAgentControls("master-agent", {
fastModelOverride: "gpt-5.4-mini",
fastReasoningEffortOverride: "low",
smartModelOverride: "gpt-5.4",
smartReasoningEffortOverride: "high",
});
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const groupProject = await createIndependentGroupChat({
memberProjectIds: [memberProjects[0].id, memberProjects[1].id],
createdBy: "17600003315",
});
const response = await POST(await createAuthedRequest(groupProject.id, { body: "请大家汇总今天的阻塞点" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
message: { id: string; body: string };
dispatchPlan: null | {
groupProjectId: string;
requestMessageId: string;
status: string;
targets: Array<{ projectId: string }>;
summary: string;
};
collaborationGate: { isGroup: boolean };
};
assert.equal(payload.ok, true);
assert.equal(payload.message.body, "请大家汇总今天的阻塞点");
assert.ok(payload.dispatchPlan, "expected dispatch plan in response");
assert.equal(payload.dispatchPlan?.groupProjectId, groupProject.id);
assert.equal(payload.dispatchPlan?.requestMessageId, payload.message.id);
assert.equal(payload.dispatchPlan?.status, "pending_user_confirmation");
assert.equal(payload.dispatchPlan?.targets.length, groupProject.groupMembers.length);
assert.match(payload.dispatchPlan?.summary ?? "", /阻塞点/);
assert.equal(payload.collaborationGate.isGroup, true);
const nextState = await readState();
const queuedGroupDispatchTasks = nextState.masterAgentTasks.filter(
(task) =>
task.projectId === groupProject.id &&
task.requestMessageId === payload.message.id &&
task.taskType === "group_dispatch_plan",
);
assert.equal(
queuedGroupDispatchTasks.length,
1,
"expected group messages to enqueue a master-agent dispatch recommendation task",
);
assert.equal(queuedGroupDispatchTasks[0]?.executionModel, "gpt-5.4");
assert.equal(queuedGroupDispatchTasks[0]?.executionReasoningEffort, "high");
});
test("POST /api/v1/projects/[projectId]/messages keeps dispatchPlan null for single-thread projects", async () => {
await setup();
const [singleProject] = await ensureTwoSingleThreadProjects();
assert.ok(singleProject, "expected a synthetic single-thread project");
const response = await POST(await createAuthedRequest(singleProject.id, { body: "单线程消息" }), {
params: Promise.resolve({ projectId: singleProject.id }),
});
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
message: { body: string };
dispatchPlan: null;
collaborationGate: { isGroup: boolean };
};
assert.equal(payload.ok, true);
assert.equal(payload.message.body, "单线程消息");
assert.equal(payload.dispatchPlan, null);
assert.equal(payload.collaborationGate.isGroup, false);
});
test("POST /api/v1/projects/master-agent/messages returns a dispatch plan for thread-operation requests", async () => {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const response = await POST(
await createAuthedRequest("master-agent", {
body: "请操作真实线程先让北区试产线回归只回复主Agent线程操作正常",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
message: { id: string; body: string };
dispatchPlan: null | {
groupProjectId: string;
requestMessageId: string;
status: string;
targets: Array<{ projectId: string }>;
summary: string;
};
masterReply?: { ok: boolean; taskId?: string };
collaborationGate: { isGroup: boolean };
};
assert.equal(payload.ok, true);
assert.equal(payload.message.body, "请操作真实线程先让北区试产线回归只回复主Agent线程操作正常");
assert.ok(payload.dispatchPlan, "expected dispatch plan in master-agent response");
assert.equal(payload.dispatchPlan?.groupProjectId, "master-agent");
assert.equal(payload.dispatchPlan?.requestMessageId, payload.message.id);
assert.equal(payload.dispatchPlan?.status, "pending_user_confirmation");
assert.ok(
(payload.dispatchPlan?.targets ?? []).some((target) => target.projectId === "dispatch-thread-a"),
"expected dispatch plan to include the matching real thread target",
);
assert.match(payload.dispatchPlan?.summary ?? "", /北区试产线回归|主 Agent/);
assert.equal(payload.collaborationGate.isGroup, false);
assert.equal(payload.masterReply?.ok, true);
const nextState = await readState();
const queuedDispatchTask = nextState.masterAgentTasks.find(
(task) =>
task.projectId === "master-agent" &&
task.requestMessageId === payload.message.id &&
task.taskType === "group_dispatch_plan",
);
assert.ok(queuedDispatchTask, "expected master-agent thread-op request to enqueue a dispatch recommendation task");
});
test("POST /api/v1/projects/[projectId]/messages marks approval_required groups as pending user approval", async () => {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const groupProject = await createIndependentGroupChat({
memberProjectIds: [memberProjects[0].id, memberProjects[1].id],
createdBy: "17600003315",
});
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === groupProject.id
? {
...project,
collaborationMode: "approval_required" as const,
approvalState: "not_required" as const,
}
: project,
),
});
const response = await POST(await createAuthedRequest(groupProject.id, { body: "请协调两个线程确认上线方案" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
dispatchPlan: { planId: string } | null;
collaborationGate: {
isGroup: boolean;
collaborationMode: "development" | "approval_required";
requiresMasterAgentApproval: boolean;
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
};
};
assert.equal(payload.ok, true);
assert.ok(payload.dispatchPlan, "expected dispatch plan");
assert.equal(payload.collaborationGate.isGroup, true);
assert.equal(payload.collaborationGate.collaborationMode, "approval_required");
assert.equal(payload.collaborationGate.requiresMasterAgentApproval, true);
assert.equal(payload.collaborationGate.approvalState, "pending_user");
const nextState = await readState();
const persistedGroup = nextState.projects.find((project) => project.id === groupProject.id);
assert.ok(persistedGroup, "expected group project to persist");
assert.equal(persistedGroup?.approvalState, "pending_user");
const pendingNotice = persistedGroup?.messages.find(
(message) =>
message.sender === "master" &&
message.kind === "system_notice" &&
message.body.includes("等待你确认"),
);
assert.ok(pendingNotice, "expected an approval notice to be persisted in the group ledger");
assert.match(pendingNotice?.body ?? "", /等待你确认|待审批|待确认/);
});
test("POST /api/v1/projects/[projectId]/messages blocks new approval_required requests while a plan is still pending", async () => {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const groupProject = await createIndependentGroupChat({
memberProjectIds: [memberProjects[0].id, memberProjects[1].id],
createdBy: "17600003315",
});
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === groupProject.id
? {
...project,
collaborationMode: "approval_required" as const,
approvalState: "not_required" as const,
}
: project,
),
});
const firstResponse = await POST(await createAuthedRequest(groupProject.id, { body: "请协调两个线程确认上线方案" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(firstResponse.status, 200);
const firstPayload = (await firstResponse.json()) as {
dispatchPlan: { planId: string } | null;
};
assert.ok(firstPayload.dispatchPlan, "expected first message to create a dispatch plan");
const blockedResponse = await POST(await createAuthedRequest(groupProject.id, { body: "再补充一个新的下发要求" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(blockedResponse.status, 409);
const blockedPayload = (await blockedResponse.json()) as {
ok: boolean;
message: string;
pendingPlan: { planId: string } | null;
collaborationGate: {
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
requiresMasterAgentApproval: boolean;
};
};
assert.equal(blockedPayload.ok, false);
assert.match(blockedPayload.message, /先确认|拒绝|待确认/);
assert.equal(blockedPayload.pendingPlan?.planId, firstPayload.dispatchPlan?.planId);
assert.equal(blockedPayload.collaborationGate.approvalState, "pending_user");
assert.equal(blockedPayload.collaborationGate.requiresMasterAgentApproval, true);
const nextState = await readState();
const groupState = nextState.projects.find((project) => project.id === groupProject.id);
assert.ok(groupState, "expected group project to exist");
assert.equal(groupState?.approvalState, "pending_user");
assert.equal(
nextState.dispatchPlans.filter((plan) => plan.groupProjectId === groupProject.id && plan.status === "pending_user_confirmation").length,
1,
"expected only the original pending dispatch plan to remain",
);
assert.equal(
groupState?.messages.some((message) => message.body === "再补充一个新的下发要求"),
false,
"expected blocked request not to append a new user message",
);
});
test("POST /api/v1/projects/[projectId]/messages keeps message success when group dispatch recommendation fails", async () => {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const groupProject = await createIndependentGroupChat({
memberProjectIds: [memberProjects[0].id, memberProjects[1].id],
createdBy: "17600003315",
});
const state = await readState();
const brokenMember = state.projects
.find((project) => project.id === groupProject.id)
?.groupMembers[0];
assert.ok(brokenMember, "expected group chat to have a member we can corrupt");
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === groupProject.id
? {
...project,
groupMembers: [
{
...brokenMember,
projectId: "missing-project-for-dispatch",
},
],
}
: project,
),
});
const response = await POST(await createAuthedRequest(groupProject.id, { body: "请重新梳理下待确认项" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
message: { id: string; body: string };
dispatchPlan: null;
dispatchRecommendation: {
ok: boolean;
taskId?: string;
status: string;
error?: string;
};
};
assert.equal(payload.ok, true);
assert.equal(payload.message.body, "请重新梳理下待确认项");
assert.equal(payload.dispatchPlan, null);
assert.equal(payload.dispatchRecommendation.ok, false);
assert.equal(payload.dispatchRecommendation.status, "failed");
assert.match(payload.dispatchRecommendation.error ?? "", /DISPATCH_TARGET_PROJECT_NOT_FOUND/);
const nextState = await readState();
const savedMessage = nextState.projects
.find((project) => project.id === groupProject.id)
?.messages.find((message) => message.id === payload.message.id);
assert.ok(savedMessage, "expected user message to remain persisted even when dispatch recommendation fails");
const persistedMessages =
nextState.projects.find((project) => project.id === groupProject.id)?.messages ?? [];
const savedMessageIndex = persistedMessages.findIndex((message) => message.id === payload.message.id);
assert.notEqual(savedMessageIndex, -1, "expected the user message to remain in the project timeline");
assert.ok(
persistedMessages
.slice(savedMessageIndex + 1)
.some((message) => message.sender === "master"),
"expected a user-visible failure notice to be appended for dispatch errors",
);
const failedTask = nextState.masterAgentTasks.find(
(task) =>
task.projectId === groupProject.id &&
task.requestMessageId === payload.message.id &&
task.taskType === "group_dispatch_plan",
);
assert.ok(failedTask, "expected failed dispatch recommendation task to be recorded");
assert.equal(failedTask?.status, "failed");
});
test("POST /api/v1/projects/[projectId]/messages excludes master-agent from group dispatch targets", async () => {
await setup();
const memberProjects = await ensureTwoSingleThreadProjects();
assert.ok(memberProjects.length >= 2, "expected seeded single-thread projects");
const groupProject = await createIndependentGroupChat({
memberProjectIds: [memberProjects[0].id, memberProjects[1].id],
createdBy: "17600003315",
});
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === groupProject.id
? {
...project,
groupMembers: [
...project.groupMembers,
{
projectId: "master-agent",
deviceId: "mac-studio",
threadId: "thread-master-agent",
threadDisplayName: "主 Agent",
folderName: "主控线程",
},
],
}
: project,
),
});
const response = await POST(await createAuthedRequest(groupProject.id, { body: "请继续同步联调进展" }), {
params: Promise.resolve({ projectId: groupProject.id }),
});
assert.equal(response.status, 200);
const payload = (await response.json()) as {
dispatchPlan: null | {
targets: Array<{ projectId: string }>;
};
};
assert.ok(payload.dispatchPlan, "expected dispatch plan");
assert.deepEqual(
payload.dispatchPlan?.targets.map((target) => target.projectId),
groupProject.groupMembers.map((member) => member.projectId),
);
assert.equal(
payload.dispatchPlan?.targets.some((target) => target.projectId === "master-agent"),
false,
"master-agent should never appear as a dispatch target",
);
});
test("createIndependentGroupChat rejects non-thread members like master-agent", async () => {
await setup();
const [realThread] = await ensureTwoSingleThreadProjects();
assert.ok(realThread, "expected a real thread-backed project");
await assert.rejects(
() =>
createIndependentGroupChat({
memberProjectIds: ["master-agent", realThread.id],
createdBy: "17600003315",
}),
/GROUP_CHAT_MEMBER_NOT_THREAD/,
);
});