feat: add thread status documents and safe thread reply handling

This commit is contained in:
kris
2026-04-04 11:50:46 +08:00
parent 010d8eda2d
commit 4d9b8e2976
10 changed files with 487 additions and 57 deletions

View File

@@ -10,6 +10,7 @@ let postMessageRoute: (typeof import("../src/app/api/v1/projects/[projectId]/mes
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() {
@@ -32,6 +33,7 @@ async function setup() {
completeMasterTaskRoute = completeModule.POST;
createAuthSession = data.createAuthSession;
readState = data.readState;
writeState = data.writeState;
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
@@ -65,10 +67,57 @@ function findSingleThreadProject(
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: [],
};
}
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 state = await readState();
const singleProject = findSingleThreadProject(state);
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
const response = await postMessageRoute(
@@ -112,8 +161,7 @@ test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task fo
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread reply back to the single-thread project", async () => {
await setup();
const state = await readState();
const singleProject = findSingleThreadProject(state);
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await postMessageRoute(
@@ -158,3 +206,56 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread re
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");
});