Files
boss/tests/conversation-home-items.test.ts

1443 lines
49 KiB
TypeScript
Raw Permalink 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, writeFile } 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 updateConversationAction: (typeof import("../src/lib/boss-data"))["updateConversationAction"];
let getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"];
let getConversationWebItems: (typeof import("../src/lib/boss-projections"))["getConversationWebItems"];
let getConversationHomeItemForProject: (typeof import("../src/lib/boss-projections"))["getConversationHomeItemForProject"];
let getConversationThreadItemForProject: (typeof import("../src/lib/boss-projections"))["getConversationThreadItemForProject"];
let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"];
let getProjectDetailView: (typeof import("../src/lib/boss-projections"))["getProjectDetailView"];
let buildProjectMessagesRealtimePayload: (typeof import("../src/lib/boss-projections"))["buildProjectMessagesRealtimePayload"];
let formatTimestampLabel: (typeof import("../src/lib/boss-projections"))["formatTimestampLabel"];
let getConversationListItemPresentation: (typeof import("../src/components/app-ui"))["getConversationListItemPresentation"];
let getConversationActionAvailability: (typeof import("../src/components/app-ui"))["getConversationActionAvailability"];
let getConversationActionsPath: (typeof import("../src/components/app-ui"))["getConversationActionsPath"];
let getConversationPinnedBadgeLabel: (typeof import("../src/components/app-ui"))["getConversationPinnedBadgeLabel"];
let seededStateSnapshot: Awaited<ReturnType<typeof import("../src/lib/boss-data").readState>> | null = null;
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-conversation-home-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, projections, ui] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-projections.ts"),
import("../src/components/app-ui.tsx"),
]);
readState = data.readState;
writeState = data.writeState;
updateConversationAction = data.updateConversationAction;
getConversationHomeItems = projections.getConversationHomeItems;
getConversationWebItems = projections.getConversationWebItems;
getConversationHomeItemForProject = projections.getConversationHomeItemForProject;
getConversationThreadItemForProject = projections.getConversationThreadItemForProject;
getConversationFolderView = projections.getConversationFolderView;
getProjectDetailView = projections.getProjectDetailView;
buildProjectMessagesRealtimePayload = projections.buildProjectMessagesRealtimePayload;
formatTimestampLabel = projections.formatTimestampLabel;
getConversationListItemPresentation = ui.getConversationListItemPresentation;
getConversationActionAvailability = ui.getConversationActionAvailability;
getConversationActionsPath = ui.getConversationActionsPath;
getConversationPinnedBadgeLabel = ui.getConversationPinnedBadgeLabel;
seededStateSnapshot = structuredClone(await readState());
}
async function resetSeedState() {
if (!seededStateSnapshot) {
throw new Error("seeded state snapshot missing");
}
await writeState(structuredClone(seededStateSnapshot));
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
function buildImportedThreadProject(deviceId: string, id: string, folderName: string, codexFolderRef: string, threadName: string, threadId: string, lastMessageAt: string) {
return {
id,
name: threadName,
pinned: false,
systemPinned: false,
deviceIds: [deviceId],
preview: `最近消息:${threadName}`,
updatedAt: lastMessageAt,
lastMessageAt,
isGroup: false,
threadMeta: {
projectId: id,
threadId,
threadDisplayName: threadName,
folderName,
activityIconCount: 1,
updatedAt: lastMessageAt,
codexFolderRef,
codexThreadRef: threadId,
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development" as const,
approvalState: "not_required" as const,
unreadCount: 0,
riskLevel: "low" as const,
messages: [],
goals: [],
versions: [],
};
}
function buildThreadContextSnapshot(
projectId: string,
threadId: string,
title: string,
contextBudgetLevel: "safe" | "watch" | "urgent" | "critical",
mustFinishBeforeCompaction: boolean,
contextBudgetRemainingPct: number,
capturedAt: string,
nodeId: string,
) {
return {
snapshotId: `${projectId}-${threadId}-snapshot`,
projectId,
taskId: `${projectId}-${threadId}-task`,
threadId,
title,
summary: `${title} 的线程状态`,
nodeId,
workerId: "worker-1",
sourceKind: "worker_estimator",
status: "context_urgent",
contextBudgetRemainingPct,
contextBudgetLevel,
mustFinishBeforeCompaction,
estimatedRemainingTurns: 4,
estimatedRemainingLargeMessages: 2,
compactionCount: 0,
patchPending: false,
testsPending: false,
evidencePending: false,
checklist: [],
capturedAt,
} satisfies import("../src/lib/boss-data").ThreadContextSnapshot;
}
test("folder archives use the latest thread preview/time while subtitle and context ring come from the right threads", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-latest",
"Boss",
"boss",
"最新线程",
"thread-latest",
"2026-04-04T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-urgent",
"Boss",
"boss",
"优先收尾",
"thread-urgent",
"2026-04-04T11:00:00+08:00",
),
);
state.threadContextSnapshots = [
buildThreadContextSnapshot(
"boss-thread-latest",
"thread-latest",
"最新线程",
"critical",
false,
12,
"2026-04-04T12:05:00+08:00",
"node-latest",
),
buildThreadContextSnapshot(
"boss-thread-urgent",
"thread-urgent",
"优先收尾",
"critical",
true,
87,
"2026-04-04T11:05:00+08:00",
"node-urgent",
),
];
const originalNow = Date.now;
Date.now = () => new Date("2026-04-04T12:30:00+08:00").getTime();
try {
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.equal(folder?.threadTitle, "Boss");
assert.equal(folder?.folderLabel, "2 个线程 · 最近:最新线程");
assert.equal(folder?.preview, "最近消息:最新线程");
assert.equal(folder?.lastMessagePreview, "最近消息:最新线程");
assert.equal(folder?.latestReplyAt, "2026-04-04T12:00:00+08:00");
assert.equal(folder?.latestReplyLabel, formatTimestampLabel("2026-04-04T12:00:00+08:00"));
assert.equal(folder?.contextBudgetIndicator.level, "critical");
assert.equal(folder?.contextBudgetIndicator.percent, 87);
assert.equal(folder?.mustFinishBeforeCompaction, true);
assert.equal(folder?.contextBudgetSourceNodeId, "node-urgent");
assert.equal(folder?.contextBudgetUpdatedAt, "2026-04-04T11:05:00+08:00");
} finally {
Date.now = originalNow;
}
});
test("conversation home patch lookup returns the visible folder archive item for grouped threads", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-a",
"Boss",
"boss",
"线程 A",
"thread-a",
"2026-04-04T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-b",
"Boss",
"boss",
"线程 B",
"thread-b",
"2026-04-04T11:00:00+08:00",
),
);
const item = getConversationHomeItemForProject(state, "boss-thread-b");
assert.ok(item, "expected grouped thread lookup to resolve to a visible home item");
assert.equal(item?.conversationType, "folder_archive");
assert.equal(item?.projectId, "mac-studio:boss");
assert.deepEqual(item?.searchTargetProjectIds, ["boss-thread-a", "boss-thread-b"]);
});
test("conversation home patch lookup returns the direct thread item when no folder archive exists", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"solo-thread",
"Solo",
"solo",
"单线程",
"thread-solo",
"2026-04-04T12:00:00+08:00",
),
);
const item = getConversationHomeItemForProject(state, "solo-thread");
assert.ok(item, "expected single thread lookup to resolve to its visible home item");
assert.equal(item?.conversationType, "single_device");
assert.equal(item?.projectId, "solo-thread");
});
test("conversation thread patch lookup keeps the direct single-thread item for grouped folders", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-a",
"Boss",
"boss",
"线程 A",
"thread-a",
"2026-04-04T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-b",
"Boss",
"boss",
"线程 B",
"thread-b",
"2026-04-04T11:00:00+08:00",
),
);
const item = getConversationThreadItemForProject(state, "boss-thread-b");
assert.ok(item, "expected grouped thread lookup to resolve to its direct thread row");
assert.equal(item?.conversationType, "single_device");
assert.equal(item?.projectId, "boss-thread-b");
assert.equal(item?.folderKey, "mac-studio:boss");
});
test("folder archive context ring prefers more urgent contextBudgetLevel when mustFinishBeforeCompaction is equal", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-latest",
"Boss",
"boss",
"最新线程",
"thread-latest",
"2026-04-04T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-watch",
"Boss",
"boss",
"次要关注",
"thread-watch",
"2026-04-04T11:00:00+08:00",
),
);
state.threadContextSnapshots = [
buildThreadContextSnapshot(
"boss-thread-latest",
"thread-latest",
"最新线程",
"watch",
false,
22,
"2026-04-04T12:05:00+08:00",
"node-watch",
),
buildThreadContextSnapshot(
"boss-thread-watch",
"thread-watch",
"次要关注",
"urgent",
false,
61,
"2026-04-04T11:05:00+08:00",
"node-urgent",
),
];
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.equal(folder?.contextBudgetIndicator.level, "urgent");
assert.equal(folder?.contextBudgetIndicator.percent, 61);
assert.equal(folder?.contextBudgetSourceNodeId, "node-urgent");
assert.equal(folder?.contextBudgetUpdatedAt, "2026-04-04T11:05:00+08:00");
});
test("folder archive context ring prefers newer latestReplyAt when mustFinishBeforeCompaction and contextBudgetLevel are equal", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-older",
"Boss",
"boss",
"较早线程",
"thread-older",
"2026-04-04T10:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-newer",
"Boss",
"boss",
"较新线程",
"thread-newer",
"2026-04-04T12:00:00+08:00",
),
);
state.threadContextSnapshots = [
buildThreadContextSnapshot(
"boss-thread-older",
"thread-older",
"较早线程",
"urgent",
false,
19,
"2026-04-04T10:05:00+08:00",
"node-older",
),
buildThreadContextSnapshot(
"boss-thread-newer",
"thread-newer",
"较新线程",
"urgent",
false,
19,
"2026-04-04T12:05:00+08:00",
"node-newer",
),
];
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.equal(folder?.latestReplyAt, "2026-04-04T12:00:00+08:00");
assert.equal(folder?.contextBudgetIndicator.level, "urgent");
assert.equal(folder?.contextBudgetIndicator.percent, 19);
assert.equal(folder?.contextBudgetSourceNodeId, "node-newer");
assert.equal(folder?.contextBudgetUpdatedAt, "2026-04-04T12:05:00+08:00");
});
test("conversation home stays visually empty after history is cleared", async () => {
await setup();
const state = await readState();
state.conversationHistoryClearedAt = "2026-04-24T10:00:00.000Z";
state.projects = state.projects.filter((project) => project.id === "master-agent");
const masterProject = state.projects.find((project) => project.id === "master-agent");
assert.ok(masterProject, "expected master-agent project");
if (masterProject) {
masterProject.preview = "";
masterProject.messages = [];
masterProject.unreadCount = 0;
}
state.threadContextSnapshots = [];
state.opsFaults = [];
state.auditRequests = [];
state.auditResults = [];
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-clear-a",
"Boss",
"boss",
"线程 A",
"thread-clear-a",
"2026-04-24T09:00:00.000Z",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-clear-b",
"Boss",
"boss",
"线程 B",
"thread-clear-b",
"2026-04-24T08:00:00.000Z",
),
);
for (const project of state.projects) {
if (project.id !== "master-agent") {
project.preview = "";
project.messages = [];
project.unreadCount = 0;
}
}
const items = getConversationHomeItems(state);
const masterItem = items.find((item) => item.projectId === "master-agent");
const folderItem = items.find((item) => item.conversationType === "folder_archive");
assert.ok(masterItem, "expected master-agent home item");
assert.equal(masterItem?.preview, "");
assert.equal(masterItem?.lastMessagePreview, "");
assert.ok(folderItem, "expected folder archive item");
assert.equal(folderItem?.preview, "");
assert.equal(folderItem?.lastMessagePreview, "");
});
test("conversation home hides legacy process-like previews on direct thread rows", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"legacy-process-preview-thread",
"Boss",
"boss",
"Boss开发主线程",
"thread-legacy-process-preview",
"2026-04-24T18:30:00+08:00",
),
preview: "我继续把这条链路又往下收了一层,补的是“历史脏消息”的兼容,不只是新消息规则。",
messages: [
{
id: "legacy-process-preview-message",
sender: "device",
senderLabel: "Boss开发主线程",
body: "我继续把这条链路又往下收了一层,补的是“历史脏消息”的兼容,不只是新消息规则。",
sentAt: "2026-04-24T18:30:00+08:00",
kind: "thread_process",
},
],
unreadCount: 0,
});
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "legacy-process-preview-thread");
assert.ok(item, "expected direct thread home item");
assert.equal(item?.preview, "");
assert.equal(item?.lastMessagePreview, "");
});
test("conversation home collapses multiline markdown-heavy previews into a short digest", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"markdown-heavy-preview-thread",
"Boss",
"boss",
"Boss开发主线程",
"thread-markdown-heavy-preview",
"2026-04-24T18:31:00+08:00",
),
preview:
"这轮我继续把真机阻塞往下压了一层,而且拿到了比 xcodebuild.log 更强的设备侧证据。\n\n" +
"我先把设备 syslog 伴随采集正式接进了 run_ipad_harness.sh并先用失败断言锁住在 run-ipad-harness-source.test.ts。\n" +
"现在每次 harness 真机跑都会自动落盘 device-syslog.log 和 device-syslog-signals.log。",
messages: [
{
id: "markdown-heavy-preview-message",
sender: "device",
senderLabel: "Boss开发主线程",
body:
"这轮我继续把真机阻塞往下压了一层,而且拿到了比 xcodebuild.log 更强的设备侧证据。\n\n" +
"我先把设备 syslog 伴随采集正式接进了 run_ipad_harness.sh并先用失败断言锁住在 run-ipad-harness-source.test.ts。\n" +
"现在每次 harness 真机跑都会自动落盘 device-syslog.log 和 device-syslog-signals.log。",
sentAt: "2026-04-24T18:31:00+08:00",
kind: "text",
},
],
unreadCount: 1,
});
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "markdown-heavy-preview-thread");
assert.ok(item, "expected direct thread home item");
assert.equal(item?.preview.includes("\n"), false);
assert.equal(item?.lastMessagePreview.includes("\n"), false);
assert.equal((item?.preview ?? "").length <= 72, true);
assert.match(item?.preview ?? "", /这轮我继续把真机阻塞往下压了一层/);
});
test("conversation home compacts structured project summary json previews into readable text", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"summary-thread-1",
"luyinka",
"/Users/kris/code/luyinka",
"Android 对齐",
"thread-1",
"2026-04-24T12:00:00+08:00",
),
preview: JSON.stringify({
projectGoal: "以安卓成熟版和正式截图为基准完成鸿蒙原生 SHMCI 的页面、录音卡链路与主要功能对齐。",
currentProgress: "主链路页面已对齐,正在收口录音卡和设备联调。",
technicalArchitecture: "鸿蒙原生 ArkUI + 音频链路适配。",
currentBlockers: "",
recommendedNextStep: "继续真机回归。",
}),
lastMessageAt: "2026-04-24T12:00:00+08:00",
});
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "summary-thread-1");
assert.ok(item, "expected json preview item");
assert.equal(item?.preview.startsWith("{"), false);
assert.match(item?.preview ?? "", /目标:以安卓成熟版和正式截图为基准完成鸿蒙原生 SHMCI/);
assert.equal((item?.preview ?? "").length <= 73, true);
});
test("conversation home upgrades and downgrades a folder archive as thread count crosses 1 and 2", async () => {
await setup();
const state = await readState();
try {
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-04-04T10:00:00+08:00",
),
);
await writeFile(process.env.BOSS_STATE_FILE as string, `${JSON.stringify(state, null, 2)}\n`);
let items = getConversationHomeItems(state);
let direct = items.find((item) => item.projectId === "boss-thread-1");
assert.ok(direct, "expected a single thread to remain direct");
assert.equal(direct?.conversationType, "single_device");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-04-04T11:00:00+08:00",
),
);
items = getConversationHomeItems(state);
const folder = items.find(
(item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss",
);
assert.ok(folder, "expected folder archive once the folder has 2 threads");
assert.equal(folder?.threadCount, 2);
assert.equal(items.some((item) => item.projectId === "boss-thread-1"), false);
assert.equal(items.some((item) => item.projectId === "boss-thread-2"), false);
state.projects = state.projects.filter((project) => project.id !== "boss-thread-2");
items = getConversationHomeItems(state);
direct = items.find((item) => item.projectId === "boss-thread-1");
assert.ok(direct, "expected a single remaining thread to downgrade back to direct");
assert.equal(direct?.conversationType, "single_device");
assert.equal(
items.some((item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss"),
false,
);
} finally {
await resetSeedState();
}
});
test("folder archive pin state follows child threads and folder toggle syncs all threads", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
{
...buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-04-04T10:00:00+08:00",
),
pinned: true,
},
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-04-04T11:00:00+08:00",
),
);
const items = getConversationHomeItems(state);
let folder = items.find((item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss");
assert.ok(folder, "expected folder archive once the folder has 2 threads");
assert.equal(folder?.topPinnedLabel, "置顶");
assert.equal(folder?.manualPinned, true);
for (const project of state.projects.filter((project) => project.id.startsWith("boss-thread"))) {
project.pinned = false;
}
folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss");
assert.ok(folder, "expected folder archive after unpinning folder");
assert.equal(folder?.topPinnedLabel, undefined);
assert.equal(folder?.manualPinned, false);
for (const project of state.projects.filter((project) => project.id.startsWith("boss-thread"))) {
project.pinned = true;
}
folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive" && item.folderKey === "mac-studio:boss");
assert.ok(folder, "expected folder archive after restoring folder pin");
assert.equal(folder?.topPinnedLabel, "置顶");
assert.equal(folder?.manualPinned, true);
});
test("folder archive toggle_pin updates all threads that share the folder key", async () => {
await setup();
const state = await readState();
try {
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"yuandi-thread-1",
"园地",
"/Users/kris/code/yuandi",
"线程一",
"thread-1",
"2026-04-05T10:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"yuandi-thread-2",
"园地",
"/Users/kris/code/yuandi",
"线程二",
"thread-2",
"2026-04-05T11:00:00+08:00",
),
);
await writeFile(process.env.BOSS_STATE_FILE as string, `${JSON.stringify(state, null, 2)}\n`);
await updateConversationAction("mac-studio:/users/kris/code/yuandi", "toggle_pin");
let nextState = await readState();
assert.deepEqual(
nextState.projects
.filter((project) => project.id.startsWith("yuandi-thread-"))
.map((project) => project.pinned),
[true, true],
);
await updateConversationAction("mac-studio:/users/kris/code/yuandi", "toggle_pin");
nextState = await readState();
assert.deepEqual(
nextState.projects
.filter((project) => project.id.startsWith("yuandi-thread-"))
.map((project) => project.pinned),
[false, false],
);
} finally {
await resetSeedState();
}
});
test("conversation home groups multiple imported threads by folder while keeping single-thread projects direct", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-03-30T12:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"yuandi-thread-1",
"源地",
"yuandi",
"首页回归",
"thread-3",
"2026-03-30T10:00:00+08:00",
),
);
const homeItems = getConversationHomeItems(state);
const bossFolder = homeItems.find((item) => item.conversationType === "folder_archive");
const directThread = homeItems.find((item) => item.projectId === "yuandi-thread-1");
assert.ok(bossFolder, "expected grouped folder item for multi-thread project");
assert.equal(bossFolder?.threadTitle, "Boss");
assert.equal(bossFolder?.threadCount, 2);
assert.equal(bossFolder?.folderKey, "mac-studio:boss");
assert.ok(directThread, "expected single-thread project to stay direct");
assert.equal(directThread?.conversationType, "single_device");
assert.equal(directThread?.threadTitle, "首页回归");
const folderView = getConversationFolderView(state, "mac-studio:boss");
assert.ok(folderView, "expected folder detail view");
assert.equal(folderView?.threadCount, 2);
assert.deepEqual(
folderView?.threads.map((item) => item.threadTitle),
["发布回滚", "归档确认"],
);
});
test("folder archive homepage rows keep the project title, compact subtitle, and folder route", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-03-30T12:00:00+08:00",
),
);
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.deepEqual(folder?.searchAliases, ["发布回滚", "归档确认"]);
const presentation = getConversationListItemPresentation({
...folder!,
projectTitle: "项目标题",
threadTitle: "线程标题",
});
assert.equal(presentation.title, "项目标题");
assert.equal(presentation.subtitle, "2 个线程 · 最近:发布回滚");
assert.equal(presentation.href, "/conversations/folders/mac-studio%3Aboss");
});
test("conversation home compacts imported previews and trims local workspace prefixes from folder subtitles", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
{
...buildImportedThreadProject(
"mac-studio",
"wenshen-thread-1",
"wenshenapp",
"/Users/kris/code/wenshenapp",
"/Users/kris/code/wenshenapp/docs/superpowers/specs/2026-04-06-context-cleanup-design.md",
"thread-1",
"2026-04-06T15:00:00+08:00",
),
preview: "已从设备 Mac Studio 导入线程《/Users/kris/code/wenshenapp/docs/superpowers/specs/2026-04-06-context-cleanup-design.md》。",
lastMessageAt: "2026-04-06T15:00:00+08:00",
},
buildImportedThreadProject(
"mac-studio",
"wenshen-thread-2",
"wenshenapp",
"/Users/kris/code/wenshenapp",
"补齐 Android 会话可读性",
"thread-2",
"2026-04-06T14:00:00+08:00",
),
);
const folder = getConversationHomeItems(state).find((item) => item.projectId === "mac-studio:/users/kris/code/wenshenapp");
assert.ok(folder, "expected grouped folder archive item");
assert.equal(folder?.conversationType, "folder_archive");
assert.equal(folder?.folderLabel, "2 个线程 · 最近wenshenapp/docs/superpowers/specs/2026-04-06-context-cleanup-design.md");
assert.equal(folder?.preview, "已导入线程");
assert.equal(folder?.lastMessagePreview, "已导入线程");
});
test("conversation home compacts imported previews and trims local workspace prefixes for single-thread items", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"zhanglaoshi-thread-1",
"zhanglaoshi",
"/Users/kris/code/zhanglaoshi",
"/Users/kris/code/zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md",
"thread-1",
"2026-04-06T15:00:00+08:00",
),
preview: "已从设备 Mac Studio 导入线程《zhanglaoshi · 019d61》。",
lastMessageAt: "2026-04-06T15:00:00+08:00",
});
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "zhanglaoshi-thread-1");
assert.ok(item, "expected single imported thread item");
assert.equal(item?.conversationType, "single_device");
assert.equal(item?.threadTitle, "zhanglaoshi/docs/superpowers/specs/2026-04-06-single-thread-cleanup-design.md");
assert.equal(item?.preview, "已导入线程");
assert.equal(item?.lastMessagePreview, "已导入线程");
});
test("conversation home sanitizes leaked prompt titles to folder fallbacks", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-prompt",
"boss",
"/Users/kris/code/boss",
"你当前接手的项目根目录是:",
"thread-prompt",
"2026-04-24T10:00:00+08:00",
),
buildImportedThreadProject(
"mac-studio",
"yuandi-thread-prompt",
"yuandi",
"/Users/kris/code/yuandi",
"你现在接手的项目根目录是 /Users/kris/code/yuandi。",
"thread-prompt-2",
"2026-04-24T10:05:00+08:00",
),
);
const bossItem = getConversationHomeItems(state).find((item) => item.projectId === "boss-thread-prompt");
const yuandiItem = getConversationHomeItems(state).find((item) => item.projectId === "yuandi-thread-prompt");
assert.ok(bossItem, "expected prompt-leak boss item");
assert.equal(bossItem?.conversationType, "single_device");
assert.equal(bossItem?.projectTitle, "boss");
assert.equal(bossItem?.threadTitle, "boss");
assert.ok(yuandiItem, "expected prompt-leak yuandi item");
assert.equal(yuandiItem?.conversationType, "single_device");
assert.equal(yuandiItem?.projectTitle, "yuandi");
assert.equal(yuandiItem?.threadTitle, "yuandi");
});
test("project detail view sanitizes leaked prompt title for single-thread projects", async () => {
await setup();
await resetSeedState();
const state = await readState();
const project = buildImportedThreadProject(
"mac-studio",
"detail-sanitize-thread",
"boss",
"boss",
"你当前接手的项目根目录是:",
"detail-sanitize-thread-id",
"2026-04-24T12:00:00+08:00",
);
state.projects.push(project);
project.name = "你当前接手的项目根目录是:";
project.threadMeta.threadDisplayName = "你当前接手的项目根目录是:";
project.threadMeta.folderName = "boss";
project.threadMeta.codexFolderRef = "boss";
const detail = getProjectDetailView(state, project.id);
assert.ok(detail, "expected project detail");
assert.equal(detail?.project.name, "boss");
assert.equal(detail?.project.threadMeta.threadDisplayName, "boss");
});
test("project messages realtime payload sanitizes leaked prompt title for thread chat header", async () => {
await setup();
await resetSeedState();
const state = await readState();
const project = buildImportedThreadProject(
"mac-studio",
"messages-sanitize-thread",
"yuandi",
"yuandi",
"你现在接手的项目根目录是 /Users/kris/code/yuandi。",
"messages-sanitize-thread-id",
"2026-04-24T12:01:00+08:00",
);
state.projects.push(project);
project.name = "你现在接手的项目根目录是 /Users/kris/code/yuandi。";
project.threadMeta.threadDisplayName = "你现在接手的项目根目录是 /Users/kris/code/yuandi。";
project.threadMeta.folderName = "";
project.threadMeta.codexFolderRef = "yuandi";
const payload = buildProjectMessagesRealtimePayload(state, project.id);
assert.ok(payload, "expected realtime payload");
assert.equal(payload?.project.name, "yuandi");
assert.equal(payload?.project.threadMeta.threadDisplayName, "yuandi");
});
test("folder archive homepage rows do not expose pin toggles in the Web surface", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
{
...buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
pinned: true,
},
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-03-30T12:00:00+08:00",
),
);
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
const actions = getConversationActionAvailability(folder!);
assert.equal(actions.canTogglePin, false);
});
test("homepage rows do not expose pinned labels in the Web surface", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
pinned: true,
});
const pinnedThread = getConversationHomeItems(state).find((item) => item.projectId === "boss-thread-1");
assert.ok(pinnedThread, "expected pinned thread item");
assert.equal(getConversationPinnedBadgeLabel(pinnedThread!), "");
const masterAgent = getConversationHomeItems(state).find((item) => item.projectId === "master-agent");
assert.ok(masterAgent, "expected master agent item");
assert.equal(getConversationPinnedBadgeLabel(masterAgent!), "");
});
test("web conversation projection strips pin metadata before rendering the Mac/Web surface", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
{
...buildImportedThreadProject(
"mac-studio",
"solo-pinned-thread",
"Solo",
"solo",
"单线程置顶验证",
"solo-thread",
"2026-03-30T13:00:00+08:00",
),
pinned: true,
},
{
...buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
pinned: true,
},
buildImportedThreadProject(
"mac-studio",
"boss-thread-2",
"Boss",
"boss",
"发布回滚",
"thread-2",
"2026-03-30T12:00:00+08:00",
),
);
const webItems = getConversationWebItems(state);
const pinnedThread = webItems.find((item) => item.projectId === "solo-pinned-thread");
const folder = webItems.find((item) => item.conversationType === "folder_archive");
const masterAgent = webItems.find((item) => item.projectId === "master-agent");
assert.ok(pinnedThread, "expected pinned thread in web projection");
assert.equal(pinnedThread?.topPinnedLabel, undefined);
assert.equal(pinnedThread?.manualPinned, false);
assert.ok(folder, "expected folder archive in web projection");
assert.equal(folder?.topPinnedLabel, undefined);
assert.equal(folder?.manualPinned, false);
assert.ok(masterAgent, "expected master agent in web projection");
assert.equal(masterAgent?.topPinnedLabel, undefined);
assert.equal(masterAgent?.manualPinned, false);
});
test("folder archive action path encodes folder keys with nested path segments", async () => {
await setup();
assert.equal(
getConversationActionsPath("mac-studio:/Users/kris/code/yuandi"),
"/api/v1/conversations/mac-studio%3A%2FUsers%2Fkris%2Fcode%2Fyuandi/actions",
);
});
test("folder archive search aliases keep full reachability across five threads", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject("mac-studio", "boss-thread-1", "Boss", "boss", "发布回滚", "thread-1", "2026-03-30T14:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-2", "Boss", "boss", "Android UI 收尾", "thread-2", "2026-03-30T13:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-3", "Boss", "boss", "日志收口", "thread-3", "2026-03-30T12:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-4", "Boss", "boss", "网络修复", "thread-4", "2026-03-30T11:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-5", "Boss", "boss", "审阅确认", "thread-5", "2026-03-30T10:00:00+08:00"),
);
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.deepEqual(folder?.searchAliases, [
"发布回滚",
"Android UI 收尾",
"日志收口",
"网络修复",
"审阅确认",
]);
assert.deepEqual(folder?.searchTargetProjectIds, [
"boss-thread-1",
"boss-thread-2",
"boss-thread-3",
"boss-thread-4",
"boss-thread-5",
]);
});
test("folder archive search aliases keep same-label threads reachable through every project id", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject("mac-studio", "boss-thread-1", "Boss", "boss", "发布回滚", "thread-1", "2026-03-30T14:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-2", "Boss", "boss", "发布回滚", "thread-2", "2026-03-30T13:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-3", "Boss", "boss", "日志收口", "thread-3", "2026-03-30T12:00:00+08:00"),
buildImportedThreadProject("mac-studio", "boss-thread-4", "Boss", "boss", "网络修复", "thread-4", "2026-03-30T11:00:00+08:00"),
);
const folder = getConversationHomeItems(state).find((item) => item.conversationType === "folder_archive");
assert.ok(folder, "expected grouped folder archive item");
assert.deepEqual(folder?.searchAliases, ["发布回滚", "发布回滚", "日志收口", "网络修复"]);
assert.deepEqual(folder?.searchTargetProjectIds, ["boss-thread-1", "boss-thread-2", "boss-thread-3", "boss-thread-4"]);
});
test("conversation items expose context status while keeping idle activity silent", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.masterAgentTasks = [];
state.dispatchExecutions = [];
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"boss-thread-1",
"Boss",
"boss",
"归档确认",
"thread-1",
"2026-03-30T11:00:00+08:00",
),
);
state.threadContextSnapshots = [
{
snapshotId: "snapshot-1",
workerId: "mac-studio",
projectId: "boss-thread-1",
threadId: "thread-1",
title: "归档确认",
summary: "上下文预算进入紧张区,需要尽快收尾。",
contextBudgetRemainingPct: 34,
contextBudgetLevel: "urgent",
mustFinishBeforeCompaction: false,
estimatedRemainingTurns: 8,
estimatedRemainingLargeMessages: 3,
compactionCount: 0,
patchPending: false,
testsPending: false,
evidencePending: false,
checklist: [],
capturedAt: "2026-03-30T11:00:00+08:00",
},
];
const [masterAgent, threadConversation] = getConversationHomeItems(state);
assert.equal(threadConversation.projectId, "boss-thread-1");
assert.equal(threadConversation.contextBudgetIndicator.visible, true);
assert.equal(threadConversation.contextBudgetIndicator.percent, 34);
assert.equal(threadConversation.contextBudgetIndicator.level, "urgent");
assert.equal(threadConversation.activityIconCount, 0);
assert.equal(masterAgent.activityIconCount, 0);
assert.equal(masterAgent.contextBudgetIndicator.visible, false);
assert.equal(masterAgent.contextBudgetIndicator.percent, undefined);
assert.equal(masterAgent.contextBudgetIndicator.level, undefined);
});
test("conversation items hide context ring when no thread snapshot exists", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
buildImportedThreadProject(
"mac-studio",
"single-thread-no-context",
"Talking",
"talking",
"调试回归",
"thread-no-context",
"2026-03-30T11:20:00+08:00",
),
);
state.threadContextSnapshots = [];
const items = getConversationHomeItems(state);
const directThread = items.find((item) => item.projectId === "single-thread-no-context");
assert.ok(directThread);
assert.equal(directThread?.contextBudgetIndicator.visible, false);
assert.equal(directThread?.contextBudgetIndicator.percent, undefined);
assert.equal(directThread?.contextBudgetIndicator.level, undefined);
});
test("conversation items keep latest reply ordering anchored to actual message time", async () => {
await setup();
const state = await readState();
const staleProject = buildImportedThreadProject(
"mac-studio",
"stale-thread",
"Talking",
"talking",
"树莓派二代查询",
"thread-stale",
"2026-04-04T06:12:00+08:00",
);
const freshProject = buildImportedThreadProject(
"mac-studio",
"fresh-thread",
"Fresh",
"fresh",
"最近真回复",
"thread-fresh",
"2026-04-04T12:10:00+08:00",
);
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push(
freshProject,
{
...staleProject,
threadMeta: {
...staleProject.threadMeta,
lastObservedCodexActivityAt: "2026-04-04T11:48:00+08:00",
},
projectUnderstanding: {
projectGoal: "保持会话列表稳定",
currentProgress: "后台只更新状态文档",
technicalArchitecture: "Boss 会话页",
currentBlockers: "",
recommendedNextStep: "不要让非消息事件抬升排序",
updatedAt: "2026-04-04T12:08:00+08:00",
},
},
);
const items = getConversationHomeItems(state);
const thread = items.find((item) => item.projectId === "stale-thread");
const freshThreadIndex = items.findIndex((item) => item.projectId === "fresh-thread");
const staleThreadIndex = items.findIndex((item) => item.projectId === "stale-thread");
assert.ok(thread);
assert.ok(freshThreadIndex >= 0);
assert.ok(staleThreadIndex >= 0);
assert.ok(freshThreadIndex < staleThreadIndex, "后台活动不应把旧会话抬到真实新消息前面");
assert.equal(thread?.latestReplyAt, "2026-04-04T06:12:00+08:00");
assert.equal(thread?.latestReplyLabel, formatTimestampLabel("2026-04-04T06:12:00+08:00"));
});
test("conversation items mark stale context-backed timestamps as waiting for sync", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"stale-context-project",
"Boss",
"boss",
"上下文老化线程",
"thread-stale",
"2026-03-25T10:58:00+08:00",
),
preview: "老上下文还在挂着",
unreadCount: 1,
});
state.threadContextSnapshots.push(
buildThreadContextSnapshot(
"stale-context-project",
"thread-stale",
"上下文老化线程",
"watch",
false,
58,
"2026-03-25T10:58:00+08:00",
"mac-studio",
),
);
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "stale-context-project");
assert.ok(item, "expected stale context conversation");
assert.equal(item?.contextBudgetIndicator.visible, true);
assert.equal(item?.latestReplyLabel, "待同步");
});
test("default seeded conversations no longer expose Boss 移动控制台", async () => {
await setup();
const state = await readState();
const items = getConversationHomeItems(state);
assert.ok(items.some((item) => item.projectId === "master-agent"), "expected master-agent to remain available");
assert.ok(items.some((item) => item.projectId === "audit-collab"), "expected audit collaboration to remain available");
assert.equal(
items.some((item) => item.projectId === "boss-console" || item.threadTitle === "Boss 移动控制台"),
false,
);
});
test("readState migrates away persisted legacy boss-console conversations", async () => {
await setup();
const state = await readState();
const legacyState = structuredClone(state) as typeof state;
legacyState.projects.push({
id: "boss-console",
name: "Boss 移动控制台",
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: "历史遗留的 boss-console 会话。",
updatedAt: "2026-04-04T12:20:00+08:00",
lastMessageAt: "2026-04-04T12:20:00+08:00",
isGroup: false,
threadMeta: {
projectId: "boss-console",
threadId: "thread-boss-console",
threadDisplayName: "Boss 移动控制台",
folderName: "Boss",
activityIconCount: 1,
updatedAt: "2026-04-04T12:20:00+08:00",
codexThreadRef: "thread-boss-console",
codexFolderRef: "boss-console",
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 0,
riskLevel: "low",
messages: [],
goals: [],
versions: [],
});
legacyState.threadContextSnapshots.push({
snapshotId: "legacy-boss-console-snapshot",
projectId: "boss-console",
taskId: "legacy-task",
threadId: "thread-boss-console",
title: "Boss 移动控制台线程",
summary: "遗留摘要",
nodeId: "mac-studio",
workerId: "worker-legacy",
sourceKind: "worker_estimator",
status: "running",
contextBudgetRemainingPct: 48,
contextBudgetLevel: "watch",
mustFinishBeforeCompaction: false,
estimatedRemainingTurns: 5,
estimatedRemainingLargeMessages: 2,
compactionCount: 0,
patchPending: false,
testsPending: false,
evidencePending: false,
checklist: [],
capturedAt: "2026-04-04T12:20:00+08:00",
});
const stateFile = process.env.BOSS_STATE_FILE;
assert.ok(stateFile, "expected test state file path");
await writeFile(stateFile, JSON.stringify(legacyState, null, 2), "utf8");
const migratedState = await readState();
assert.equal(migratedState.projects.some((item) => item.id === "boss-console"), false);
assert.equal(migratedState.threadContextSnapshots.some((item) => item.projectId === "boss-console"), false);
});