Files
boss/tests/single-thread-message-execution.test.ts
2026-06-08 12:22:50 +08:00

2177 lines
88 KiB
TypeScript
Raw Permalink 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 completeMasterTaskRoute: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let queueMasterAgentTask: (typeof import("../src/lib/boss-data"))["queueMasterAgentTask"];
let upsertDeviceHeartbeat: (typeof import("../src/lib/boss-data"))["upsertDeviceHeartbeat"];
let AUTH_SESSION_COOKIE = "";
const TEST_ACCOUNT = "krisolo";
type MasterAgentTask = Awaited<ReturnType<typeof readState>>["masterAgentTasks"][number];
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-single-thread-message-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [messageModule, completeModule, data, auth] = await Promise.all([
import("../src/app/api/v1/projects/[projectId]/messages/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;
completeMasterTaskRoute = completeModule.POST;
createAuthSession = data.createAuthSession;
readState = data.readState;
writeState = data.writeState;
queueMasterAgentTask = data.queueMasterAgentTask;
upsertDeviceHeartbeat = data.upsertDeviceHeartbeat;
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: TEST_ACCOUNT,
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),
});
}
function findSingleThreadProject(
state: Awaited<ReturnType<typeof readState>>,
) {
return state.projects.find((project) => project.id !== "master-agent" && !project.isGroup);
}
function buildSingleThreadProject(projectId: string) {
return {
id: projectId,
name: "测试线程",
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: "测试线程等待继续处理。",
updatedAt: "2026-04-04T11:30:00+08:00",
lastMessageAt: "2026-04-04T11:30:00+08:00",
isGroup: false,
threadMeta: {
projectId,
threadId: `${projectId}-thread`,
threadDisplayName: "测试线程",
folderName: "测试项目",
activityIconCount: 0,
updatedAt: "2026-04-04T11:30:00+08:00",
codexThreadRef: `${projectId}-thread`,
codexFolderRef: `/Users/kris/code/${projectId}`,
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development" as const,
approvalState: "not_required" as const,
unreadCount: 0,
riskLevel: "low" as const,
messages: [],
goals: [],
versions: [],
};
}
function buildProjectFolderKey(project: ReturnType<typeof buildSingleThreadProject>) {
const folderRef = (project.threadMeta.codexFolderRef?.trim() || project.threadMeta.folderName.trim()).toLowerCase();
return `${project.deviceIds[0]}:${folderRef}`;
}
async function ensureSingleThreadProject() {
const state = await readState();
const existing = findSingleThreadProject(state);
if (existing) {
return existing;
}
const project = buildSingleThreadProject("single-thread-test");
await writeState({
...state,
projects: state.projects.concat(project),
});
const nextState = await readState();
return findSingleThreadProject(nextState);
}
async function setProjectTakeover(projectId: string, enabled: boolean) {
const state = await readState();
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
state.userProjectAgentControls.unshift({
account: TEST_ACCOUNT,
projectId,
controls: {
takeoverEnabled: enabled,
updatedAt: enabled ? "2026-04-17T10:00:00.000Z" : "2026-04-17T10:10:00.000Z",
},
});
await writeState(state);
}
async function resetThreadExecutionState(projectId: string) {
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
const targetDevice = project ? state.devices.find((device) => device.id === project.deviceIds[0]) : null;
if (targetDevice) {
targetDevice.preferredExecutionMode = "cli";
}
state.projectExecutionPolicies = state.projectExecutionPolicies.filter((policy) => policy.projectId !== projectId);
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
await writeState(state);
}
test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task for single-thread projects", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请同步一下当前阻塞情况" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
message: { id: string; body: string; sentAt: string };
task?: { taskId: string; taskType: string; status: string } | null;
dispatchPlan: null;
};
assert.equal(payload.ok, true);
assert.equal(payload.message.body, "请同步一下当前阻塞情况");
assert.equal(payload.dispatchPlan, null);
assert.ok(payload.task, "expected single-thread message to return a queued task");
assert.equal(payload.task?.taskType, "conversation_reply");
assert.equal(payload.task?.status, "queued");
const nextState = await readState();
const task = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "请同步一下当前阻塞情况",
);
assert.ok(task, "expected a queued conversation_reply task for the single-thread project");
assert.equal(task?.targetProjectId, singleProject.id);
assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId);
assert.equal(task?.targetCodexThreadRef, singleProject.threadMeta.codexThreadRef);
assert.equal(task?.targetCodexFolderRef, singleProject.threadMeta.codexFolderRef);
assert.equal(task?.sourceMessageId, payload.message.id);
assert.equal(task?.sourceMessageBody, "请同步一下当前阻塞情况");
assert.equal(task?.sourceMessageSentAt, payload.message.sentAt);
assert.equal(task?.mirrorBossUserMessageToCodexDesktop, true);
assert.ok(!task?.executionPrompt?.includes("threadProjectId:"), "thread prompt should not include project id labels");
assert.ok(!task?.executionPrompt?.includes("folderName:"), "thread prompt should not include folder labels");
assert.ok(!task?.executionPrompt?.includes("deviceIds:"), "thread prompt should not include device id labels");
assert.equal(task?.relayViaMasterAgent, undefined);
assert.equal(task?.executionPrompt, "请同步一下当前阻塞情况");
const progressMessage = nextState.projects
.find((project) => project.id === singleProject.id)
?.messages.find((message) => message.executionProgress?.taskId === task.taskId);
assert.ok(progressMessage, "expected a structured execution progress card for the queued thread task");
assert.equal(progressMessage?.kind, "execution_progress");
assert.equal(progressMessage?.executionProgress?.status, "queued");
assert.equal(progressMessage?.executionProgress?.steps[0]?.text, "接收对话任务");
assert.equal(progressMessage?.executionProgress?.steps.at(-1)?.text, "回写 Boss 对话窗口");
});
test("POST /api/v1/projects/[projectId]/messages routes takeover mode through the target Codex thread", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请继续同步当前线程进展" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "请继续同步当前线程进展",
);
assert.ok(task, "expected a queued conversation_reply task for takeover mode");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, singleProject.id);
assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId);
assert.equal(task?.targetCodexThreadRef, singleProject.threadMeta.codexThreadRef);
assert.equal(task?.targetCodexFolderRef, singleProject.threadMeta.codexFolderRef);
assert.equal(task?.mirrorBossUserMessageToCodexDesktop, true);
assert.equal(task?.sourceMessageId, task?.requestMessageId);
assert.equal(task?.sourceMessageBody, "请继续同步当前线程进展");
assert.ok(task?.executionPrompt?.includes("Boss APP 发来一条托管消息"));
assert.ok(task?.executionPrompt?.includes("请继续同步当前线程进展"));
assert.ok(!task?.executionPrompt?.includes("目标线程名称:"));
assert.ok(!task?.executionPrompt?.includes("不要自称主 Agent"));
const progressMessage = nextState.projects
.find((project) => project.id === singleProject.id)
?.messages.find((message) => message.executionProgress?.taskId === task.taskId);
assert.ok(progressMessage, "expected takeover mode to show the same thread execution progress card");
assert.equal(progressMessage?.sender, "master");
assert.equal(progressMessage?.kind, "execution_progress");
assert.equal(progressMessage?.executionProgress?.status, "queued");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete updates the existing execution progress card", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const task = await queueMasterAgentTask({
projectId: singleProject.id,
taskType: "conversation_reply",
requestMessageId: "msg-progress-complete",
requestText: "继续开发并回写进度",
executionPrompt: "继续开发并回写进度",
requestedBy: "Boss 超级管理员",
requestedByAccount: TEST_ACCOUNT,
deviceId: "mac-studio",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
targetThreadDisplayName: singleProject.threadMeta.threadDisplayName,
targetCodexThreadRef: singleProject.threadMeta.codexThreadRef,
targetCodexFolderRef: singleProject.threadMeta.codexFolderRef,
mirrorBossUserMessageToCodexDesktop: true,
});
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: "已完成本轮开发,并更新了验证记录。",
executionProgress: {
branch: {
additions: 42,
deletions: 3,
gitStatus: "有未提交变更",
githubCliStatus: "unavailable",
},
artifacts: [
{ label: "development_version_log_20260508.md", kind: "file" },
{ label: "已生成图像 1", kind: "image" },
],
agents: [
{ name: "Mendel", role: "explorer" },
],
},
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const progressMessages = nextState.projects
.find((project) => project.id === singleProject.id)
?.messages.filter((message) => message.executionProgress?.taskId === task.taskId);
assert.equal(progressMessages?.length, 1, "progress updates should mutate the existing card");
const progress = progressMessages?.[0]?.executionProgress;
assert.equal(progress?.status, "completed");
assert.equal(progress?.branch?.additions, 42);
assert.equal(progress?.branch?.deletions, 3);
assert.equal(progress?.artifacts?.[0]?.label, "development_version_log_20260508.md");
assert.equal(progress?.agents?.[0]?.name, "Mendel");
assert.ok(progress?.steps.every((step) => step.status === "done"));
});
test("POST /api/v1/projects/[projectId]/messages routes master-agent mentions in the message body only to master agent", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "先别发给子线程,麻烦 @主Agent 帮我判断这个线程下一步怎么推进" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected the mention to queue a master-agent reply task in this conversation");
assert.equal(task?.projectId, singleProject.id);
assert.equal(task?.requestText, "先别发给子线程,麻烦 帮我判断这个线程下一步怎么推进");
assert.equal(task?.relayViaMasterAgent, undefined);
assert.equal(task?.targetProjectId, undefined);
assert.equal(task?.targetThreadId, undefined);
const childThreadTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "先别发给子线程,麻烦 @主Agent 帮我判断这个线程下一步怎么推进" &&
item.targetProjectId === singleProject.id,
);
assert.equal(childThreadTask, undefined, "at-mention messages must not be sent to the child thread");
});
test("POST /api/v1/projects/[projectId]/messages lets @主Agent create browser control tasks inside a thread conversation", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "@主Agent 打开 Chrome 去后台看一下订单页" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
executionMode?: string;
riskLevel?: string;
requiresConfirmation?: boolean;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "browser_control");
assert.equal(payload.executionMode, "browser");
assert.equal(payload.riskLevel, "medium");
assert.equal(payload.requiresConfirmation, false);
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.equal(task?.projectId, singleProject.id);
assert.equal(task?.taskType, "browser_control");
assert.equal(task?.intentCategory, "browser_control");
assert.equal(task?.runtimeKind, "browser-automation-runtime");
assert.equal(task?.requiresUserConfirmation, undefined);
});
test("POST /api/v1/projects/[projectId]/messages routes direct GUI browser commands to browser control", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
targetDevice.capabilities = {
...(targetDevice.capabilities ?? {}),
browserAutomation: {
...(targetDevice.capabilities?.browserAutomation ?? {}),
connected: true,
},
};
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "打开浏览器,用浏览器打开 YouTube找一个 MV 播放" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
executionMode?: string;
riskLevel?: string;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "browser_control");
assert.equal(payload.task?.status, "queued");
assert.equal(payload.executionMode, "browser");
assert.equal(payload.riskLevel, "medium");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected a queued browser_control task");
assert.equal(task?.projectId, singleProject.id);
assert.equal(task?.deviceId, singleProject.deviceIds[0]);
assert.equal(task?.taskType, "browser_control");
assert.equal(task?.intentCategory, "browser_control");
assert.equal(task?.runtimeKind, "browser-automation-runtime");
const staleConversationTask = nextState.masterAgentTasks.find(
(item) =>
item.projectId === singleProject.id &&
item.taskType === "conversation_reply" &&
item.requestText === "打开浏览器,用浏览器打开 YouTube找一个 MV 播放",
);
assert.equal(staleConversationTask, undefined, "direct GUI control should not queue an unclaimable thread task");
});
test("POST /api/v1/projects/[projectId]/messages creates native remote progress for direct GUI control", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
targetDevice.capabilities = {
...(targetDevice.capabilities ?? {}),
browserAutomation: {
...(targetDevice.capabilities?.browserAutomation ?? {}),
connected: true,
},
};
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "打开浏览器,用浏览器打开 YouTube找一个 MV 播放" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as { task?: { taskId: string } | null };
const nextState = await readState();
const progressMessage = nextState.projects
.find((project) => project.id === singleProject.id)
?.messages.find((message) => message.executionProgress?.taskId === payload.task?.taskId);
const progress = progressMessage?.executionProgress as
| (NonNullable<typeof progressMessage>["executionProgress"] & {
controlMode?: string;
runtimeKind?: string;
controlPlatform?: string;
computerUseProvider?: string;
})
| undefined;
assert.ok(progressMessage, "expected native control task to render its own progress card");
assert.equal(progress?.title, "远程控制进度");
assert.equal(progress?.controlMode, "native_remote_control");
assert.equal(progress?.runtimeKind, "browser-automation-runtime");
assert.equal(progress?.controlPlatform, "macos");
assert.equal(progress?.computerUseProvider, "openai-computer-use");
assert.ok(progress?.steps.some((step) => step.text.includes("连接目标电脑")));
assert.ok(!progress?.steps.some((step) => /Codex|Git|线程记录/.test(step.text)));
assert.equal(progress?.branch, undefined);
assert.equal(progress?.agents, undefined);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete appends a visible master summary after control tasks", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const task = await queueMasterAgentTask({
projectId: singleProject.id,
taskType: "browser_control",
requestMessageId: "msg-browser-summary",
requestText: "打开浏览器,用浏览器打开 YouTube找一个周杰伦 MV 播放",
executionPrompt: "打开浏览器并搜索周杰伦 MV",
requestedBy: "Boss 超级管理员",
requestedByAccount: TEST_ACCOUNT,
deviceId: "mac-studio",
accountLabel: "主 GPT",
intentCategory: "browser_control",
runtimeKind: "browser-automation-runtime",
riskLevel: "medium",
});
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
replyBody: "浏览器控制已完成:打开浏览器,用浏览器打开 YouTube找一个周杰伦 MV 播放",
targetUrl: "https://www.youtube.com/results?search_query=%E5%91%A8%E6%9D%B0%E4%BC%A6%20MV",
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const controlSummary = updatedProject?.messages.find(
(message) => message.kind === "control_summary" && message.executionProgress?.taskId !== task.taskId,
);
const visibleSummary = updatedProject?.messages.at(-1);
assert.equal(controlSummary?.body, "浏览器控制已完成:打开浏览器,用浏览器打开 YouTube找一个周杰伦 MV 播放");
assert.equal(visibleSummary?.sender, "master");
assert.equal(visibleSummary?.senderLabel, "主 Agent · 主 GPT");
assert.equal(visibleSummary?.kind, "text");
assert.match(visibleSummary?.body ?? "", /任务小结:浏览器控制已完成/);
assert.match(visibleSummary?.body ?? "", /周杰伦 MV/);
assert.ok(visibleSummary?.body.includes("youtube.com/results"));
assert.ok(!visibleSummary?.body.includes("\n"));
assert.equal(updatedProject?.preview, visibleSummary?.body);
assert.equal(updatedProject?.unreadCount, 1);
});
test("POST /api/v1/projects/[projectId]/messages lets @主Agent trigger project summary sync that writes back to top entries", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
let state = await readState();
const seededProject = state.projects.find((project) => project.id === singleProject.id);
assert.ok(seededProject, "expected seeded single-thread project in state");
seededProject!.projectUnderstanding = undefined;
seededProject!.versions = [];
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "@主Agent 请同步一下当前线程的项目目标和版本记录,同步完成记得告诉我" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body?: string };
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.match(payload.replyMessage?.body ?? "", /项目目标.*版本记录/);
assert.match(payload.replyMessage?.body ?? "", /完成后.*告诉你|回你|回执/);
state = await readState();
const syncTask = state.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(syncTask, "expected a hidden project understanding sync task for @主Agent summary sync");
assert.equal(syncTask?.projectId, "master-agent");
assert.equal(syncTask?.projectUnderstandingTargetProjectId, singleProject.id);
assert.equal(syncTask?.projectUnderstandingReplyProjectId, singleProject.id);
assert.equal(syncTask?.projectUnderstandingNotifyOnCompletion, true);
const completeResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${syncTask!.taskId}/complete`,
"POST",
{
deviceId: syncTask!.deviceId,
status: "completed",
replyBody: JSON.stringify({
projectGoal: "把 @主Agent 触发的项目摘要同步也写回顶部入口",
currentProgress: "已经补上线程内 @主Agent 触发摘要同步的写回链路",
technicalArchitecture: "消息路由创建隐藏同步任务,状态账本持久化项目理解与版本记录",
currentBlockers: "",
recommendedNextStep: "继续做 Android 顶部入口真机回归",
versionRecord: "新增线程内 @主Agent 同步项目摘要后自动回写顶部入口。",
}),
},
),
{ params: Promise.resolve({ taskId: syncTask!.taskId }) },
);
assert.equal(completeResponse.status, 200);
state = await readState();
const updatedProject = state.projects.find((project) => project.id === singleProject.id);
assert.equal(updatedProject?.projectUnderstanding?.projectGoal, "把 @主Agent 触发的项目摘要同步也写回顶部入口");
assert.equal(updatedProject?.versions[0]?.summary, "新增线程内 @主Agent 同步项目摘要后自动回写顶部入口。");
});
test("POST /api/v1/projects/[projectId]/messages lets takeover mode create browser control tasks with execution metadata", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "打开 Chrome 去后台看一下订单页" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
executionMode?: string;
riskLevel?: string;
requiresConfirmation?: boolean;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "browser_control");
assert.equal(payload.executionMode, "browser");
assert.equal(payload.riskLevel, "medium");
assert.equal(payload.requiresConfirmation, false);
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.equal(task?.projectId, singleProject.id);
assert.equal(task?.taskType, "browser_control");
assert.equal(task?.intentCategory, "browser_control");
assert.equal(task?.runtimeKind, "browser-automation-runtime");
});
test("POST /api/v1/projects/[projectId]/messages accepts punctuated master-agent mention variants", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "@ 主a'gent 帮我看下这个线程为什么不回消息" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected the punctuated mention to queue a master-agent reply task");
assert.equal(task?.requestText, "帮我看下这个线程为什么不回消息");
});
test("POST /api/v1/projects/[projectId]/messages lets a master mention enable current conversation takeover", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "@主Agent 帮我开启当前线程托管" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task, null);
assert.match(payload.replyMessage?.body ?? "", /已.*开启.*主 Agent 协同接管|已.*开启.*托管/);
const nextState = await readState();
const scopedControls = nextState.userProjectAgentControls.find(
(item) => item.projectId === singleProject.id && item.account === TEST_ACCOUNT,
);
assert.equal(scopedControls?.controls.takeoverEnabled, true);
const childThreadTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "@主Agent 帮我开启当前线程托管" &&
item.targetProjectId === singleProject.id,
);
assert.equal(childThreadTask, undefined, "takeover command should not be sent to the child thread");
});
test("POST /api/v1/projects/[projectId]/messages lets a master mention disable current conversation takeover", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "@主Agent 退出当前的接管模式" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task, null);
assert.match(payload.replyMessage?.body ?? "", /已.*关闭.*主 Agent 协同接管|已.*退出.*接管/);
const nextState = await readState();
const scopedControls = nextState.userProjectAgentControls.find(
(item) => item.projectId === singleProject.id && item.account === TEST_ACCOUNT,
);
assert.equal(scopedControls?.controls.takeoverEnabled, false);
});
test("takeover mode can exit current conversation takeover directly from chat", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "退出当前的接管模式" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task, null);
assert.match(payload.replyMessage?.body ?? "", /已.*关闭.*主 Agent 协同接管|已.*退出.*接管/);
const nextState = await readState();
const scopedControls = nextState.userProjectAgentControls.find(
(item) => item.projectId === singleProject.id && item.account === TEST_ACCOUNT,
);
assert.equal(scopedControls?.controls.takeoverEnabled, false);
const genericTakeoverTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "退出当前的接管模式" &&
item.relayViaMasterAgent === true,
);
assert.equal(genericTakeoverTask, undefined, "exit takeover command should not enqueue a generic takeover reply");
});
test("master-agent conversation can enable takeover for a specified thread", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const response = await postMessageRoute(
await createAuthedRequest(
"http://127.0.0.1:3000/api/v1/projects/master-agent/messages",
"POST",
{ body: `帮我开启${singleProject.threadMeta.threadDisplayName}的接管模式` },
),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task, null);
assert.match(payload.replyMessage?.body ?? "", /已.*开启.*主 Agent 协同接管|已.*开启.*接管/);
assert.match(payload.replyMessage?.body ?? "", new RegExp(singleProject.threadMeta.threadDisplayName));
const nextState = await readState();
const scopedControls = nextState.userProjectAgentControls.find(
(item) => item.projectId === singleProject.id && item.account === TEST_ACCOUNT,
);
assert.equal(scopedControls?.controls.takeoverEnabled, true);
});
test("master-agent conversation can disable takeover for a specified thread", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
"http://127.0.0.1:3000/api/v1/projects/master-agent/messages",
"POST",
{ body: `帮我关闭${singleProject.threadMeta.threadDisplayName}的接管模式` },
),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task, null);
assert.match(payload.replyMessage?.body ?? "", /已.*关闭.*主 Agent 协同接管|已.*退出.*接管/);
assert.match(payload.replyMessage?.body ?? "", new RegExp(singleProject.threadMeta.threadDisplayName));
const nextState = await readState();
const scopedControls = nextState.userProjectAgentControls.find(
(item) => item.projectId === singleProject.id && item.account === TEST_ACCOUNT,
);
assert.equal(scopedControls?.controls.takeoverEnabled, false);
});
test("takeover summary sync only queues verified project understanding and returns a concise ack", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "核对一下项目目标和版本记录,确认后同步到顶部入口" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyMessage?: { body: string };
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
dispatchPlan: null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.dispatchPlan, null);
assert.equal(payload.task?.taskType, "conversation_reply");
assert.ok(payload.replyMessage?.body.includes(singleProject.threadMeta.threadDisplayName));
assert.ok(payload.replyMessage?.body.includes("项目目标"));
assert.ok(payload.replyMessage?.body.includes("版本记录"));
assert.ok(!/OTA|在线设备|APP 日志|运行时|heartbeat|心跳/.test(payload.replyMessage?.body ?? ""));
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected queued project understanding sync task");
assert.equal(task?.projectId, "master-agent");
assert.equal(task?.projectUnderstandingTargetProjectId, singleProject.id);
assert.equal(task?.relayViaMasterAgent, undefined);
assert.match(task!.executionPrompt, /只输出 JSON/);
assert.match(task!.executionPrompt, /不要把全局 OTA 可用状态/);
const understandingTask = nextState.masterAgentTasks.find(
(item) =>
item.projectId === "master-agent" &&
item.projectUnderstandingTargetProjectId === singleProject.id &&
item.status === "queued",
);
assert.ok(understandingTask, "expected a hidden project understanding sync task");
assert.match(understandingTask!.executionPrompt, /先基于当前项目本地可见的开发文档和实际代码进行汇总/);
const genericTakeoverTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "核对一下项目目标和版本记录,确认后同步到顶部入口" &&
item.relayViaMasterAgent === true,
);
assert.equal(genericTakeoverTask, undefined, "summary sync should not also queue a generic master reply");
});
test("takeover summary wording also queues verified project understanding instead of a generic master reply", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const requestText = "帮我总结一下当前项目目标和版本记录";
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: requestText },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyMessage?: { body: string };
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
dispatchPlan: null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.dispatchPlan, null);
assert.ok(payload.replyMessage?.body.includes("项目目标"));
assert.ok(payload.replyMessage?.body.includes("版本记录"));
assert.ok(!/OTA|在线设备|APP 日志|运行时|heartbeat|心跳/.test(payload.replyMessage?.body ?? ""));
const nextState = await readState();
const understandingTask = nextState.masterAgentTasks.find(
(item) =>
item.projectId === "master-agent" &&
item.projectUnderstandingTargetProjectId === singleProject.id &&
item.status === "queued",
);
assert.ok(understandingTask, "expected summary wording to queue a hidden project understanding sync task");
assert.equal(understandingTask!.requestText, `请同步项目《${singleProject.name}》当前目标与进度`);
const genericTakeoverTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === requestText &&
item.relayViaMasterAgent === true,
);
assert.equal(genericTakeoverTask, undefined, "summary wording should not queue a generic master reply");
});
test("takeover summary sync remembers completion notice preference and replies after sync completes", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const requestText = "核对一下项目目标和版本记录,同步完成记得和我说,以后也是这样";
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: requestText },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyMessage?: { body: string };
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.match(payload.replyMessage?.body ?? "", /完成后.*回你|回执|告诉你/);
let state = await readState();
const rememberedRule = state.masterAgentMemories.find(
(memory) =>
memory.account === TEST_ACCOUNT &&
memory.scope === "global" &&
memory.memoryType === "workflow_rule" &&
/同步.*项目目标.*版本记录.*完成后.*提醒|项目摘要同步完成/.test(memory.title + memory.content),
);
assert.ok(rememberedRule, "expected the master agent to persist the completion notice preference");
const syncTask = state.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(syncTask, "expected a hidden project understanding sync task");
assert.equal(syncTask.projectUnderstandingNotifyOnCompletion, true);
assert.equal(syncTask.projectUnderstandingReplyProjectId, singleProject.id);
const completeResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${syncTask!.taskId}/complete`,
"POST",
{
deviceId: syncTask!.deviceId,
status: "completed",
replyBody: JSON.stringify({
projectGoal: "稳定 AI 眼镜线程的项目目标和版本记录同步",
currentProgress: "主 Agent 已基于本地文档重新汇总当前状态",
technicalArchitecture: "Boss 通过文件账本保存项目理解并由 Android 顶部入口读取",
currentBlockers: "",
recommendedNextStep: "继续用真机验证同步回执",
versionRecord: "新增项目目标和版本记录同步完成后的主动回执。",
}),
},
),
{ params: Promise.resolve({ taskId: syncTask!.taskId }) },
);
assert.equal(completeResponse.status, 200);
state = await readState();
const updatedProject = state.projects.find((project) => project.id === singleProject.id);
assert.equal(updatedProject?.projectUnderstanding?.projectGoal, "稳定 AI 眼镜线程的项目目标和版本记录同步");
assert.ok(
updatedProject?.messages.some((message) =>
/项目目标和版本记录已同步完成|同步完成/.test(message.body) &&
/顶部入口/.test(message.body),
),
"expected a completion notice in the conversation that requested the sync",
);
const followupResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "再同步一下项目目标和版本记录" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(followupResponse.status, 200);
const followupPayload = (await followupResponse.json()) as {
replyMessage?: { body: string };
task?: { taskId: string } | null;
};
assert.match(followupPayload.replyMessage?.body ?? "", /完成后.*回你|回执|告诉你/);
state = await readState();
const followupTask: MasterAgentTask | undefined = state.masterAgentTasks.find(
(item) => item.taskId === followupPayload.task?.taskId,
);
assert.equal(followupTask?.projectUnderstandingNotifyOnCompletion, true);
assert.equal(followupTask?.projectUnderstandingReplyProjectId, singleProject.id);
});
test("POST /api/v1/projects/[projectId]/messages still lets takeover mode talk to master agent during gui conflict", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "我先和你确认一下接下来怎么推进" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
replyPresenter?: "thread" | "master";
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.replyPresenter, "master");
assert.equal(payload.task?.taskType, "conversation_reply");
const nextState = await readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
assert.ok(task, "expected takeover mode to queue a master-agent task");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, undefined);
});
test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when the target device prefers gui mode", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "gui";
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "继续推进当前线程" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 409);
const payload = (await response.json()) as {
ok: boolean;
code?: string;
message?: string;
executionConflict?: {
projectId: string;
deviceId: string;
preferredExecutionMode: "gui" | "cli";
allowPolicy: "forbid" | "allow_once" | "allow_always";
conflictState: "none" | "warning" | "blocked";
reason: string;
actions: string[];
};
};
assert.equal(payload.ok, false);
assert.equal(payload.code, "THREAD_EXECUTION_CONFLICT");
assert.equal(payload.executionConflict?.projectId, singleProject.id);
assert.equal(payload.executionConflict?.deviceId, singleProject.deviceIds[0]);
assert.equal(payload.executionConflict?.preferredExecutionMode, "gui");
assert.equal(payload.executionConflict?.allowPolicy, "forbid");
assert.equal(payload.executionConflict?.conflictState, "blocked");
assert.equal(payload.executionConflict?.reason, "preferred_gui_mode");
assert.deepEqual(payload.executionConflict?.actions, ["forbid", "allow_once", "allow_always"]);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const blockedMessage = updatedProject?.messages.find((message) => message.body.includes("继续推进当前线程"));
assert.equal(blockedMessage, undefined, "blocked send should not append a local chat message");
const queuedTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "继续推进当前线程",
);
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
});
test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when the current project folder is forbidden", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await setProjectTakeover(singleProject.id, false);
const state = await readState();
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
assert.ok(targetDevice, "expected a seeded target device");
targetDevice.preferredExecutionMode = "cli";
state.projectExecutionPolicies = [
{
deviceId: singleProject.deviceIds[0],
folderKey: buildProjectFolderKey(singleProject),
projectId: singleProject.id,
allowPolicy: "forbid",
conflictState: "blocked",
updatedAt: "2026-04-06T13:20:00.000Z",
},
];
await writeState(state);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "继续同步项目进度" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 409);
const payload = (await response.json()) as {
ok: boolean;
code?: string;
executionConflict?: {
projectId: string;
folderKey?: string;
preferredExecutionMode: "gui" | "cli";
allowPolicy: "forbid" | "allow_once" | "allow_always";
conflictState: "none" | "warning" | "blocked";
reason: string;
};
};
assert.equal(payload.ok, false);
assert.equal(payload.code, "THREAD_EXECUTION_CONFLICT");
assert.equal(payload.executionConflict?.projectId, singleProject.id);
assert.equal(payload.executionConflict?.folderKey, buildProjectFolderKey(singleProject));
assert.equal(payload.executionConflict?.preferredExecutionMode, "cli");
assert.equal(payload.executionConflict?.allowPolicy, "forbid");
assert.equal(payload.executionConflict?.conflictState, "blocked");
assert.equal(payload.executionConflict?.reason, "project_conflict_forbid");
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const blockedMessage = updatedProject?.messages.find((message) => message.body.includes("继续同步项目进度"));
assert.equal(blockedMessage, undefined, "blocked send should not append a local chat message");
const queuedTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "继续同步项目进度",
);
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
});
test("POST /api/v1/projects/[projectId]/messages blocks before queueing when recent codex activity exists without a stored policy", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const recentExternalActivityAt = new Date(Date.now() - 60_000).toISOString();
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === singleProject.id
? {
...project,
threadMeta: {
...project.threadMeta,
lastObservedCodexActivityAt: recentExternalActivityAt,
},
}
: project,
),
projectExecutionPolicies: state.projectExecutionPolicies.filter(
(policy) => policy.projectId !== singleProject.id,
),
});
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请看一下这个项目现在卡在哪里" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 409);
const payload = (await response.json()) as {
ok: boolean;
code?: string;
executionConflict?: {
projectId: string;
preferredExecutionMode: "gui" | "cli";
allowPolicy: "forbid" | "allow_once" | "allow_always";
conflictState: "none" | "warning" | "blocked";
reason: string;
};
};
assert.equal(payload.ok, false);
assert.equal(payload.code, "THREAD_EXECUTION_CONFLICT");
assert.equal(payload.executionConflict?.projectId, singleProject.id);
assert.equal(payload.executionConflict?.preferredExecutionMode, "cli");
assert.equal(payload.executionConflict?.allowPolicy, "forbid");
assert.equal(payload.executionConflict?.conflictState, "blocked");
assert.equal(payload.executionConflict?.reason, "project_conflict_forbid");
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const blockedMessage = updatedProject?.messages.find((message) =>
message.body.includes("请看一下这个项目现在卡在哪里"),
);
assert.equal(blockedMessage, undefined, "blocked send should not append a local chat message");
const queuedTask = nextState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.requestText === "请看一下这个项目现在卡在哪里",
);
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
});
test("POST /api/v1/projects/[projectId]/messages ignores stale scoped conflict policies", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const staleExternalActivityAt = new Date(Date.now() - 30 * 60_000).toISOString();
const state = await readState();
await writeState({
...state,
projects: state.projects.map((project) =>
project.id === singleProject.id
? {
...project,
threadMeta: {
...project.threadMeta,
lastObservedCodexActivityAt: staleExternalActivityAt,
},
}
: project,
),
projectExecutionPolicies: [
...state.projectExecutionPolicies.filter((policy) => policy.projectId !== singleProject.id),
{
deviceId: singleProject.deviceIds[0],
folderKey: buildProjectFolderKey(singleProject),
projectId: singleProject.id,
allowPolicy: "forbid" as const,
conflictState: "blocked" as const,
recentExternalActivityAt: staleExternalActivityAt,
updatedAt: staleExternalActivityAt,
},
],
});
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "继续同步这个线程" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
assert.equal(payload.task?.taskType, "conversation_reply");
assert.equal(payload.task?.status, "queued");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread reply back to the single-thread project", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请同步一下当前阻塞情况" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as {
task?: { taskId: string };
};
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: "当前阻塞点已经同步:视觉验收待今晚回归。",
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const mirroredReply = updatedProject?.messages.find((message) =>
message.body.includes("当前阻塞点已经同步:视觉验收待今晚回归。"),
);
assert.ok(mirroredReply, "expected single-thread reply to be written back to the project");
assert.equal(mirroredReply?.sender, "device");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete folds thread commentary into thread_process and only counts final result unread", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请继续推进当前线程" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.targetProjectId === singleProject.id,
);
assert.ok(task, "expected a queued conversation_reply task");
const processText = "我先检查聊天折叠链路,确认过程消息不会直接展开。";
const finalText = "这轮已经完成折叠修复,未读现在只会算最终结果。";
const processResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: processText,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(processResponse.status, 200);
let nextState = await readState();
let updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
let processMessage = updatedProject?.messages.find((message) => message.body === processText);
assert.ok(processMessage, "expected process message to be written back");
assert.equal(processMessage?.kind, "thread_process");
assert.equal(updatedProject?.preview, "请继续推进当前线程");
assert.equal(updatedProject?.unreadCount, 0);
const finalTask = await queueMasterAgentTask({
projectId: singleProject.id,
taskType: "conversation_reply",
requestMessageId: "msg-final-round",
requestText: "继续推进当前线程",
executionPrompt: "请继续回复用户",
requestedBy: "Boss 超级管理员",
requestedByAccount: TEST_ACCOUNT,
deviceId: "mac-studio",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
targetThreadDisplayName: singleProject.threadMeta.threadDisplayName,
targetCodexThreadRef: singleProject.threadMeta.codexThreadRef,
targetCodexFolderRef: singleProject.threadMeta.codexFolderRef,
});
const finalResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${finalTask.taskId}/complete`,
"POST",
{
deviceId: finalTask.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: finalText,
},
),
{ params: Promise.resolve({ taskId: finalTask.taskId }) },
);
assert.equal(finalResponse.status, 200);
nextState = await readState();
updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
processMessage = updatedProject?.messages.find((message) => message.body === processText);
const finalMessage = updatedProject?.messages.find((message) => message.body === finalText);
assert.equal(processMessage?.kind, "thread_process");
assert.equal(finalMessage?.kind, "text");
assert.equal(updatedProject?.preview, finalText);
assert.equal(updatedProject?.unreadCount, 1);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete strips already mirrored process text from aggregate thread replies", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "只要不对原有项目有任何影响。你按最推荐的方式做。" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as { task?: { taskId: string } };
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
const processOne = "我先按非侵入方式收口:不碰原项目代码,只把这次 APP 端问题整理成可复现、可提交的诊断文档。";
const processTwo = "我在按调试流程收证据,但会收在文档里,不会动任何现有项目文件。";
const processThree = "文档已经落下来了,我再做一次范围确认,确保只有独立报告被新增。";
const finalText = "我按非侵入方式处理了,没有碰任何原有项目代码,只新增了一份独立排查文档。";
const aggregateReply = `${processOne}${processTwo}${processThree}${finalText}`;
const requestedAtMs = Date.parse(task.requestedAt);
assert.ok(Number.isFinite(requestedAtMs), "expected task requestedAt to be parseable");
const taskRelativeTime = (offsetMs: number) => new Date(requestedAtMs + offsetMs).toISOString();
const mirroredState = await readState();
const project = mirroredState.projects.find((item) => item.id === singleProject.id);
assert.ok(project, "expected the single-thread project to exist");
project.messages = project.messages.filter(
(message) =>
message.id === task.requestMessageId ||
message.executionProgress?.taskId === task.taskId,
);
project.messages.push(
{
id: "msg-process-one",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processOne,
sentAt: taskRelativeTime(1_000),
kind: "thread_process",
},
{
id: "msg-process-two",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processTwo,
sentAt: taskRelativeTime(2_000),
kind: "thread_process",
},
{
id: "msg-process-three",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processThree,
sentAt: taskRelativeTime(3_000),
kind: "thread_process",
},
{
id: "msg-final-mirrored",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: finalText,
sentAt: taskRelativeTime(4_000),
kind: "text",
},
);
project.preview = finalText;
project.lastMessageAt = taskRelativeTime(4_000);
project.unreadCount = 1;
await writeState(mirroredState);
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: aggregateReply,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((item) => item.id === singleProject.id);
const aggregateMessages =
updatedProject?.messages.filter((message) => message.body === aggregateReply) ?? [];
const finalMessages = updatedProject?.messages.filter((message) => message.body === finalText) ?? [];
assert.equal(aggregateMessages.length, 0, "aggregate process+final reply should not be displayed");
assert.equal(finalMessages.length, 1, "already mirrored final result should not be duplicated");
assert.equal(updatedProject?.preview, finalText);
assert.equal(updatedProject?.unreadCount, 1);
const cleanupState = await readState();
const cleanupProject = cleanupState.projects.find((item) => item.id === singleProject.id);
if (cleanupProject) {
cleanupProject.messages = [];
cleanupProject.preview = "测试线程等待继续处理。";
cleanupProject.lastMessageAt = "2026-04-04T11:30:00+08:00";
cleanupProject.unreadCount = 0;
}
await writeState(cleanupState);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete keeps compact numbered progress updates folded", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "继续处理" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.targetProjectId === singleProject.id,
);
assert.ok(task, "expected a queued conversation_reply task");
const processText = [
"1. 先检查当前消息折叠链路。",
"2. 再确认 Android 端只把最终结果记成未读。",
"3. 处理完成后我会回你最终结果。"
].join("\n");
const processResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: processText,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(processResponse.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const processMessage = updatedProject?.messages.find((message) => message.body === processText);
assert.ok(processMessage, "expected compact process message to be written back");
assert.equal(processMessage?.kind, "thread_process");
assert.equal(updatedProject?.preview, "继续处理");
assert.equal(updatedProject?.unreadCount, 0);
});
test("device heartbeat activity does not overwrite conversation preview with desktop process text", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
const state = await readState();
const project = state.projects.find((item) => item.id === singleProject.id);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
project.messages = [
{
id: "msg-final-existing",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: "这是上一轮最终结果。",
sentAt: "2026-04-24T05:30:00.000Z",
kind: "text",
},
];
project.preview = "这是上一轮最终结果。";
project.lastMessageAt = "2026-04-24T05:30:00.000Z";
project.unreadCount = 0;
await writeState(state);
await upsertDeviceHeartbeat({
deviceId: "mac-studio",
token: (await readState()).devices.find((device) => device.id === "mac-studio")?.token,
name: "Mac Studio",
avatar: "M",
account: TEST_ACCOUNT,
status: "online",
quota5h: 90,
quota7d: 92,
projects: [],
endpoint: "mac://studio",
projectCandidates: [
{
folderName: singleProject.threadMeta.folderName,
folderRef: singleProject.threadMeta.codexFolderRef,
threadId: singleProject.threadMeta.threadId,
threadDisplayName: singleProject.threadMeta.threadDisplayName,
codexFolderRef: singleProject.threadMeta.codexFolderRef,
codexThreadRef: singleProject.threadMeta.codexThreadRef,
lastActiveAt: "2026-04-24T05:41:14.246Z",
suggestedImport: true,
recentAssistantMessages: [
{
messageId: "codex-thread:preview-keep:2026-04-24T05:41:14.246Z:p1",
body: "1. 先检查当前消息折叠链路。\n2. 再确认 Android 端只把最终结果记成未读。\n3. 处理完成后我会回你最终结果。",
sentAt: "2026-04-24T05:41:14.246Z",
phase: "commentary",
},
],
},
],
});
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const processMessage = updatedProject?.messages.find(
(message) => message.externalMessageId === "codex-thread:preview-keep:2026-04-24T05:41:14.246Z:p1",
);
assert.equal(processMessage, undefined);
assert.equal(updatedProject?.messages.length, 1);
assert.equal(updatedProject?.preview, "这是上一轮最终结果。");
assert.equal(updatedProject?.unreadCount, 0);
assert.equal(updatedProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-24T05:41:14.246Z");
});
test("device heartbeat activity clears stale process preview without appending desktop process text", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
const state = await readState();
const project = state.projects.find((item) => item.id === singleProject.id);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
project.messages = [];
project.preview = "历史遗留的过程预览";
project.lastMessageAt = "2026-04-24T05:30:00.000Z";
project.unreadCount = 0;
await writeState(state);
await upsertDeviceHeartbeat({
deviceId: "mac-studio",
token: (await readState()).devices.find((device) => device.id === "mac-studio")?.token,
name: "Mac Studio",
avatar: "M",
account: TEST_ACCOUNT,
status: "online",
quota5h: 90,
quota7d: 92,
projects: [],
endpoint: "mac://studio",
projectCandidates: [
{
folderName: singleProject.threadMeta.folderName,
folderRef: singleProject.threadMeta.codexFolderRef,
threadId: singleProject.threadMeta.threadId,
threadDisplayName: singleProject.threadMeta.threadDisplayName,
codexFolderRef: singleProject.threadMeta.codexFolderRef,
codexThreadRef: singleProject.threadMeta.codexThreadRef,
lastActiveAt: "2026-04-24T05:41:14.246Z",
suggestedImport: true,
recentAssistantMessages: [
{
messageId: "codex-thread:preview-empty:2026-04-24T05:41:14.246Z:p1",
body: "1. 先检查当前消息折叠链路。\n2. 再确认 Android 端只把最终结果记成未读。\n3. 处理完成后我会回你最终结果。",
sentAt: "2026-04-24T05:41:14.246Z",
phase: "commentary",
},
],
},
],
});
const nextState = await readState();
const updatedProject = nextState.projects.find((item) => item.id === singleProject.id);
const processMessage = updatedProject?.messages.find(
(message) => message.externalMessageId === "codex-thread:preview-empty:2026-04-24T05:41:14.246Z:p1",
);
assert.equal(processMessage, undefined);
assert.equal(updatedProject?.messages.length, 0);
assert.equal(updatedProject?.preview, "");
assert.equal(updatedProject?.unreadCount, 0);
assert.equal(updatedProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-24T05:41:14.246Z");
});
test("legacy device process text is reclassified and no longer pollutes preview or unread", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
const state = await readState();
const project = state.projects.find((item) => item.id === singleProject.id);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
project.messages = [
{
id: "legacy-process",
sender: "device",
senderLabel: singleProject.threadMeta.threadDisplayName,
body: "我继续往下收,这一轮先检查折叠链路,再确认未读逻辑,随后回你结果。",
sentAt: "2026-04-24T05:45:00.000Z",
kind: "text",
},
{
id: "legacy-final",
sender: "device",
senderLabel: singleProject.threadMeta.threadDisplayName,
body: "这轮已经处理完成,最终结果已回写。",
sentAt: "2026-04-24T05:46:00.000Z",
kind: "text",
},
];
project.preview = "我继续往下收,这一轮先检查折叠链路,再确认未读逻辑,随后回你结果。";
project.lastMessageAt = "2026-04-24T05:46:00.000Z";
project.unreadCount = 2;
await writeState(state);
const nextState = await readState();
const updatedProject = nextState.projects.find((item) => item.id === singleProject.id);
const processMessage = updatedProject?.messages.find((message) => message.id === "legacy-process");
const finalMessage = updatedProject?.messages.find((message) => message.id === "legacy-final");
assert.equal(processMessage?.kind, "thread_process");
assert.equal(finalMessage?.kind, "text");
assert.equal(updatedProject?.preview, "这轮已经处理完成,最终结果已回写。");
assert.equal(updatedProject?.unreadCount, 1);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes takeover master replies to the current project", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "托管后请帮我问一下当前阻塞" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as {
task?: { taskId: string };
};
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
assert.equal(task?.relayViaMasterAgent, true);
assert.equal(task?.targetProjectId, singleProject.id);
assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId);
await setProjectTakeover(singleProject.id, false);
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
replyBody: "我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?",
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const relayedReply = updatedProject?.messages.find((message) =>
message.body.includes("我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?"),
);
assert.ok(relayedReply, "expected a master reply to be written back to the current project");
assert.equal(relayedReply?.sender, "master");
assert.match(relayedReply?.senderLabel ?? "", /主 Agent/);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete converts a mirrored takeover thread reply instead of duplicating it", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, true);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "托管回归测试" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as {
task?: { taskId: string };
};
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
assert.equal(task?.relayViaMasterAgent, true);
const replyBody = "主Agent托管链路已收到。";
const mirroredState = await readState();
const project = mirroredState.projects.find((item) => item.id === singleProject.id);
assert.ok(project);
project!.messages.push({
id: "msg-mirrored-before-complete",
sender: "device",
senderLabel: singleProject.threadMeta.threadDisplayName,
body: replyBody,
sentAt: new Date().toISOString(),
kind: "text",
externalMessageId: "codex-thread:takeover-dedupe:reply",
});
project!.preview = replyBody;
project!.unreadCount = 1;
await writeState(mirroredState);
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
replyBody,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((item) => item.id === singleProject.id);
const replies = updatedProject?.messages.filter(
(message) => message.sender !== "user" && message.body === replyBody,
);
assert.equal(replies?.length, 1);
assert.equal(replies?.[0]?.sender, "master");
assert.match(replies?.[0]?.senderLabel ?? "", /主 Agent/);
assert.equal(replies?.[0]?.externalMessageId, "codex-thread:takeover-dedupe:reply");
assert.equal(updatedProject?.preview, replyBody);
assert.equal(updatedProject?.unreadCount, 1);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete blocks leaked thread environment diagnostics from the chat transcript", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请继续推进当前线程" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) =>
item.taskType === "conversation_reply" &&
item.projectId === singleProject.id &&
item.targetProjectId === singleProject.id,
);
assert.ok(task, "expected a queued conversation_reply task");
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody:
"我不能直接把当前会话环境从只读改回可写也不能替你修改这层运行配置。cwd 我可以在命令里指向 /Users/kris/code/gptpluscontrol。",
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
const leakedReply = updatedProject?.messages.find((message) =>
message.body.includes("当前会话环境从只读改回可写"),
);
assert.equal(leakedReply, undefined);
const opsNotice = updatedProject?.messages.find((message) =>
message.body.includes("线程环境异常,请重新绑定到正确项目或工作目录后再试。"),
);
assert.ok(opsNotice, "expected a user-facing system notice instead of raw environment diagnostics");
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete hides leaked Codex CLI envelopes from master chat", async () => {
await setup();
const task = await queueMasterAgentTask({
projectId: "master-agent",
requestMessageId: "msg-master-cli-leak",
requestText: "同步完成记得要和我说,以后也是这样。",
executionPrompt: "请按主 Agent 规则回复用户。",
requestedBy: "Boss 超级管理员",
requestedByAccount: TEST_ACCOUNT,
deviceId: "mac-studio",
});
const leakedEnvelope = [
"OpenAI Codex v0.114.0 (research preview)",
"--------",
"workdir: /Users/kris/code/boss",
"model: gpt-5.4",
"provider: openai",
"approval: never",
"sandbox: workspace-write [workdir, /tmp, $TMPDIR, /Users/kris/.codex/memories]",
"session id: 019da4e5-9b1d-7dc1-8aa5-a74a74b6b021",
"--------",
"user",
"同步完成记得要和我说,以后也是这样。",
"mcp: chrome-devtools starting",
].join("\n");
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "failed",
errorMessage: leakedEnvelope,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const masterProject = nextState.projects.find((project) => project.id === "master-agent");
const leakedMessage = masterProject?.messages.find((message) =>
/OpenAI Codex|workdir:|sandbox:|session id:|mcp: chrome-devtools/.test(message.body),
);
assert.equal(leakedMessage, undefined);
assert.ok(
masterProject?.messages.some((message) =>
message.body.includes("主 Agent 执行节点返回了内部启动日志,已拦截"),
),
"expected a concise user-facing failure notice",
);
});