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 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; 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: "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), }); } 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); } 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; task?: { taskId: string; taskType: string; status: 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"); 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 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"); });