import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import { mkdir, mkdtemp, rm, writeFile } 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 updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"]; let AUTH_SESSION_COOKIE = ""; 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; updateProjectAgentControls = data.updateProjectAgentControls; AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE; } test.after(async () => { if (runtimeRoot) { await rm(runtimeRoot, { recursive: true, force: true }); } }); test.beforeEach(async () => { await setup(); await rm(runtimeRoot, { recursive: true, force: true }); await mkdir(runtimeRoot, { recursive: true }); }); async function createAuthedRequest(url: string, method: "POST", body: unknown) { const session = await createAuthSession({ account: "17600003315", role: "highest_admin", displayName: "Boss 超级管理员", loginMethod: "password", }); return new NextRequest(url, { method, headers: { "content-type": "application/json", cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`, }, body: JSON.stringify(body), }); } async function waitFor(predicate: () => Promise, timeoutMs = 5_000) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { if (await predicate()) { return; } await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error("waitFor timed out"); } function findSingleThreadProject( state: Awaited>, projectId?: string, ) { return state.projects.find( (project) => project.id !== "master-agent" && !project.isGroup && (projectId ? project.id === projectId : true), ); } 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(projectId = "single-thread-test") { const state = await readState(); const existing = findSingleThreadProject(state, projectId); if (existing) { return existing; } const project = buildSingleThreadProject(projectId); await writeState({ ...state, projects: state.projects.concat(project), }); const nextState = await readState(); return findSingleThreadProject(nextState, projectId); } 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"); 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 }; task?: { taskId: string; taskType: string; status: string; requestMessageId: string } | null; dispatchPlan: null; }; assert.equal(payload.ok, true); 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"); assert.equal(payload.task?.requestMessageId, payload.message.id); 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.ok(task?.executionPrompt?.includes("请同步一下当前阻塞情况")); assert.ok(task?.executionPrompt?.includes(singleProject.threadMeta.threadDisplayName)); 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"); }); test("POST /api/v1/projects/[projectId]/messages preserves default local-agent path when ordinary thread has no backend override", async () => { await setup(); const singleProject = await ensureSingleThreadProject(); assert.ok(singleProject, "expected a seeded single-thread project"); 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 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 task"); assert.equal(task?.deviceId, singleProject.deviceIds[0]); assert.equal(task?.accountId, undefined); assert.equal(task?.accountLabel, undefined); }); test("POST /api/v1/projects/[projectId]/messages routes ordinary thread conversation_reply to hermes-runtime when backendOverride is set", async () => { await setup(); const singleProject = await ensureSingleThreadProject("single-thread-hermes-test"); assert.ok(singleProject, "expected a seeded single-thread project"); const hermesDir = await mkdtemp(path.join(os.tmpdir(), "boss-thread-hermes-route-")); const hermesScriptPath = path.join(hermesDir, "hermes-thread-route-runtime.mjs"); await writeFile( hermesScriptPath, ` process.stdout.write("Hermes 路由测试已执行\\n\\n"); process.stdout.write("session_id: hermes-thread-route-123\\n"); `, "utf8", ); const previousEnv = { BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED, BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND, BOSS_HERMES_ARGS: process.env.BOSS_HERMES_ARGS, BOSS_HERMES_TIMEOUT_MS: process.env.BOSS_HERMES_TIMEOUT_MS, }; process.env.BOSS_HERMES_ENABLED = "true"; process.env.BOSS_HERMES_COMMAND = process.execPath; process.env.BOSS_HERMES_ARGS = hermesScriptPath; process.env.BOSS_HERMES_TIMEOUT_MS = "1000"; try { await updateProjectAgentControls( singleProject.id, { backendOverride: "hermes-runtime", }, "17600003315", ); const response = await postMessageRoute( await createAuthedRequest( `http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`, "POST", { body: "请让 Hermes 接管当前线程回复" }, ), { params: Promise.resolve({ projectId: singleProject.id }) }, ); assert.equal(response.status, 200); const nextState = await readState(); const task = nextState.masterAgentTasks.find( (item) => item.taskType === "conversation_reply" && item.projectId === singleProject.id && item.requestText === "请让 Hermes 接管当前线程回复", ); assert.ok(task, "expected a queued conversation task"); assert.equal(task?.deviceId, "master-agent-hermes"); assert.equal(task?.accountId, "hermes-runtime"); assert.equal(task?.accountLabel, "Hermes Runtime"); assert.equal(task?.targetProjectId, singleProject.id); assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId); await waitFor(async () => { const state = await readState(); const currentTask = state.masterAgentTasks.find((item) => item.taskId === task?.taskId); return currentTask?.status === "completed"; }); } finally { process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED; process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND; process.env.BOSS_HERMES_ARGS = previousEnv.BOSS_HERMES_ARGS; process.env.BOSS_HERMES_TIMEOUT_MS = previousEnv.BOSS_HERMES_TIMEOUT_MS; await rm(hermesDir, { recursive: true, force: true }); } }); test("POST /api/v1/projects/[projectId]/messages falls back to the default local-agent path when a saved hermes override is no longer available", async () => { await setup(); const singleProject = await ensureSingleThreadProject("single-thread-hermes-fallback-test"); assert.ok(singleProject, "expected a seeded single-thread project"); const previousEnv = { BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED, BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND, BOSS_HERMES_ARGS: process.env.BOSS_HERMES_ARGS, BOSS_HERMES_TIMEOUT_MS: process.env.BOSS_HERMES_TIMEOUT_MS, }; try { await updateProjectAgentControls( singleProject.id, { backendOverride: "hermes-runtime", }, "17600003315", ); delete process.env.BOSS_HERMES_ENABLED; delete process.env.BOSS_HERMES_COMMAND; delete process.env.BOSS_HERMES_ARGS; delete process.env.BOSS_HERMES_TIMEOUT_MS; const response = await postMessageRoute( await createAuthedRequest( `http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`, "POST", { body: "Hermes 不可用时请回退到默认线程链路" }, ), { params: Promise.resolve({ projectId: singleProject.id }) }, ); assert.equal(response.status, 200); const nextState = await readState(); const task = nextState.masterAgentTasks.find( (item) => item.taskType === "conversation_reply" && item.projectId === singleProject.id && item.requestText === "Hermes 不可用时请回退到默认线程链路", ); assert.ok(task, "expected a queued conversation task"); assert.equal(task?.deviceId, singleProject.deviceIds[0]); assert.equal(task?.accountId, undefined); assert.equal(task?.accountLabel, undefined); } finally { process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED; process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND; process.env.BOSS_HERMES_ARGS = previousEnv.BOSS_HERMES_ARGS; process.env.BOSS_HERMES_TIMEOUT_MS = previousEnv.BOSS_HERMES_TIMEOUT_MS; } }); test("POST /api/v1/projects/[projectId]/messages lets Hermes asynchronously complete ordinary thread replies when backendOverride is set", async () => { await setup(); const singleProject = await ensureSingleThreadProject("single-thread-hermes-async-test"); assert.ok(singleProject, "expected a seeded single-thread project"); const hermesDir = await mkdtemp(path.join(os.tmpdir(), "boss-thread-hermes-queue-")); const hermesScriptPath = path.join(hermesDir, "hermes-thread-runtime.mjs"); await writeFile( hermesScriptPath, ` const args = process.argv.slice(2); const queryIndex = args.findIndex((item) => item === "-q" || item === "--query"); const query = queryIndex >= 0 ? args[queryIndex + 1] ?? "" : ""; process.stdout.write("Hermes 线程已接管:" + query + "\\n\\n"); process.stdout.write("session_id: hermes-thread-session-123\\n"); `, "utf8", ); const previousEnv = { BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED, BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND, BOSS_HERMES_ARGS: process.env.BOSS_HERMES_ARGS, BOSS_HERMES_TIMEOUT_MS: process.env.BOSS_HERMES_TIMEOUT_MS, }; process.env.BOSS_HERMES_ENABLED = "true"; process.env.BOSS_HERMES_COMMAND = process.execPath; process.env.BOSS_HERMES_ARGS = hermesScriptPath; process.env.BOSS_HERMES_TIMEOUT_MS = "1000"; try { await updateProjectAgentControls( singleProject.id, { backendOverride: "hermes-runtime", }, "17600003315", ); const response = await postMessageRoute( await createAuthedRequest( `http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`, "POST", { body: "请让 Hermes 真正回复当前线程" }, ), { params: Promise.resolve({ projectId: singleProject.id }) }, ); assert.equal(response.status, 200); const queuedState = await readState(); const task = queuedState.masterAgentTasks.find( (item) => item.taskType === "conversation_reply" && item.projectId === singleProject.id && item.requestText === "请让 Hermes 真正回复当前线程", ); assert.ok(task, "expected a queued Hermes conversation task"); await waitFor(async () => { const state = await readState(); const currentTask = state.masterAgentTasks.find((item) => item.taskId === task?.taskId); return currentTask?.status === "completed"; }); const nextState = await readState(); const completedTask = nextState.masterAgentTasks.find((item) => item.taskId === task?.taskId); assert.equal(completedTask?.status, "completed"); assert.match(completedTask?.replyBody ?? "", /Hermes 线程已接管:/); assert.equal(completedTask?.sessionId, "hermes-thread-session-123"); const updatedProject = nextState.projects.find((project) => project.id === singleProject.id); const mirroredReply = updatedProject?.messages.find((message) => message.body.includes("Hermes 线程已接管:"), ); assert.ok(mirroredReply, "expected Hermes reply to be written back to the thread project"); assert.equal(mirroredReply?.sender, "device"); } finally { process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED; process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND; process.env.BOSS_HERMES_ARGS = previousEnv.BOSS_HERMES_ARGS; process.env.BOSS_HERMES_TIMEOUT_MS = previousEnv.BOSS_HERMES_TIMEOUT_MS; await rm(hermesDir, { recursive: true, force: true }); } }); test("ordinary thread Hermes async execution blocks leaked environment diagnostics from the chat transcript", async () => { await setup(); const singleProject = await ensureSingleThreadProject("single-thread-hermes-env-test"); assert.ok(singleProject, "expected a seeded single-thread project"); const hermesDir = await mkdtemp(path.join(os.tmpdir(), "boss-thread-hermes-env-")); const hermesScriptPath = path.join(hermesDir, "hermes-thread-env-runtime.mjs"); await writeFile( hermesScriptPath, ` process.stdout.write("我不能直接把当前会话环境从只读改回可写,也不能替你修改这层运行配置。cwd 我可以在命令里指向 /Users/kris/code/gptpluscontrol。\\n\\n"); process.stdout.write("session_id: hermes-thread-env-123\\n"); `, "utf8", ); const previousEnv = { BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED, BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND, BOSS_HERMES_ARGS: process.env.BOSS_HERMES_ARGS, BOSS_HERMES_TIMEOUT_MS: process.env.BOSS_HERMES_TIMEOUT_MS, }; process.env.BOSS_HERMES_ENABLED = "true"; process.env.BOSS_HERMES_COMMAND = process.execPath; process.env.BOSS_HERMES_ARGS = hermesScriptPath; process.env.BOSS_HERMES_TIMEOUT_MS = "1000"; try { await updateProjectAgentControls( singleProject.id, { backendOverride: "hermes-runtime", }, "17600003315", ); 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 queuedState = await readState(); const task = queuedState.masterAgentTasks.find( (item) => item.taskType === "conversation_reply" && item.projectId === singleProject.id && item.requestText === "请继续推进当前线程", ); assert.ok(task, "expected a queued Hermes conversation task"); await waitFor(async () => { const state = await readState(); const currentTask = state.masterAgentTasks.find((item) => item.taskId === task?.taskId); return currentTask?.status === "failed"; }); const nextState = await readState(); const failedTask = nextState.masterAgentTasks.find((item) => item.taskId === task?.taskId); assert.equal(failedTask?.status, "failed"); assert.match(failedTask?.errorMessage ?? "", /THREAD_ENVIRONMENT_INVALID/); 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"); } finally { process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED; process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND; process.env.BOSS_HERMES_ARGS = previousEnv.BOSS_HERMES_ARGS; process.env.BOSS_HERMES_TIMEOUT_MS = previousEnv.BOSS_HERMES_TIMEOUT_MS; await rm(hermesDir, { recursive: true, force: true }); } }); 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"); 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"); 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/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 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: "当前阻塞点已经同步:视觉验收待今晚回归。", }, ), { 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 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 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 persists remote warnings onto execution warning records", async () => { await setup(); const singleProject = await ensureSingleThreadProject("single-thread-warning-test"); assert.ok(singleProject, "expected a seeded single-thread project"); 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 && item.requestText === "请同步当前线程的风险点", ); 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, requestId: "req-thread-warning-1", warnings: [ { title: "上下文接近上限", summary: "本轮回复过长,建议尽快压缩。", }, { title: " ", summary: " ", }, ], replyBody: "当前风险点已同步。", }, ), { params: Promise.resolve({ taskId: task.taskId }) }, ); assert.equal(response.status, 200); const nextState = await readState(); const warnings = nextState.threadExecutionWarnings.filter((warning) => warning.taskId === task.taskId); assert.deepEqual(warnings, [ { warningId: warnings[0]?.warningId, taskId: task.taskId, requestMessageId: task.requestMessageId, projectId: singleProject.id, targetProjectId: singleProject.id, targetThreadId: singleProject.threadMeta.threadId, sessionId: undefined, requestId: "req-thread-warning-1", title: "上下文接近上限", summary: "本轮回复过长,建议尽快压缩。", createdAt: warnings[0]?.createdAt, }, ]); });