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

202 lines
6.4 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdtemp, rm } from "node:fs/promises";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"];
let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"];
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;
}
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");
});