Files
boss/tests/device-heartbeat-codex-message-sync.test.ts

523 lines
17 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 upsertDeviceHeartbeat: (typeof import("../src/lib/boss-data"))["upsertDeviceHeartbeat"];
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-heartbeat-message-sync-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const data = await import("../src/lib/boss-data.ts");
readState = data.readState;
upsertDeviceHeartbeat = data.upsertDeviceHeartbeat;
writeState = data.writeState;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("device heartbeat mirrors recent codex desktop replies into the matching thread conversation once", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-sync",
token: "device-message-sync-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-main",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-main",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-main",
);
assert.ok(importedProject, "expected heartbeat auto-import to create the thread conversation");
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-main:2026-04-20T10:02:10.000Z:reply-1",
body: "桌面 Codex 已经把会话实时同步链路修好了。",
sentAt: "2026-04-20T10:02:10.000Z",
},
],
},
],
});
let nextState = await readState();
let nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
const mirroredMessage = nextProject?.messages.find(
(message) => message.externalMessageId === "codex-thread:thread-boss-main:2026-04-20T10:02:10.000Z:reply-1",
);
assert.ok(mirroredMessage);
assert.equal(mirroredMessage?.sender, "device");
assert.equal(mirroredMessage?.senderLabel, "Boss开发主线程");
assert.equal(mirroredMessage?.body, "桌面 Codex 已经把会话实时同步链路修好了。");
assert.equal(nextProject?.lastMessageAt, "2026-04-20T10:02:10.000Z");
assert.equal(nextProject?.preview, "桌面 Codex 已经把会话实时同步链路修好了。");
assert.equal(nextProject?.unreadCount, 1);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-main:2026-04-20T10:02:10.000Z:reply-1",
body: "桌面 Codex 已经把会话实时同步链路修好了。",
sentAt: "2026-04-20T10:02:10.000Z",
},
],
},
],
});
nextState = await readState();
nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
const mirroredCopies = nextProject?.messages.filter(
(message) => message.externalMessageId === "codex-thread:thread-boss-main:2026-04-20T10:02:10.000Z:reply-1",
);
assert.equal(mirroredCopies?.length, 1);
assert.equal(nextProject?.unreadCount, 1);
});
test("device heartbeat does not duplicate a reply already written by task completion", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-completion-dedupe",
token: "device-message-completion-dedupe-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-completion-dedupe",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-completion-dedupe",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-completion-dedupe",
);
assert.ok(importedProject);
const directReplyBody = "BOSS回归APP消息已收到。";
const targetProject = initialState.projects.find((project) => project.id === importedProject?.id);
assert.ok(targetProject);
targetProject!.messages = [
{
id: "msg-direct-completion",
sender: "device",
senderLabel: "Boss开发主线程",
body: directReplyBody,
sentAt: "2026-04-20T10:02:10.000Z",
kind: "text",
},
];
targetProject!.preview = directReplyBody;
targetProject!.lastMessageAt = "2026-04-20T10:02:10.000Z";
targetProject!.unreadCount = 1;
await writeState(initialState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-completion-dedupe:2026-04-20T10:02:10.001Z:reply-1",
body: directReplyBody,
sentAt: "2026-04-20T10:02:10.001Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
const matchingReplies = nextProject?.messages.filter((message) => message.body === directReplyBody);
assert.equal(matchingReplies?.length, 1);
assert.equal(matchingReplies?.[0]?.externalMessageId, undefined);
assert.equal(nextProject?.unreadCount, 1);
assert.equal(nextProject?.preview, directReplyBody);
});
test("device heartbeat does not duplicate a takeover reply already written by master agent", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-master-dedupe",
token: "device-message-master-dedupe-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-master-dedupe",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-master-dedupe",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-master-dedupe",
);
assert.ok(importedProject);
const replyBody = "主Agent托管链路已收到。";
const targetProject = initialState.projects.find((project) => project.id === importedProject?.id);
assert.ok(targetProject);
targetProject!.messages = [
{
id: "msg-master-completion",
sender: "master",
senderLabel: "主 Agent",
body: replyBody,
sentAt: "2026-04-20T10:02:10.000Z",
kind: "text",
},
];
targetProject!.preview = replyBody;
targetProject!.lastMessageAt = "2026-04-20T10:02:10.000Z";
targetProject!.unreadCount = 1;
await writeState(initialState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-master-dedupe:2026-04-20T10:02:10.001Z:reply-1",
body: replyBody,
sentAt: "2026-04-20T10:02:10.001Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
const matchingReplies = nextProject?.messages.filter((message) => message.body === replyBody);
assert.equal(matchingReplies?.length, 1);
assert.equal(matchingReplies?.[0]?.sender, "master");
assert.equal(matchingReplies?.[0]?.externalMessageId, undefined);
assert.equal(nextProject?.unreadCount, 1);
assert.equal(nextProject?.preview, replyBody);
});
test("device heartbeat does not count commentary replies as unread and keeps only the final result unread", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-phase",
token: "device-message-phase-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-phase",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-phase",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-phase:2026-04-20T10:03:00.000Z:commentary-1",
body: "我先检查聊天折叠链路,确认过程消息不会直接展开。",
sentAt: "2026-04-20T10:03:00.000Z",
phase: "commentary",
},
{
messageId: "codex-thread:thread-boss-phase:2026-04-20T10:05:00.000Z:final-1",
body: "这轮已经完成折叠修复,未读现在只会算最终结果。",
sentAt: "2026-04-20T10:05:00.000Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-phase",
);
const processMessage = nextProject?.messages.find(
(message) =>
message.externalMessageId === "codex-thread:thread-boss-phase:2026-04-20T10:03:00.000Z:commentary-1",
);
const finalMessage = nextProject?.messages.find(
(message) =>
message.externalMessageId === "codex-thread:thread-boss-phase:2026-04-20T10:05:00.000Z:final-1",
);
assert.ok(nextProject);
assert.equal(processMessage?.kind, "thread_process");
assert.equal(finalMessage?.kind, "text");
assert.equal(nextProject?.preview, "这轮已经完成折叠修复,未读现在只会算最终结果。");
assert.equal(nextProject?.unreadCount, 1);
});
test("device heartbeat does not replay old desktop replies after conversation history is cleared", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-reset",
token: "device-message-reset-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-reset",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-reset",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-reset",
);
assert.ok(importedProject);
initialState.conversationHistoryClearedAt = "2026-04-20T10:10:00.000Z";
const targetProject = initialState.projects.find((project) => project.id === importedProject?.id);
assert.ok(targetProject);
targetProject!.messages = [];
targetProject!.preview = "";
targetProject!.unreadCount = 0;
await writeState(initialState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
lastActiveAt: "2026-04-20T10:12:00.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-reset:2026-04-20T10:05:00.000Z:old-final",
body: "这条旧回复不应该在清空历史后被重新导回。",
sentAt: "2026-04-20T10:05:00.000Z",
phase: "final_answer",
},
{
messageId: "codex-thread:thread-boss-reset:2026-04-20T10:11:00.000Z:new-final",
body: "这条新回复应该继续同步回来。",
sentAt: "2026-04-20T10:11:00.000Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
assert.ok(nextProject);
assert.equal(
nextProject?.messages.some(
(message) =>
message.externalMessageId === "codex-thread:thread-boss-reset:2026-04-20T10:05:00.000Z:old-final",
),
false,
);
assert.equal(
nextProject?.messages.some(
(message) =>
message.externalMessageId === "codex-thread:thread-boss-reset:2026-04-20T10:11:00.000Z:new-final",
),
true,
);
assert.equal(nextProject?.preview, "这条新回复应该继续同步回来。");
assert.equal(nextProject?.unreadCount, 1);
});
test("device heartbeat legacy process text is normalized to thread_process and does not become preview", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-legacy-process",
token: "device-message-legacy-process-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "boss",
folderRef: "/Users/kris/code/boss",
threadId: "thread-boss-legacy-process",
threadDisplayName: "Boss开发主线程",
codexFolderRef: "/Users/kris/code/boss",
codexThreadRef: "thread-boss-legacy-process",
lastActiveAt: "2026-04-20T10:00:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const resetState = await readState();
resetState.conversationHistoryClearedAt = undefined;
await writeState(resetState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-legacy-process:2026-04-20T10:03:00.000Z:commentary-legacy",
body: "我继续把这条链路又往下收了一层,补的是“历史脏消息”的兼容,不只是新消息规则。",
sentAt: "2026-04-20T10:03:00.000Z",
phase: "commentary",
},
{
messageId: "codex-thread:thread-boss-legacy-process:2026-04-20T10:05:00.000Z:final-1",
body: "这轮已经完成折叠修复,未读现在只会算最终结果。",
sentAt: "2026-04-20T10:05:00.000Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-legacy-process",
);
const legacyProcessMessage = nextProject?.messages.find(
(message) =>
message.externalMessageId ===
"codex-thread:thread-boss-legacy-process:2026-04-20T10:03:00.000Z:commentary-legacy",
);
assert.ok(nextProject);
assert.equal(legacyProcessMessage?.kind, "thread_process");
assert.equal(nextProject?.preview, "这轮已经完成折叠修复,未读现在只会算最终结果。");
assert.equal(nextProject?.unreadCount, 1);
});