Files
boss/tests/device-import-draft.test.ts

1140 lines
42 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 createEnrollmentRoute: (typeof import("../src/app/api/v1/devices/enrollments/route"))["POST"];
let deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
let getImportDraftRoute: (typeof import("../src/app/api/v1/devices/[deviceId]/import-draft/route"))["GET"];
let selectImportDraftRoute: (typeof import("../src/app/api/v1/devices/[deviceId]/import-draft/select/route"))["POST"];
let reviewImportDraftRoute: (typeof import("../src/app/api/v1/devices/[deviceId]/import-draft/review/route"))["POST"];
let completeMasterTaskRoute: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route"))["POST"];
let applyImportDraftRoute: (typeof import("../src/app/api/v1/devices/[deviceId]/import-draft/apply/route"))["POST"];
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let AUTH_SESSION_COOKIE = "";
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-import-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [enrollmentModule, heartbeatModule, importDraftModule, selectModule, reviewModule, completeModule, applyModule, data, auth] =
await Promise.all([
import("../src/app/api/v1/devices/enrollments/route.ts"),
import("../src/app/api/device-heartbeat/route.ts"),
import("../src/app/api/v1/devices/[deviceId]/import-draft/route.ts"),
import("../src/app/api/v1/devices/[deviceId]/import-draft/select/route.ts"),
import("../src/app/api/v1/devices/[deviceId]/import-draft/review/route.ts"),
import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts"),
import("../src/app/api/v1/devices/[deviceId]/import-draft/apply/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
createEnrollmentRoute = enrollmentModule.POST;
deviceHeartbeatRoute = heartbeatModule.POST;
getImportDraftRoute = importDraftModule.GET;
selectImportDraftRoute = selectModule.POST;
reviewImportDraftRoute = reviewModule.POST;
completeMasterTaskRoute = completeModule.POST;
applyImportDraftRoute = applyModule.POST;
createAuthSession = data.createAuthSession;
readState = data.readState;
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: "GET" | "POST", body?: unknown) {
return createAuthedRequestFor("17600003315", "highest_admin", url, method, body);
}
async function createAuthedRequestFor(
account: string,
role: "member" | "admin" | "highest_admin",
url: string,
method: "GET" | "POST",
body?: unknown,
) {
const session = await createAuthSession({
account,
role,
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return new NextRequest(url, {
method,
headers: {
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
...(body ? { "content-type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
}
test("device import draft review queues a master-agent task, then completion writes back a ready resolution and apply still works", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "MacBook Pro",
avatar: "P",
account: "17600003315",
endpoint: "mac://mbp.local",
note: "待导入新设备",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { deviceId: string; pairingCode: string };
device: { id: string };
};
const heartbeatResponse = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "MacBook Pro",
avatar: "P",
account: "17600003315",
status: "online",
quota5h: 72,
quota7d: 88,
projects: [],
endpoint: "mac://mbp.local",
projectCandidates: [
{
folderName: "北区试产线",
folderRef: "north-line",
threadId: "thread-north-regression",
threadDisplayName: "北区试产线回归",
codexFolderRef: "north-line",
codexThreadRef: "thread-north-regression",
lastActiveAt: "2026-03-30T10:18:00+08:00",
suggestedImport: true,
},
{
folderName: "北区试产线",
folderRef: "north-line",
threadId: "thread-north-audit",
threadDisplayName: "北区试产线审计",
codexFolderRef: "north-line",
codexThreadRef: "thread-north-audit",
lastActiveAt: "2026-03-30T10:20:00+08:00",
suggestedImport: true,
},
],
}),
}),
);
assert.equal(heartbeatResponse.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 }) },
);
assert.equal(draftResponse.status, 200);
const draftPayload = (await draftResponse.json()) as {
draft: { draftId: string; candidates: Array<{ candidateId: string; threadDisplayName: string }> } | null;
};
assert.ok(draftPayload.draft, "expected an import draft after heartbeat candidates");
assert.equal(draftPayload.draft.candidates.length, 2);
const selectedCandidateIds = draftPayload.draft.candidates
.filter((candidate) => candidate.threadDisplayName === "北区试产线回归")
.map((candidate) => candidate.candidateId);
assert.equal(selectedCandidateIds.length, 1);
const selectResponse = 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 }) },
);
assert.equal(selectResponse.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 {
draft?: { status: string; selectedCandidateIds: string[] };
resolution?: { summary: string; items: Array<{ action: string; threadDisplayName: string }> };
understandingTasks?: Array<{ taskId: string; status: "queued" | "running" | "completed" | "failed" }>;
task: { taskId: string; status: "queued" | "running" | "completed" | "failed" };
};
assert.equal(reviewPayload.task.status, "queued");
assert.equal(reviewPayload.draft?.status, "pending_resolution");
assert.equal(reviewPayload.resolution, undefined);
assert.equal(reviewPayload.understandingTasks?.length, 1);
assert.equal(reviewPayload.understandingTasks?.[0]?.status, "queued");
const reviewedState = await readState();
const resolutionTask = reviewedState.masterAgentTasks.find(
(task) =>
task.taskType === "device_import_resolution" &&
task.deviceImportDraftId &&
task.status === "queued",
);
assert.ok(resolutionTask, "expected import review to leave a queued master-agent task trace");
const understandingTask = reviewedState.masterAgentTasks.find(
(task) =>
task.taskType === "conversation_reply" &&
task.deviceImportDraftId === draftPayload.draft?.draftId &&
task.status === "queued",
);
assert.ok(understandingTask, "expected import review to leave a queued thread understanding task trace");
assert.equal(understandingTask?.targetThreadDisplayName, "北区试产线回归");
const completionResponse = 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: "MacBook Pro 导入建议:将回归线程导入为独立会话。",
items: [
{
candidateId: draftPayload.draft?.candidates[0]?.candidateId,
action: "create_thread_conversation",
reason: "需要保留独立上下文,建议新建会话。",
},
],
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: reviewPayload.task.taskId }) },
);
assert.equal(completionResponse.status, 200);
const understandingCompletionResponse = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${understandingTask?.taskId}/complete`,
"POST",
{
deviceId: enrollmentPayload.device.id,
status: "completed",
replyBody: JSON.stringify(
{
projectGoal: "完成北区试产线树莓派二代接入与联调。",
currentProgress: "已经完成线程导入准备,当前在核对设备接线和控制链路。",
technicalArchitecture: "前台是 Next.js 控制台,设备端通过 local-agent 与 Codex 线程联动。",
currentBlockers: "还缺少树莓派二代的串口稳定性验证。",
recommendedNextStep: "先确认接线和串口日志,再继续设备控制指令联调。",
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: understandingTask?.taskId ?? "" }) },
);
assert.equal(understandingCompletionResponse.status, 200);
const completedState = await readState();
const completedDraft = completedState.deviceImportDrafts.find(
(draft) => draft.deviceId === enrollmentPayload.device.id,
);
const completedResolution = completedState.deviceImportResolutions.find(
(resolution) => resolution.deviceId === enrollmentPayload.device.id,
);
assert.equal(completedDraft?.status, "resolved");
assert.equal(completedResolution?.status, "ready");
assert.match(completedResolution?.summary ?? "", /MacBook Pro 导入建议/);
const refreshedDraftResponse = 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 }) },
);
assert.equal(refreshedDraftResponse.status, 200);
const refreshedDraftPayload = (await refreshedDraftResponse.json()) as {
projectUnderstandings?: Array<{
threadDisplayName: string;
projectGoal: string;
currentProgress: string;
technicalArchitecture: string;
currentBlockers: string;
recommendedNextStep: string;
}>;
understandingTasks?: Array<{ taskId: string; status: string }>;
};
assert.equal(refreshedDraftPayload.projectUnderstandings?.length, 1);
assert.equal(refreshedDraftPayload.projectUnderstandings?.[0]?.threadDisplayName, "北区试产线回归");
assert.match(
refreshedDraftPayload.projectUnderstandings?.[0]?.projectGoal ?? "",
/树莓派二代接入与联调/,
);
assert.equal(refreshedDraftPayload.understandingTasks?.[0]?.status, "completed");
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);
const applyPayload = (await applyResponse.json()) as {
importedProjects?: Array<{ id: string; name: string }>;
};
assert.equal(applyPayload.importedProjects?.length, 1);
assert.equal(applyPayload.importedProjects?.[0]?.name, "北区试产线回归");
const nextState = await readState();
const importedProject = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-north-regression",
);
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, ["北区试产线"]);
const appliedDraft = nextState.deviceImportDrafts.find(
(draft) => draft.deviceId === enrollmentPayload.device.id,
);
const appliedResolution = nextState.deviceImportResolutions.find(
(resolution) => resolution.deviceId === enrollmentPayload.device.id,
);
assert.equal(appliedDraft?.status, "applied");
assert.deepEqual(appliedDraft?.appliedProjectNames, ["北区试产线回归"]);
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,
"优先压平实时状态刷新抖动,再验证群聊调度链路。",
);
const masterAgentProject = currentState.projects.find((project) => project.id === "master-agent");
const syncNotice = masterAgentProject?.messages.findLast(
(message) =>
message.kind === "system_notice" &&
/已同步项目理解:智能看板主线程/.test(message.body) &&
/实时状态同步和 UI 联调阶段/.test(message.body),
);
assert.ok(syncNotice, "expected master-agent conversation to receive a lightweight sync digest");
const nextStepNotice = masterAgentProject?.messages.findLast(
(message) =>
message.kind === "system_notice" &&
/建议下一步推进:智能看板主线程/.test(message.body) &&
/优先压平实时状态刷新抖动,再验证群聊调度链路。/.test(message.body),
);
assert.ok(nextStepNotice, "expected master-agent conversation to receive a lightweight next-step suggestion");
});
test("heartbeat candidates no longer auto-create chat windows from legacy projects when import draft is present", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "ThinkPad",
avatar: "T",
account: "17600003315",
endpoint: "pc://thinkpad.local",
note: "legacy projects should not auto import",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { pairingCode: string };
device: { id: string };
};
const beforeState = await readState();
const beforeCount = beforeState.projects.filter((project) => project.name === "Legacy Folder").length;
const heartbeatResponse = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "ThinkPad",
avatar: "T",
account: "17600003315",
status: "online",
quota5h: 60,
quota7d: 75,
projects: ["Legacy Folder"],
endpoint: "pc://thinkpad.local",
projectCandidates: [
{
folderName: "Legacy Folder",
folderRef: "legacy-folder",
threadId: "thread-legacy-1",
threadDisplayName: "Legacy 线程",
codexFolderRef: "legacy-folder",
codexThreadRef: "thread-legacy-1",
lastActiveAt: "2026-03-30T10:30:00+08:00",
suggestedImport: true,
},
],
}),
}),
);
assert.equal(heartbeatResponse.status, 200);
const nextState = await readState();
const afterCount = nextState.projects.filter((project) => project.name === "Legacy Folder").length;
assert.equal(afterCount, beforeCount, "legacy project folders should wait for import apply");
const draft = nextState.deviceImportDrafts.find((item) => item.deviceId === enrollmentPayload.device.id);
assert.ok(draft, "expected import draft to be created");
});
test("device import apply is idempotent and heartbeat preserves applied status", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "Studio Mac",
avatar: "S",
account: "17600003315",
endpoint: "mac://studio.local",
note: "idempotent import apply",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { pairingCode: string };
device: { id: string };
};
const heartbeatPayload = {
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "Studio Mac",
avatar: "S",
account: "17600003315",
status: "online" as const,
quota5h: 68,
quota7d: 82,
projects: [],
endpoint: "mac://studio.local",
projectCandidates: [
{
folderName: "导入目录",
folderRef: "import-folder",
threadId: "thread-import-1",
threadDisplayName: "导入线程一",
codexFolderRef: "import-folder",
codexThreadRef: "thread-import-1",
lastActiveAt: "2026-03-30T10:40:00+08:00",
suggestedImport: true,
},
],
};
assert.equal(
(
await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(heartbeatPayload),
}),
)
).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; status: "queued" };
};
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: "Studio Mac 导入建议:将导入目录里的线程导入为独立会话。",
items: selectedCandidateIds.map((candidateId) => ({
candidateId,
action: "create_thread_conversation",
reason: "需要保留独立上下文,建议新建会话。",
})),
},
null,
2,
),
},
),
{ params: Promise.resolve({ taskId: reviewPayload.task.taskId }) },
)
).status,
200,
);
const applyUrl = `http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft/apply`;
const firstApply = await applyImportDraftRoute(
await createAuthedRequest(applyUrl, "POST", {}),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
);
assert.equal(firstApply.status, 200);
const secondApply = await applyImportDraftRoute(
await createAuthedRequest(applyUrl, "POST", {}),
{ params: Promise.resolve({ deviceId: enrollmentPayload.device.id }) },
);
assert.equal(secondApply.status, 200);
let nextState = await readState();
const importedProjects = nextState.projects.filter(
(project) => project.threadMeta.codexThreadRef === "thread-import-1",
);
assert.equal(importedProjects.length, 1, "replaying apply should not duplicate imported thread windows");
assert.equal(
(
await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(heartbeatPayload),
}),
)
).status,
200,
);
nextState = await readState();
const appliedDraft = nextState.deviceImportDrafts.find((item) => item.deviceId === enrollmentPayload.device.id);
assert.equal(appliedDraft?.status, "applied", "later heartbeats should not regress applied drafts");
});
test("clearing device import selection resets draft back to pending_selection and drops old resolution", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "Review Mac",
avatar: "R",
account: "17600003315",
endpoint: "mac://review.local",
note: "selection reset",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { pairingCode: string };
device: { id: string };
};
assert.equal(
(
await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "Review Mac",
avatar: "R",
account: "17600003315",
status: "online",
quota5h: 66,
quota7d: 79,
projects: [],
endpoint: "mac://review.local",
projectCandidates: [
{
folderName: "回归目录",
folderRef: "review-folder",
threadId: "thread-review-1",
threadDisplayName: "回归线程一",
codexFolderRef: "review-folder",
codexThreadRef: "thread-review-1",
lastActiveAt: "2026-03-30T11:00:00+08:00",
suggestedImport: true,
},
],
}),
}),
)
).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,
);
assert.equal(
(
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 }) },
)
).status,
200,
);
const clearSelectionResponse = 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 }) },
);
assert.equal(clearSelectionResponse.status, 200);
const nextState = await readState();
const draft = nextState.deviceImportDrafts.find((item) => item.deviceId === enrollmentPayload.device.id);
const resolution = nextState.deviceImportResolutions.find((item) => item.deviceId === enrollmentPayload.device.id);
assert.equal(draft?.status, "pending_selection");
assert.deepEqual(draft?.selectedCandidateIds, []);
assert.equal(draft?.resolutionId, undefined);
assert.equal(resolution, undefined);
});
test("device import routes reject unrelated logged-in members", async () => {
await setup();
const enrollmentResponse = await createEnrollmentRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/devices/enrollments", "POST", {
name: "Build Mac",
avatar: "B",
account: "17600003315",
endpoint: "mac://build.local",
note: "route auth test",
}),
);
assert.equal(enrollmentResponse.status, 200);
const enrollmentPayload = (await enrollmentResponse.json()) as {
enrollment: { pairingCode: string };
device: { id: string };
};
assert.equal(
(
await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: enrollmentPayload.device.id,
pairingCode: enrollmentPayload.enrollment.pairingCode,
name: "Build Mac",
avatar: "B",
account: "17600003315",
status: "online",
quota5h: 71,
quota7d: 80,
projects: [],
endpoint: "mac://build.local",
projectCandidates: [
{
folderName: "受控目录",
folderRef: "secured-folder",
threadId: "thread-secured",
threadDisplayName: "受控线程",
codexFolderRef: "secured-folder",
codexThreadRef: "thread-secured",
lastActiveAt: "2026-03-30T10:50:00+08:00",
suggestedImport: true,
},
],
}),
}),
)
).status,
200,
);
const outsiderRequest = await createAuthedRequestFor(
"15500001111",
"member",
`http://127.0.0.1:3000/api/v1/devices/${enrollmentPayload.device.id}/import-draft`,
"GET",
);
const getResponse = await getImportDraftRoute(outsiderRequest, {
params: Promise.resolve({ deviceId: enrollmentPayload.device.id }),
});
assert.equal(getResponse.status, 403);
});
test("existing bound production devices auto-sync suggested candidates into conversations on heartbeat", async () => {
await setup();
const heartbeatResponse = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: "mac-studio",
token: "boss-mac-studio-token",
name: "Mac Studio",
avatar: "M",
account: "17600003315",
status: "online",
quota5h: 68,
quota7d: 81,
projects: ["Boss 移动控制台", "硬件审计协作"],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "yuandi",
folderRef: "/Users/kris/code/yuandi",
threadId: "session-yuandi-1",
threadDisplayName: "Epicurus",
codexFolderRef: "/Users/kris/code/yuandi",
codexThreadRef: "session-yuandi-1",
lastActiveAt: "2026-03-30T12:42:56+08:00",
suggestedImport: true,
},
{
folderName: "wenshenapp",
folderRef: "/Users/kris/code/wenshenapp",
threadId: "session-wenshenapp-1",
threadDisplayName: "wenshenapp · ea5f",
codexFolderRef: "/Users/kris/code/wenshenapp",
codexThreadRef: "session-wenshenapp-1",
lastActiveAt: "2026-03-30T12:34:51+08:00",
suggestedImport: true,
},
],
}),
}),
);
assert.equal(heartbeatResponse.status, 200);
const payload = (await heartbeatResponse.json()) as {
importDraft?: { status: string; selectedCandidateIds: string[] } | null;
};
assert.equal(payload.importDraft?.status, "applied");
assert.equal(payload.importDraft?.selectedCandidateIds.length, 2);
let nextState = await readState();
const yuandiProject = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "session-yuandi-1",
);
const wenshenProject = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "session-wenshenapp-1",
);
assert.ok(yuandiProject, "expected a discovered yuandi session to become a real chat window");
assert.ok(wenshenProject, "expected a discovered wenshenapp session to become a real chat window");
assert.equal(yuandiProject?.threadMeta.folderName, "yuandi");
assert.equal(wenshenProject?.threadMeta.folderName, "wenshenapp");
const device = nextState.devices.find((item) => item.id === "mac-studio");
assert.deepEqual(device?.projects, ["yuandi", "wenshenapp"]);
const followupHeartbeat = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: "mac-studio",
token: "boss-mac-studio-token",
name: "Mac Studio",
avatar: "M",
account: "17600003315",
status: "online",
quota5h: 68,
quota7d: 81,
projects: ["Boss 移动控制台", "硬件审计协作"],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "yuandi",
folderRef: "/Users/kris/code/yuandi",
threadId: "session-yuandi-1",
threadDisplayName: "Epicurus",
codexFolderRef: "/Users/kris/code/yuandi",
codexThreadRef: "session-yuandi-1",
lastActiveAt: "2026-03-30T12:55:56+08:00",
suggestedImport: true,
},
],
}),
}),
);
assert.equal(followupHeartbeat.status, 200);
nextState = await readState();
const remainingYuandi = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "session-yuandi-1",
);
const removedWenshen = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "session-wenshenapp-1",
);
assert.ok(remainingYuandi, "expected still-selected candidate to stay imported");
assert.equal(removedWenshen, undefined, "auto-sync should prune stale imported threads that disappeared from candidates");
assert.deepEqual(
nextState.devices.find((item) => item.id === "mac-studio")?.projects,
["yuandi"],
);
});