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