feat: tighten conversation folder archive projections
This commit is contained in:
@@ -65,6 +65,157 @@ function buildImportedThreadProject(deviceId: string, id: string, folderName: st
|
||||
};
|
||||
}
|
||||
|
||||
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 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");
|
||||
});
|
||||
|
||||
test("conversation home upgrades and downgrades a folder archive as thread count crosses 1 and 2", 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",
|
||||
),
|
||||
);
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
test("conversation home groups multiple imported threads by folder while keeping single-thread projects direct", async () => {
|
||||
await setup();
|
||||
const state = await readState();
|
||||
|
||||
Reference in New Issue
Block a user