Files
boss/tests/project-goal-events.test.ts

173 lines
6.5 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";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
let toggleGoal: (typeof import("../src/lib/boss-data"))["toggleGoal"];
let updateGoalText: (typeof import("../src/lib/boss-data"))["updateGoalText"];
let createGoal: (typeof import("../src/lib/boss-data"))["createGoal"];
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"];
let forceProjectUnderstandingSyncTask: (typeof import("../src/lib/boss-data"))["forceProjectUnderstandingSyncTask"];
let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"];
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-goal-events-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, events] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-events.ts"),
]);
readState = data.readState;
writeState = data.writeState;
toggleGoal = data.toggleGoal;
updateGoalText = data.updateGoalText;
createGoal = data.createGoal;
updateProjectAgentControls = data.updateProjectAgentControls;
completeMasterAgentTask = data.completeMasterAgentTask;
forceProjectUnderstandingSyncTask = data.forceProjectUnderstandingSyncTask;
subscribeBossEvents = events.subscribeBossEvents;
}
async function resetGoalState() {
const state = await readState();
const existingProject = state.projects.find((project) => project.id !== "master-agent") ?? state.projects[0];
const project = structuredClone(existingProject);
project.id = "project-goal-events";
project.name = "项目目标事件测试";
project.goals = [
{
id: "goal-1",
text: "完成接入验证",
state: "pending",
note: "等待执行",
},
];
project.versions = [];
project.messages = [];
project.lastMessageAt = "2026-04-07T10:00:00.000Z";
state.projects = state.projects.filter((item) => item.id !== "project-goal-events");
state.projects.unshift(project);
await writeState(state);
}
test.beforeEach(async () => {
await setup();
await resetGoalState();
});
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("toggleGoal publishes project goal refresh marker for the project", async () => {
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
const unsubscribe = subscribeBossEvents((event, payload) => {
events.push({ event, payload });
});
await toggleGoal("project-goal-events", "goal-1");
unsubscribe();
const latest = events.at(-1);
assert.ok(latest);
assert.equal(latest.event, "conversation.updated");
assert.equal(latest.payload.projectId, "project-goal-events");
assert.equal(latest.payload.note, "project_goals.updated");
});
test("updateGoalText publishes project goal refresh marker for the project", async () => {
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
const unsubscribe = subscribeBossEvents((event, payload) => {
events.push({ event, payload });
});
await updateGoalText("project-goal-events", "goal-1", "完成接入验证并复盘");
unsubscribe();
const latest = events.at(-1);
assert.ok(latest);
assert.equal(latest.event, "conversation.updated");
assert.equal(latest.payload.projectId, "project-goal-events");
assert.equal(latest.payload.note, "project_goals.updated");
});
test("createGoal publishes project goal refresh marker for the project", async () => {
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
const unsubscribe = subscribeBossEvents((event, payload) => {
events.push({ event, payload });
});
await createGoal("project-goal-events", "补一条回归目标");
unsubscribe();
const latest = events.at(-1);
assert.ok(latest);
assert.equal(latest.event, "conversation.updated");
assert.equal(latest.payload.projectId, "project-goal-events");
assert.equal(latest.payload.note, "project_goals.updated");
});
test("project understanding sync completion also publishes project goal refresh marker", async () => {
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
const unsubscribe = subscribeBossEvents((event, payload) => {
events.push({ event, payload });
});
const state = await readState();
const existingProject = state.projects.find((project) => project.id !== "master-agent");
assert.ok(existingProject);
const project = structuredClone(existingProject);
project.id = "project-goal-understanding-sync";
project.name = "项目目标理解同步测试";
project.goals = [];
project.versions = [];
project.messages = [];
project.lastMessageAt = "2026-04-07T10:00:00.000Z";
project.threadMeta.lastObservedCodexActivityAt = "2026-04-07T10:00:00.000Z";
state.projects = state.projects.filter((item) => item.id !== project.id);
state.projects.unshift(project);
await writeState(state);
await updateProjectAgentControls(project.id, { takeoverEnabled: true });
const queuedTask = await forceProjectUnderstandingSyncTask({
projectId: project.id,
observedActivityAt: "2026-04-07T10:00:00.000Z",
reason: "thread_reply",
});
assert.ok(queuedTask);
await completeMasterAgentTask({
taskId: queuedTask!.taskId,
deviceId: "mac-studio",
status: "completed",
replyBody: JSON.stringify({
projectGoal: "完成项目目标和版本记录自动同步",
currentProgress: "项目目标页需要展示主 Agent 最新核对结果",
technicalArchitecture: "Boss Web 与 Android 共用文件账本状态",
currentBlockers: "",
recommendedNextStep: "继续优化聊天阅读排版",
versionRecord: "已补项目目标/版本记录的自动同步链路。",
}),
});
unsubscribe();
const goalRefreshEvent = events.find(
(item) =>
item.event === "conversation.updated" &&
item.payload.projectId === project.id &&
item.payload.note === "project_goals.updated",
);
assert.ok(goalRefreshEvent, "expected project understanding sync to publish a goal refresh marker");
});