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 getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"]; let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"]; let formatTimestampLabel: (typeof import("../src/lib/boss-projections"))["formatTimestampLabel"]; 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] = await Promise.all([ import("../src/lib/boss-data.ts"), import("../src/lib/boss-projections.ts"), ]); readState = data.readState; getConversationHomeItems = projections.getConversationHomeItems; getConversationFolderView = projections.getConversationFolderView; formatTimestampLabel = projections.formatTimestampLabel; } 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: [], }; } 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("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, true); assert.equal(masterAgent.contextBudgetIndicator.percent, 100); assert.equal(masterAgent.contextBudgetIndicator.level, "safe"); }); test("conversation items keep a safe context ring even 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, true); assert.equal(directThread?.contextBudgetIndicator.percent, 100); assert.equal(directThread?.contextBudgetIndicator.level, "safe"); }); test("conversation items prefer latest observed codex activity over stale last message time", async () => { await setup(); const state = await readState(); const baseProject = buildImportedThreadProject( "mac-studio", "stale-thread", "Talking", "talking", "树莓派二代查询", "thread-stale", "2026-04-04T06:12:00+08:00", ); state.projects = state.projects.filter((project) => project.id === "master-agent"); state.projects.push( { ...baseProject, threadMeta: { ...baseProject.threadMeta, lastObservedCodexActivityAt: "2026-04-04T11:48:00+08:00", }, }, ); const items = getConversationHomeItems(state); const thread = items.find((item) => item.projectId === "stale-thread"); assert.ok(thread); assert.equal(thread?.latestReplyAt, "2026-04-04T11:48:00+08:00"); assert.equal(thread?.latestReplyLabel, formatTimestampLabel("2026-04-04T11:48:00+08:00")); }); 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.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); });