feat: keep project understanding in sync after import

This commit is contained in:
kris
2026-04-04 08:09:47 +08:00
parent 908ad8858b
commit 01f438e3af
3 changed files with 640 additions and 7 deletions

View File

@@ -321,6 +321,23 @@ test("device import draft review queues a master-agent task, then completion wri
assert.ok(importedProject, "expected selected candidate to become a real chat window");
assert.equal(importedProject?.threadMeta.threadDisplayName, "北区试产线回归");
assert.equal(importedProject?.threadMeta.folderName, "北区试产线");
assert.equal(importedProject?.projectUnderstanding?.projectGoal, "完成北区试产线树莓派二代接入与联调。");
assert.match(importedProject?.projectUnderstanding?.technicalArchitecture ?? "", /local-agent 与 Codex 线程联动/);
assert.equal(importedProject?.projectUnderstanding?.sourceKind, "device_import");
assert.ok(importedProject?.threadMeta.lastProjectUnderstandingSyncedAt);
const importedMemories = nextState.masterAgentMemories.filter(
(memory) => memory.projectId === importedProject?.id,
);
assert.equal(importedMemories.length, 5);
assert.equal(
importedMemories.find((memory) => memory.title === "项目目标 · 北区试产线回归")?.content,
"完成北区试产线树莓派二代接入与联调。",
);
assert.match(
importedMemories.find((memory) => memory.title === "下一步建议 · 北区试产线回归")?.content ?? "",
/确认接线和串口日志/,
);
const device = nextState.devices.find((item) => item.id === enrollmentPayload.device.id);
assert.deepEqual(device?.projects, ["北区试产线"]);
@@ -336,6 +353,264 @@ test("device import draft review queues a master-agent task, then completion wri
assert.equal(appliedResolution?.status, "applied");
});
test("imported thread projects queue hidden understanding sync tasks on newer activity and refresh project understanding", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "Mac mini",
avatar: "M",
account: "17600003315",
endpoint: "mac://mini.local",
note: "project sync follow-up",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { pairingCode: string };
device: { id: string };
};
const heartbeatBody = {
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "Mac mini",
avatar: "M",
account: "17600003315",
status: "online" as const,
quota5h: 73,
quota7d: 84,
projects: [],
endpoint: "mac://mini.local",
projectCandidates: [
{
folderName: "智能看板",
folderRef: "smart-board",
threadId: "thread-smart-board",
threadDisplayName: "智能看板主线程",
codexFolderRef: "smart-board",
codexThreadRef: "thread-smart-board",
lastActiveAt: "2026-03-30T11:00:00+08:00",
suggestedImport: true,
},
],
};
const firstHeartbeatResponse = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(heartbeatBody),
}),
);
assert.equal(firstHeartbeatResponse.status, 200);
const draftResponse = await getImportDraftRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft`,
"GET",
),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
);
const draftPayload = (await draftResponse.json()) as {
draft: { candidates: Array<{ candidateId: string }> };
};
const selectedCandidateIds = draftPayload.draft.candidates.map((candidate) => candidate.candidateId);
assert.equal(
(
await selectImportDraftRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft/select`,
"POST",
{ selectedCandidateIds },
),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
)
).status,
200,
);
const reviewResponse = await reviewImportDraftRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft/review`,
"POST",
{},
),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
);
assert.equal(reviewResponse.status, 200);
const reviewPayload = (await reviewResponse.json()) as {
task: { taskId: string; deviceId: string };
};
const stateAfterReview = await readState();
const initialUnderstandingTask = stateAfterReview.masterAgentTasks.find(
(task) =>
task.taskType === "conversation_reply" &&
task.deviceImportDraftId &&
task.deviceImportCandidateId &&
task.status === "queued",
);
assert.ok(initialUnderstandingTask);
assert.equal(
(
await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${reviewPayload.task.taskId}/complete`,
"POST",
{
deviceId: reviewPayload.task.deviceId,
status: "completed",
replyBody: JSON.stringify(
{
summary: "Mac mini 导入建议:将智能看板主线程导入为独立会话。",
items: selectedCandidateIds.map((candidateId) => ({
candidateId,
action: "create_thread_conversation",
reason: "需要保留独立上下文,建议新建会话。",
})),
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: reviewPayload.task.taskId }) },
)
).status,
200,
);
assert.equal(
(
await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${initialUnderstandingTask.taskId}/complete`,
"POST",
{
deviceId: enrollmentPayload.device.id,
status: "completed",
replyBody: JSON.stringify(
{
projectGoal: "让智能看板项目能够稳定接入主控面板。",
currentProgress: "已经完成导入前梳理,准备开始界面和设备联调。",
technicalArchitecture: "Android 原生端连接 Boss Web再通过 local-agent 对接 Codex 线程。",
currentBlockers: "还缺少设备端实时推送状态的统一协议。",
recommendedNextStep: "先对齐状态推送协议,再做前后端联调。",
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: initialUnderstandingTask.taskId }) },
)
).status,
200,
);
const applyResponse = await applyImportDraftRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft/apply`,
"POST",
{},
),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
);
assert.equal(applyResponse.status, 200);
let currentState = await readState();
const importedProject = currentState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-smart-board",
);
assert.ok(importedProject);
assert.equal(importedProject?.projectUnderstanding?.currentProgress, "已经完成导入前梳理,准备开始界面和设备联调。");
const secondHeartbeatResponse = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
...heartbeatBody,
lastSeenAt: "2026-04-05T11:40:00+08:00",
projectCandidates: [
{
...heartbeatBody.projectCandidates[0],
lastActiveAt: "2026-04-05T11:35:00+08:00",
},
],
}),
}),
);
assert.equal(secondHeartbeatResponse.status, 200);
currentState = await readState();
const hiddenSyncTask = currentState.masterAgentTasks.find(
(task) =>
task.taskType === "conversation_reply" &&
task.projectId === "master-agent" &&
task.projectUnderstandingTargetProjectId === importedProject?.id &&
task.projectUnderstandingReason === "heartbeat_activity" &&
task.status === "queued",
);
assert.ok(hiddenSyncTask, "expected a hidden follow-up sync task for newer thread activity");
assert.equal(
(
await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${hiddenSyncTask.taskId}/complete`,
"POST",
{
deviceId: enrollmentPayload.device.id,
status: "completed",
replyBody: JSON.stringify(
{
projectGoal: "让智能看板项目能够稳定接入主控面板。",
currentProgress: "用户已经继续推进到实时状态同步和 UI 联调阶段。",
technicalArchitecture: "Android 原生端通过 SSE 接收 Boss 更新local-agent 负责把线程状态回流到控制台。",
currentBlockers: "高刷设备上的 UI 更新仍需继续优化。",
recommendedNextStep: "优先压平实时状态刷新抖动,再验证群聊调度链路。",
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: hiddenSyncTask.taskId }) },
)
).status,
200,
);
currentState = await readState();
const refreshedProject = currentState.projects.find((project) => project.id === importedProject?.id);
assert.equal(refreshedProject?.projectUnderstanding?.currentProgress, "用户已经继续推进到实时状态同步和 UI 联调阶段。");
assert.match(refreshedProject?.projectUnderstanding?.technicalArchitecture ?? "", /SSE 接收 Boss 更新/);
assert.equal(refreshedProject?.projectUnderstanding?.sourceKind, "thread_sync");
assert.ok(refreshedProject?.threadMeta.lastProjectUnderstandingRequestedAt);
assert.ok(refreshedProject?.threadMeta.lastProjectUnderstandingSyncedAt);
assert.equal(
currentState.masterAgentMemories.find(
(memory) =>
memory.projectId === refreshedProject?.id &&
memory.title === "项目进度 · 智能看板主线程",
)?.content,
"用户已经继续推进到实时状态同步和 UI 联调阶段。",
);
assert.equal(
currentState.masterAgentMemories.find(
(memory) =>
memory.projectId === refreshedProject?.id &&
memory.title === "下一步建议 · 智能看板主线程",
)?.content,
"优先压平实时状态刷新抖动,再验证群聊调度链路。",
);
});
test("heartbeat candidates no longer auto-create chat windows from legacy projects when import draft is present", async () => {
await setup();