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> | 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); });