Files
boss/tests/thread-status-sync.test.ts

360 lines
14 KiB
TypeScript

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 type { BossState, MasterAgentTask, Project, ThreadProgressEvent, ThreadStatusDocument } from "../src/lib/boss-data.ts";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let appendProjectMessage: (typeof import("../src/lib/boss-data"))["appendProjectMessage"];
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
type MutableBossState = BossState & {
threadStatusDocuments: ThreadStatusDocument[];
threadProgressEvents: ThreadProgressEvent[];
masterAgentTasks: MasterAgentTask[];
projects: Project[];
};
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-thread-status-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const data = await import("../src/lib/boss-data.ts");
readState = data.readState;
writeState = data.writeState;
appendProjectMessage = data.appendProjectMessage;
updateProjectAgentControls = data.updateProjectAgentControls;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("thread status documents and progress events normalize, sort, and trim correctly", async () => {
await setup();
const state = (await readState()) as MutableBossState;
const baseDocTime = Date.parse("2026-04-04T10:00:00.000Z");
state.threadStatusDocuments = Array.from({ length: 81 }, (_, index) => ({
documentId: `doc-${index}`,
projectId: "master-agent",
threadId: `thread-${index}`,
threadDisplayName: index === 80 ? " 树莓派二代查询 " : `线程 ${index}`,
folderName: index === 80 ? " Talking " : `文件夹 ${index}`,
deviceId: " mac-studio ",
projectGoal: index === 80 ? " 完成树莓派二代查询链路 " : `目标 ${index}`,
currentPhase: index === 80 ? " 功能实现 " : `阶段 ${index}`,
currentProgress: index === 80 ? " 已完成排序修复 " : `进度 ${index}`,
technicalArchitecture: index === 80 ? " Next.js API + Android 原生客户端 " : `架构 ${index}`,
currentBlockers: index === 80 ? " " : `阻塞 ${index}`,
recommendedNextStep: index === 80 ? " 补会话页展示与排序 " : `下一步 ${index}`,
keyFiles: index === 80 ? [" src/lib/boss-data.ts ", " tests/thread-status-sync.test.ts "] : [`file-${index}.ts`],
keyCommands: index === 80 ? [" npm run build ", " npm run lint "] : [`cmd-${index}`],
updatedAt: new Date(baseDocTime + index * 60_000).toISOString(),
sourceTaskId: `task-${index}`,
sourceKind: index === 80 ? undefined : "full_sync",
}));
state.threadProgressEvents = [
...Array.from({ length: 25 }, (_, index) => ({
eventId: `event-a-${index}`,
projectId: "master-agent",
threadId: "thread-a",
threadDisplayName: "线程 A",
deviceId: "mac-studio",
eventType: "progress_updated",
summary: `线程 A 进展 ${index}`,
phase: "功能实现",
createdAt: `2026-04-04T19:${String(index).padStart(2, "0")}:00+08:00`,
sourceTaskId: `task-a-${index}`,
})),
...Array.from({ length: 381 }, (_, index) => ({
eventId: `event-b-${index}`,
projectId: "master-agent",
threadId: `thread-b-${index}`,
threadDisplayName: `线程 B${index}`,
deviceId: "mac-studio",
eventType: "progress_updated",
summary: `线程 B${index} 进展`,
createdAt: `2026-04-04T17:${String(index % 60).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}+08:00`,
sourceTaskId: `task-b-${index}`,
})),
];
await writeState(state);
const normalized = (await readState()) as MutableBossState;
assert.equal(normalized.threadStatusDocuments.length, 80);
assert.equal(normalized.threadStatusDocuments[0]?.documentId, "doc-80");
assert.equal(normalized.threadStatusDocuments[0]?.threadDisplayName, "树莓派二代查询");
assert.equal(normalized.threadStatusDocuments[0]?.folderName, "Talking");
assert.equal(normalized.threadStatusDocuments[0]?.deviceId, "mac-studio");
assert.equal(normalized.threadStatusDocuments[0]?.projectGoal, "完成树莓派二代查询链路");
assert.equal(normalized.threadStatusDocuments[0]?.currentPhase, "功能实现");
assert.deepEqual(normalized.threadStatusDocuments[0]?.keyFiles, [
"src/lib/boss-data.ts",
"tests/thread-status-sync.test.ts",
]);
assert.deepEqual(normalized.threadStatusDocuments[0]?.keyCommands, ["npm run build", "npm run lint"]);
assert.equal(normalized.threadStatusDocuments[0]?.sourceKind, "incremental_sync");
assert.equal(normalized.threadProgressEvents.length, 400);
assert.equal(normalized.threadProgressEvents[0]?.eventId, "event-a-24");
assert.equal(
normalized.threadProgressEvents.filter((event) => event.projectId === "master-agent" && event.threadId === "thread-a")
.length,
20,
);
});
test("thread replies append lightweight progress events and skip redundant understanding sync when status document is fresh", async () => {
await setup();
const state = (await readState()) as MutableBossState;
state.threadProgressEvents = [];
state.projects.push({
id: "thread-sync-demo",
name: "线程状态演示",
pinned: false,
deviceIds: ["mac-studio"],
preview: "初始状态",
updatedAt: "2026-04-04T18:00:00+08:00",
lastMessageAt: "2026-04-04T18:00:00+08:00",
isGroup: false,
threadMeta: {
projectId: "thread-sync-demo",
threadId: "thread-sync-demo-thread",
threadDisplayName: "线程状态演示",
folderName: "演示文件夹",
activityIconCount: 1,
updatedAt: "2026-04-04T18:00:00+08:00",
lastObservedCodexActivityAt: "2026-04-04T18:00:00+08:00",
lastProjectUnderstandingRequestedAt: "2026-04-04T17:00:00+08:00",
lastProjectUnderstandingSyncedAt: "2026-04-04T18:00:00+08:00",
codexThreadRef: "thread-sync-demo-thread",
codexFolderRef: "thread-sync-demo-folder",
},
groupMembers: [],
createdByAgent: false,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 0,
riskLevel: "low",
projectUnderstanding: {
projectGoal: "完成线程状态回归",
currentProgress: "旧进度",
technicalArchitecture: "旧架构",
currentBlockers: "",
recommendedNextStep: "旧下一步",
sourceTaskId: "task-old",
updatedAt: "2026-04-04T18:00:00+08:00",
sourceKind: "thread_sync",
},
messages: [],
goals: [],
versions: [],
} as Project);
state.threadStatusDocuments = [
{
documentId: "doc-old",
projectId: "thread-sync-demo",
threadId: "thread-sync-demo-thread",
threadDisplayName: "线程状态演示",
folderName: "演示文件夹",
deviceId: "mac-studio",
projectGoal: "完成线程状态回归",
currentPhase: "全量理解",
currentProgress: "旧进度",
technicalArchitecture: "旧架构",
currentBlockers: "",
recommendedNextStep: "旧下一步",
keyFiles: ["src/lib/boss-data.ts"],
keyCommands: ["npm run build"],
updatedAt: "2026-04-04T18:00:00+08:00",
sourceTaskId: "task-old",
sourceKind: "full_sync",
},
];
await writeState(state);
const before = (await readState()) as MutableBossState;
const beforeCount = before.threadProgressEvents.filter(
(event) => event.projectId === "thread-sync-demo",
).length;
const message = await appendProjectMessage({
projectId: "thread-sync-demo",
sender: "device",
senderLabel: "线程执行器",
body: "已完成手机端排序修复",
kind: "text",
});
assert.equal(message.body, "已完成手机端排序修复");
const after = (await readState()) as MutableBossState;
const events = after.threadProgressEvents.filter((event) => event.projectId === "thread-sync-demo");
assert.equal(events.length, beforeCount + 1);
assert.equal(events[0]?.summary, "已完成手机端排序修复");
assert.equal(events[0]?.eventType, "progress_updated");
assert.equal(
after.masterAgentTasks.some(
(task) =>
task.projectUnderstandingTargetProjectId === "thread-sync-demo" && task.status === "queued",
),
false,
);
});
test("turning off single-thread takeover clears pending project understanding sync tasks for that thread", async () => {
await setup();
const projectId = "thread-sync-takeover-clear";
const state = (await readState()) as MutableBossState;
state.projects = state.projects.filter((project) => project.id !== projectId);
state.userProjectAgentControls = state.userProjectAgentControls.filter(
(item) => item.projectId !== projectId,
);
state.masterAgentTasks = state.masterAgentTasks.filter(
(task) => task.projectUnderstandingTargetProjectId !== projectId,
);
state.projects.push({
id: projectId,
name: "接管关闭清理演示",
pinned: false,
deviceIds: ["mac-studio"],
preview: "等待同步",
updatedAt: "2026-04-04T18:00:00+08:00",
lastMessageAt: "2026-04-04T18:00:00+08:00",
isGroup: false,
threadMeta: {
projectId,
threadId: "thread-sync-takeover-clear-thread",
threadDisplayName: "接管关闭清理演示",
folderName: "演示文件夹",
activityIconCount: 1,
updatedAt: "2026-04-04T18:00:00+08:00",
codexThreadRef: "thread-sync-takeover-clear-thread",
codexFolderRef: "thread-sync-takeover-clear-folder",
},
groupMembers: [],
createdByAgent: false,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 0,
riskLevel: "low",
messages: [],
goals: [],
versions: [],
} as Project);
state.userProjectAgentControls.push({
account: "krisolo",
projectId,
controls: {
takeoverEnabled: true,
updatedAt: "2026-04-04T18:00:00+08:00",
},
});
state.masterAgentTasks.unshift(
{
taskId: "queued-understanding-clear",
projectId: "master-agent",
taskType: "conversation_reply",
requestMessageId: "message-understanding-clear",
requestText: "请同步项目状态",
executionPrompt: "你正在向主 Agent 同步当前项目状态。",
requestedBy: "krisolo",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
targetProjectId: projectId,
targetThreadId: "thread-sync-takeover-clear-thread",
targetThreadDisplayName: "接管关闭清理演示",
projectUnderstandingTargetProjectId: projectId,
projectUnderstandingReason: "heartbeat_activity",
status: "queued",
requestedAt: "2026-04-04T18:01:00+08:00",
},
{
taskId: "completed-understanding-kept",
projectId: "master-agent",
taskType: "conversation_reply",
requestMessageId: "message-understanding-completed",
requestText: "请同步项目状态",
executionPrompt: "你正在向主 Agent 同步当前项目状态。",
requestedBy: "krisolo",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
targetProjectId: projectId,
targetThreadId: "thread-sync-takeover-clear-thread",
targetThreadDisplayName: "接管关闭清理演示",
projectUnderstandingTargetProjectId: projectId,
projectUnderstandingReason: "heartbeat_activity",
status: "completed",
requestedAt: "2026-04-04T18:00:30+08:00",
completedAt: "2026-04-04T18:00:45+08:00",
replyBody: "{}",
},
);
await writeState(state);
const controls = await updateProjectAgentControls(projectId, { takeoverEnabled: false }, "krisolo");
assert.equal(controls?.effectiveTakeoverEnabled, false);
const after = (await readState()) as MutableBossState;
assert.equal(
after.masterAgentTasks.some((task) => task.taskId === "queued-understanding-clear"),
false,
);
assert.equal(
after.masterAgentTasks.some((task) => task.taskId === "completed-understanding-kept"),
true,
);
});
test("turning off global takeover clears pending project understanding sync tasks", async () => {
await setup();
const projectId = "thread-sync-global-clear";
const state = (await readState()) as MutableBossState;
state.masterAgentTasks = state.masterAgentTasks.filter(
(task) => task.projectUnderstandingTargetProjectId !== projectId,
);
state.masterAgentTasks.unshift({
taskId: "running-global-understanding-clear",
projectId: "master-agent",
taskType: "conversation_reply",
requestMessageId: "message-global-understanding-clear",
requestText: "请同步项目状态",
executionPrompt: "你正在向主 Agent 同步当前项目状态。",
requestedBy: "krisolo",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
targetProjectId: projectId,
targetThreadId: "thread-sync-global-clear-thread",
targetThreadDisplayName: "全局接管清理演示",
projectUnderstandingTargetProjectId: projectId,
projectUnderstandingReason: "heartbeat_activity",
status: "running",
requestedAt: "2026-04-04T18:02:00+08:00",
claimedAt: "2026-04-04T18:02:05+08:00",
});
await writeState(state);
await updateProjectAgentControls("master-agent", { globalTakeoverEnabled: true }, "krisolo");
const controls = await updateProjectAgentControls("master-agent", { globalTakeoverEnabled: false }, "krisolo");
assert.equal(controls?.globalTakeoverEnabled, false);
const after = (await readState()) as MutableBossState;
assert.equal(
after.masterAgentTasks.some((task) => task.taskId === "running-global-understanding-clear"),
false,
);
});