Files
boss/tests/single-thread-message-execution.test.ts

737 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdtemp, rm } from "node:fs/promises";
import { NextRequest } from "next/server";
let runtimeRoot = "";
let 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 = "";
const TEST_ACCOUNT = "17600003315";
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: TEST_ACCOUNT,
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return new NextRequest(url, {
method,
headers: {
"content-type": "application/json",
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
},
body: JSON.stringify(body),
});
}
function findSingleThreadProject(
state: Awaited<ReturnType<typeof readState>>,
) {
return state.projects.find((project) => project.id !== "master-agent" && !project.isGroup);
}
function buildSingleThreadProject(projectId: string) {
return {
id: projectId,
name: "测试线程",
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: "测试线程等待继续处理。",
updatedAt: "2026-04-04T11:30:00+08:00",
lastMessageAt: "2026-04-04T11:30:00+08:00",
isGroup: false,
threadMeta: {
projectId,
threadId: `${projectId}-thread`,
threadDisplayName: "测试线程",
folderName: "测试项目",
activityIconCount: 0,
updatedAt: "2026-04-04T11:30:00+08:00",
codexThreadRef: `${projectId}-thread`,
codexFolderRef: `/Users/kris/code/${projectId}`,
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development" as const,
approvalState: "not_required" as const,
unreadCount: 0,
riskLevel: "low" as const,
messages: [],
goals: [],
versions: [],
};
}
function buildProjectFolderKey(project: ReturnType<typeof buildSingleThreadProject>) {
const folderRef = (project.threadMeta.codexFolderRef?.trim() || project.threadMeta.folderName.trim()).toLowerCase();
return `${project.deviceIds[0]}:${folderRef}`;
}
async function ensureSingleThreadProject() {
const state = await readState();
const existing = findSingleThreadProject(state);
if (existing) {
return existing;
}
const project = buildSingleThreadProject("single-thread-test");
await writeState({
...state,
projects: state.projects.concat(project),
});
const nextState = await readState();
return findSingleThreadProject(nextState);
}
async function setProjectTakeover(projectId: string, enabled: boolean) {
const state = await readState();
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
state.userProjectAgentControls.unshift({
account: TEST_ACCOUNT,
projectId,
controls: {
takeoverEnabled: enabled,
updatedAt: enabled ? "2026-04-17T10:00:00.000Z" : "2026-04-17T10:10:00.000Z",
},
});
await writeState(state);
}
async function resetThreadExecutionState(projectId: string) {
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
const targetDevice = project ? state.devices.find((device) => device.id === project.deviceIds[0]) : null;
if (targetDevice) {
targetDevice.preferredExecutionMode = "cli";
}
state.projectExecutionPolicies = state.projectExecutionPolicies.filter((policy) => policy.projectId !== projectId);
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
);
await writeState(state);
}
test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task for single-thread projects", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
const response = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "请同步一下当前阻塞情况" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
assert.equal(response.status, 200);
const payload = (await response.json()) as {
ok: boolean;
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.equal(task?.targetCodexThreadRef, singleProject.threadMeta.codexThreadRef);
assert.equal(task?.targetCodexFolderRef, singleProject.threadMeta.codexFolderRef);
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");
assert.equal(task?.relayViaMasterAgent, undefined);
});
test("POST /api/v1/projects/[projectId]/messages routes takeover mode to master-agent conversation first", 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, undefined);
assert.equal(task?.targetThreadId, undefined);
assert.equal(task?.targetCodexThreadRef, undefined);
assert.equal(task?.targetCodexFolderRef, undefined);
assert.ok(task?.executionPrompt?.includes("主 Agent"));
assert.ok(task?.executionPrompt?.includes("协同接管"));
assert.ok(task?.executionPrompt?.includes("先准确理解并确认用户意图"));
});
test("takeover prompt asks master agent to sync verified goals and version records when user requests review", 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;
task?: { taskId: string; taskType: string; status: string } | null;
};
assert.equal(payload.ok, true);
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 a queued takeover task");
assert.equal(task?.relayViaMasterAgent, true);
assert.match(task!.executionPrompt, /用户要求核对或更新项目目标、版本记录时/);
assert.match(task!.executionPrompt, /先让当前线程基于本地开发文档和实际代码重新汇总/);
assert.match(task!.executionPrompt, /自动同步到当前会话顶部的“项目目标”和“版本记录”入口/);
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, /先基于当前项目本地可见的开发文档和实际代码进行汇总/);
});
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 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, undefined);
assert.equal(task?.targetThreadId, undefined);
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 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");
});