chore: checkpoint Boss app v2.5.11

This commit is contained in:
AI Bot
2026-06-08 12:22:50 +08:00
parent bddbe8b5ba
commit 3b51641d99
78 changed files with 5706 additions and 954 deletions

View File

@@ -398,6 +398,12 @@ test("backoffice bff exposes yudao style management contract without secrets", a
const staleTask = payload.insights.taskRiskSummary.rows.find((row: { taskId: string }) => row.taskId === "task-stale");
assert.equal(staleTask?.stale, true);
assert.equal(staleTask?.phase, "awaiting_reply");
assert.equal(Array.isArray(payload.insights.taskSlaPanel.rows), true);
assert.equal(payload.insights.taskSlaPanel.summary.breached >= 1, true);
const breachedTask = payload.insights.taskSlaPanel.rows.find((row: { taskId: string }) => row.taskId === "task-stale");
assert.equal(breachedTask?.slaLevel, "breached");
assert.equal(breachedTask?.riskId, "master-task:task-stale");
assert.equal(typeof breachedTask?.recommendedAction, "string");
assert.equal(payload.yudaoMapping.tenant, "adminCompanies");
assert.equal(payload.yudaoMapping.user, "authAccounts");
assert.equal(payload.yudaoMapping.role, "BOSS_PERMISSION_TEMPLATES");

View File

@@ -39,6 +39,7 @@ test("Caddy serves the platform admin subdomain", async () => {
const source = await readSource(caddyfilePath);
assert.match(source, /admin\.boss\.hyzq\.net/);
assert.match(source, /handle \/admin-web\/\* \{\s*root \* \/opt\/boss\/public\s*file_server\s*\}/s);
assert.match(source, /@adminRoot path \//);
assert.match(source, /rewrite \* \/admin-web\/index\.html/);
assert.doesNotMatch(source, /redir \/ \/enterprise-admin/);

View File

@@ -208,3 +208,97 @@ test("risk scan creates operational faults for computer use and boss-agent OTA f
const secondPayload = await second.json();
assert.equal(secondPayload.createdFaults.length, 0);
});
test("risk scan creates SLA notifications for stuck master agent tasks", async () => {
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
masterAgentTasks: [
{
taskId: "task-stuck",
projectId: "project-a",
taskType: "conversation_reply",
requestMessageId: "msg-stuck",
requestText: "让线程继续执行",
executionPrompt: "继续执行并回写结果",
requestedBy: "客户负责人",
requestedByAccount: "customer@example.com",
deviceId: "mac-a",
status: "running",
phase: "awaiting_reply",
requestedAt: "2026-04-27T13:00:00+08:00",
claimedAt: "2026-04-27T13:01:00+08:00",
lastProgressAt: "2026-04-27T13:01:00+08:00",
leaseExpiresAt: "2026-04-27T13:16:00+08:00",
attemptCount: 1,
maxAttempts: 2,
},
],
});
const response = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(
payload.created.some((notification: { riskId: string }) => notification.riskId === "master-task:task-stuck"),
true,
);
assert.equal(
payload.notifications.some((notification: { title: string }) => notification.title.includes("任务 SLA")),
true,
);
});
test("risk scan automatically requeues safely recoverable master agent tasks", async () => {
const state = await data.readState();
await data.writeState({
...state,
adminNotifications: [],
permissionAuditLogs: [],
adminRiskTimeline: [],
masterAgentTasks: [
{
taskId: "task-recoverable",
projectId: "project-a",
taskType: "conversation_reply",
requestMessageId: "msg-recoverable",
requestText: "继续处理",
executionPrompt: "继续处理并回写结果",
requestedBy: "客户负责人",
requestedByAccount: "customer@example.com",
deviceId: "mac-a",
status: "running",
phase: "recoverable_failed",
requestedAt: "2026-04-27T13:00:00+08:00",
claimedAt: "2026-04-27T13:01:00+08:00",
lastProgressAt: "2026-04-27T13:02:00+08:00",
leaseExpiresAt: "2026-04-27T13:16:00+08:00",
attemptCount: 1,
maxAttempts: 2,
recoverable: true,
nextRetryAt: "2026-04-27T13:03:00+08:00",
lastErrorCode: "RECOVERABLE_RUNTIME_FAILURE",
errorMessage: "CODEX_APP_SERVER_TIMEOUT",
},
],
});
const response = await postScan(await adminRequest("http://127.0.0.1:3000/api/v1/admin/risks/scan", {
method: "POST",
}));
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.autoRecovered.length, 1);
assert.equal(payload.autoRecovered[0].taskId, "task-recoverable");
const nextState = await data.readState();
const task = nextState.masterAgentTasks.find((item) => item.taskId === "task-recoverable");
assert.equal(task?.status, "queued");
assert.equal(task?.phase, "queued");
assert.equal(task?.recoverable, false);
assert.equal(nextState.permissionAuditLogs.some((log) => log.action === "master_agent.task_retried"), true);
assert.equal(nextState.adminRiskTimeline.some((event) => event.action === "task.auto_recovery_requeued"), true);
});

View File

@@ -16,7 +16,7 @@ test("BossApiClient exposes a lightweight project messages endpoint", async () =
);
assert.match(
source,
/return requestWithRestore\("GET", "\/api\/v1\/projects\/" \+ encode\(projectId\) \+ "\/messages", null\);/,
/return requestWithRestoreRaw\(\s*"GET",\s*"\/api\/v1\/projects\/" \+ encode\(projectId\) \+ "\/messages",\s*null,\s*DEFAULT_CONNECT_TIMEOUT_MS,\s*CONVERSATIONS_READ_TIMEOUT_MS\s*\);/s,
"expected lightweight message refreshes to reuse the dedicated messages route",
);
});
@@ -36,7 +36,7 @@ test("ProjectDetailActivity reserves full realtime reloads for non-message event
);
assert.match(
source,
/void triggerRealtimeReload\(boolean requireFullSnapshot\) \{\s*if \(requireFullSnapshot\) \{\s*reload\(\);\s*return;\s*\}\s*reloadMessagesOnly\(\);\s*\}/s,
/void triggerRealtimeReload\(boolean requireFullSnapshot\) \{\s*if \(requireFullSnapshot\) \{\s*reloadInBackground\(false\);\s*return;\s*\}\s*reloadMessagesOnly\(\);\s*\}/s,
"expected debounced realtime reloads to choose between full and lightweight refresh paths",
);
});

View File

@@ -6,13 +6,23 @@ async function readSource(path: string) {
return readFile(new URL(path, import.meta.url), "utf8");
}
test("events route enriches message events with a lightweight project chat payload", async () => {
test("events route supports message patch v1 while retaining snapshot fallback", async () => {
const source = await readSource("../src/app/api/v1/events/route.ts");
assert.match(
source,
/projectMessagesPayload:\s*buildProjectMessagesRealtimePayload\(state,\s*String\(payload\.projectId \?\? ""\)\)/,
"expected realtime event route to include a lightweight project chat payload for message events",
/x-boss-realtime-capabilities/,
"expected realtime event route to inspect native app capability headers",
);
assert.match(
source,
/projectMessagesPatch/,
"expected realtime event route to emit a message patch for capable clients",
);
assert.match(
source,
/projectMessagesPayload:\s*buildProjectMessagesRealtimePayload/,
"expected realtime event route to retain full snapshot fallback for older clients",
);
});
@@ -29,6 +39,16 @@ test("ProjectDetailActivity applies lightweight realtime chat payloads before sc
/JSONObject projectMessagesPayload = event\.payload\.optJSONObject\("projectMessagesPayload"\);/,
"expected chat page to read the lightweight message payload from realtime events",
);
assert.match(
source,
/JSONObject projectMessagesPatch = event\.payload\.optJSONObject\("projectMessagesPatch"\);/,
"expected chat page to read message-patch-v1 realtime payloads first",
);
assert.match(
source,
/scheduleRealtimeReload\(false\);/,
"expected chat page to fall back to a debounced message reload when a patch has a gap",
);
assert.match(
source,
/renderLoadedProjectSnapshot\(new ProjectSnapshot\(projectMessagesPayload,\s*null,\s*null\)\);/,

View File

@@ -142,6 +142,30 @@ test("highest admin can inspect and revoke all active sessions", async () => {
assert.equal(await data.getAuthSession(worker.sessionToken), null);
});
test("getAuthSession validates a session without touching lastSeenAt", async () => {
const session = await data.createAuthSession({
account: "krisolo",
role: "highest_admin",
displayName: "Boss",
loginMethod: "password",
});
const stableLastSeenAt = "2026-04-26T12:00:00+08:00";
const state = await data.readState();
const storedSession = state.authSessions.find((item) => item.sessionId === session.sessionId);
assert.ok(storedSession);
storedSession.lastSeenAt = stableLastSeenAt;
await data.writeState(state);
const resolvedSession = await data.getAuthSession(session.sessionToken);
assert.equal(resolvedSession?.lastSeenAt, stableLastSeenAt);
const after = await data.readState();
assert.equal(
after.authSessions.find((item) => item.sessionId === session.sessionId)?.lastSeenAt,
stableLastSeenAt,
);
});
test("primary admin session uses the current production admin account", async () => {
const session = await data.createPrimaryAdminSession();
assert.equal(session.account, "krisolo");

View File

@@ -134,6 +134,52 @@ test("independent Boss admin web app exposes management actions instead of read
assert.match(appSource, /handleCodexRemoteControl/);
});
test("independent Boss admin web app exposes skill management dispatch workspace", async () => {
const [appSource, apiSource] = await Promise.all([
readSource("../apps/boss-admin-web/src/App.vue"),
readSource("../apps/boss-admin-web/src/api/bossAdmin.ts"),
]);
assert.match(apiSource, /fetchSkillLifecycleRequests/);
assert.match(apiSource, /\/api\/v1\/admin\/skills\/requests/);
assert.match(apiSource, /method:\s*["']GET["']/);
for (const label of [
"Skill 管理分发",
"快捷下发",
"Skill 请求队列",
"待执行",
"执行中",
"最近请求",
"安装远端 Skill",
"更新下发",
"回滚",
"版本锁定",
]) {
assert.match(appSource, new RegExp(label));
}
assert.match(appSource, /skillLifecycleRequests/);
assert.match(appSource, /loadSkillLifecycleRequests/);
assert.match(appSource, /quickSkillRequest/);
});
test("independent Boss admin web app keeps backup tables inside their cards", async () => {
const [appSource, cssSource] = await Promise.all([
readSource("../apps/boss-admin-web/src/App.vue"),
readSource("../apps/boss-admin-web/src/styles.css"),
]);
assert.match(appSource, /boss-admin-wide-card/);
assert.match(cssSource, /\.ant-card\s*\{/);
assert.match(cssSource, /\.boss-admin-wide-card/);
assert.match(cssSource, /grid-column:\s*1\s*\/\s*-1/);
assert.match(cssSource, /min-width:\s*0/);
assert.match(cssSource, /\.ant-table-wrapper\s*\{/);
assert.match(cssSource, /overflow-x:\s*auto/);
assert.match(cssSource, /word-break:\s*break-word/);
assert.match(cssSource, /white-space:\s*normal/);
});
test("root Next project isolates the independent Vue admin workspace", async () => {
const [tsconfigSource, eslintSource, rootPkgSource] = await Promise.all([
readSource("../tsconfig.json"),

View File

@@ -26,6 +26,21 @@ test("events route enriches project conversation events with a visible home item
);
});
test("events route coalesces enriched payload building across realtime clients", async () => {
const source = await readSource("../src/app/api/v1/events/route.ts");
assert.match(
source,
/getSharedEventPayload/,
"expected realtime event route to share one enriched payload build across concurrent SSE clients",
);
assert.match(
source,
/sharedEventPayloads/,
"expected realtime event route to keep a short-lived shared payload cache",
);
});
test("MainActivity applies realtime conversation patches without forcing a network refresh", async () => {
const [mainActivity, mapper] = await Promise.all([
readSource("../android/app/src/main/java/com/hyzq/boss/MainActivity.java"),

View File

@@ -180,7 +180,7 @@ test("allow_always applies only to the active folder and does not unlock other f
assert.equal(blocked.policy.allowPolicy, "forbid");
});
test("claimNextMasterAgentTask keeps conversation replies queued when the device prefers gui mode", async () => {
test("claimNextMasterAgentTask lets conversation replies run through gui mode when a gui execution channel is available", async () => {
await setup();
const project = await getCliProject();
@@ -205,6 +205,49 @@ test("claimNextMasterAgentTask keeps conversation replies queued when the device
const claimed = await claimNextMasterAgentTask("mac-studio");
assert.equal(claimed?.taskId, task.taskId);
const state = await readState();
const running = state.masterAgentTasks.find((item) => item.taskId === task.taskId);
assert.equal(running?.status, "running");
});
test("claimNextMasterAgentTask keeps conversation replies queued when preferred gui mode has no gui channel", async () => {
await setup();
const project = await getCliProject();
await updateDevice("mac-studio", {
preferredExecutionMode: "gui",
capabilities: {
gui: {
connected: false,
lastSeenAt: "2026-04-06T10:00:00.000Z",
lastActiveProjectId: "",
},
codexAppServer: {
connected: false,
lastSeenAt: "2026-04-06T10:00:00.000Z",
lastActiveProjectId: "",
},
},
});
const task = await queueMasterAgentTask({
projectId: project.id,
requestMessageId: "msg-preferred-gui-no-channel",
requestText: "继续推进当前线程任务",
executionPrompt: "请继续推进当前线程任务",
requestedBy: "Boss 超级管理员",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
taskType: "conversation_reply",
targetProjectId: project.id,
targetThreadId: project.threadMeta.threadId,
targetThreadDisplayName: project.threadMeta.threadDisplayName,
targetCodexThreadRef: project.threadMeta.codexThreadRef,
targetCodexFolderRef: project.threadMeta.codexFolderRef,
});
const claimed = await claimNextMasterAgentTask("mac-studio");
assert.equal(claimed, null);
const state = await readState();
const queued = state.masterAgentTasks.find((item) => item.taskId === task.taskId);
@@ -389,6 +432,7 @@ test("claimNextMasterAgentTask reclaims stale running conversation replies for t
const runningTask = state.masterAgentTasks.find((item) => item.taskId === task.taskId);
assert.equal(runningTask?.status, "running");
runningTask!.claimedAt = "2026-04-01T00:00:00.000Z";
runningTask!.lastProgressAt = "2026-04-01T00:00:00.000Z";
await writeState(state);
const reclaimed = await claimNextMasterAgentTask("mac-studio");

View File

@@ -28,7 +28,7 @@ test.after(async () => {
}
});
test("device heartbeat mirrors recent codex desktop replies into the matching thread conversation once", async () => {
test("device heartbeat records recent codex desktop activity without mirroring reply text", async () => {
await setup();
const seedHeartbeat = {
@@ -70,6 +70,7 @@ test("device heartbeat mirrors recent codex desktop replies into the matching th
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
lastActiveAt: "2026-04-20T10:02:10.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-main:2026-04-20T10:02:10.000Z:reply-1",
@@ -87,13 +88,10 @@ test("device heartbeat mirrors recent codex desktop replies into the matching th
(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);
assert.equal(mirroredMessage, undefined);
assert.equal(nextProject?.messages.some((message) => message.externalMessageId), false);
assert.equal(nextProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-20T10:02:10.000Z");
assert.equal(nextProject?.unreadCount, 0);
await upsertDeviceHeartbeat({
...seedHeartbeat,
@@ -116,8 +114,86 @@ test("device heartbeat mirrors recent codex desktop replies into the matching th
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);
assert.equal(mirroredCopies?.length, 0);
assert.equal(nextProject?.unreadCount, 0);
});
test("device heartbeat records codex activity without appending uncorrelated desktop replies", async () => {
await setup();
const seedHeartbeat = {
deviceId: "device-message-activity-only",
token: "device-message-activity-only-token",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
status: "online" as const,
quota5h: 76,
quota7d: 85,
projects: [],
endpoint: "mac://kris.local",
projectCandidates: [
{
folderName: "juyuwan",
folderRef: "/Users/kris/Documents/juyuwan",
threadId: "thread-juyuwan-activity-only",
threadDisplayName: "juyuwan",
codexFolderRef: "/Users/kris/Documents/juyuwan",
codexThreadRef: "thread-juyuwan-activity-only",
lastActiveAt: "2026-06-07T16:30:00.000Z",
suggestedImport: true,
},
],
};
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-juyuwan-activity-only",
);
assert.ok(importedProject, "expected heartbeat auto-import to create the thread conversation");
importedProject!.messages = [];
importedProject!.preview = "";
importedProject!.unreadCount = 0;
await writeState(initialState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
lastActiveAt: "2026-06-07T16:34:15.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-juyuwan-activity-only:2026-06-07T16:34:15.000Z:final-1",
body: "已完成下一步并实机验证了。APK 已安装到 K30 Ultra。",
sentAt: "2026-06-07T16:34:15.000Z",
phase: "final_answer",
},
],
},
],
});
const nextState = await readState();
const nextProject = nextState.projects.find((project) => project.id === importedProject?.id);
const mirroredMessage = nextProject?.messages.find(
(message) =>
message.externalMessageId ===
"codex-thread:thread-juyuwan-activity-only:2026-06-07T16:34:15.000Z:final-1",
);
const progressEvent = nextState.threadProgressEvents.find(
(event) => event.projectId === importedProject?.id && event.createdAt === "2026-06-07T16:34:15.000Z",
);
assert.equal(mirroredMessage, undefined);
assert.equal(nextProject?.messages.length, 0);
assert.equal(nextProject?.preview, "");
assert.equal(nextProject?.unreadCount, 0);
assert.equal(nextProject?.threadMeta.lastObservedCodexActivityAt, "2026-06-07T16:34:15.000Z");
assert.ok(progressEvent, "expected heartbeat activity to remain visible as a thread progress event");
});
test("device heartbeat does not duplicate a reply already written by task completion", async () => {
@@ -283,7 +359,7 @@ test("device heartbeat does not duplicate a takeover reply already written by ma
assert.equal(nextProject?.preview, replyBody);
});
test("device heartbeat does not count commentary replies as unread and keeps only the final result unread", async () => {
test("device heartbeat ignores commentary and final assistant text as chat messages", async () => {
await setup();
const seedHeartbeat = {
@@ -314,11 +390,22 @@ test("device heartbeat does not count commentary replies as unread and keeps onl
await upsertDeviceHeartbeat(seedHeartbeat);
await upsertDeviceHeartbeat(seedHeartbeat);
const initialState = await readState();
const importedProject = initialState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-phase",
);
assert.ok(importedProject);
importedProject!.messages = [];
importedProject!.preview = "";
importedProject!.unreadCount = 0;
await writeState(initialState);
await upsertDeviceHeartbeat({
...seedHeartbeat,
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
lastActiveAt: "2026-04-20T10:05:00.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-phase:2026-04-20T10:03:00.000Z:commentary-1",
@@ -351,10 +438,12 @@ test("device heartbeat does not count commentary replies as unread and keeps onl
);
assert.ok(nextProject);
assert.equal(processMessage?.kind, "thread_process");
assert.equal(finalMessage?.kind, "text");
assert.equal(nextProject?.preview, "这轮已经完成折叠修复,未读现在只会算最终结果。");
assert.equal(nextProject?.unreadCount, 1);
assert.equal(processMessage, undefined);
assert.equal(finalMessage, undefined);
assert.equal(nextProject?.messages.length, 0);
assert.equal(nextProject?.preview, "");
assert.equal(nextProject?.unreadCount, 0);
assert.equal(nextProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-20T10:05:00.000Z");
});
test("device heartbeat does not replay old desktop replies after conversation history is cleared", async () => {
@@ -441,13 +530,14 @@ test("device heartbeat does not replay old desktop replies after conversation hi
(message) =>
message.externalMessageId === "codex-thread:thread-boss-reset:2026-04-20T10:11:00.000Z:new-final",
),
true,
false,
);
assert.equal(nextProject?.preview, "这条新回复应该继续同步回来。");
assert.equal(nextProject?.unreadCount, 1);
assert.equal(nextProject?.preview, "");
assert.equal(nextProject?.unreadCount, 0);
assert.equal(nextProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-20T10:12:00.000Z");
});
test("device heartbeat legacy process text is normalized to thread_process and does not become preview", async () => {
test("device heartbeat process text is kept out of the chat transcript", async () => {
await setup();
const seedHeartbeat = {
@@ -480,6 +570,13 @@ test("device heartbeat legacy process text is normalized to thread_process and d
const resetState = await readState();
resetState.conversationHistoryClearedAt = undefined;
const importedProject = resetState.projects.find(
(project) => project.threadMeta.codexThreadRef === "thread-boss-legacy-process",
);
assert.ok(importedProject);
importedProject!.messages = [];
importedProject!.preview = "";
importedProject!.unreadCount = 0;
await writeState(resetState);
await upsertDeviceHeartbeat({
@@ -487,6 +584,7 @@ test("device heartbeat legacy process text is normalized to thread_process and d
projectCandidates: [
{
...seedHeartbeat.projectCandidates[0],
lastActiveAt: "2026-04-20T10:05:00.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-legacy-process:2026-04-20T10:03:00.000Z:commentary-legacy",
@@ -516,7 +614,9 @@ test("device heartbeat legacy process text is normalized to thread_process and d
);
assert.ok(nextProject);
assert.equal(legacyProcessMessage?.kind, "thread_process");
assert.equal(nextProject?.preview, "这轮已经完成折叠修复,未读现在只会算最终结果。");
assert.equal(nextProject?.unreadCount, 1);
assert.equal(legacyProcessMessage, undefined);
assert.equal(nextProject?.messages.length, 0);
assert.equal(nextProject?.preview, "");
assert.equal(nextProject?.unreadCount, 0);
assert.equal(nextProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-20T10:05:00.000Z");
});

View File

@@ -38,6 +38,12 @@ function buildHeartbeatPayload(deviceId: string, projectCandidates: Array<{
codexFolderRef: string;
codexThreadRef: string;
lastActiveAt: string;
recentAssistantMessages?: Array<{
messageId: string;
body: string;
sentAt: string;
phase?: "commentary" | "final_answer";
}>;
}>) {
return {
deviceId,
@@ -84,6 +90,52 @@ test("unchanged device heartbeats do not publish conversation refresh events", a
assert.deepEqual(events.map((event) => event.event), ["devices.updated"]);
});
test("assistant observations refresh conversation metadata without publishing message refresh events", async () => {
await setup();
const deviceId = "noise-device-assistant-observation";
const heartbeat = buildHeartbeatPayload(deviceId, [bossThreadCandidate]);
await upsertDeviceHeartbeat(heartbeat);
await upsertDeviceHeartbeat(heartbeat);
const observedHeartbeat = buildHeartbeatPayload(deviceId, [
{
...bossThreadCandidate,
lastActiveAt: "2026-04-10T10:02:00.000Z",
recentAssistantMessages: [
{
messageId: "codex-thread:thread-boss-main:2026-04-10T10:02:00.000Z:final",
body: "桌面线程的最终回复不应作为手机聊天消息刷新。",
sentAt: "2026-04-10T10:02:00.000Z",
phase: "final_answer",
},
],
},
]);
const firstEvents: Array<{ event: string; payload: { projectId?: string } }> = [];
const unsubscribeFirst = subscribeBossEvents((event, payload) => {
firstEvents.push({ event, payload });
});
await upsertDeviceHeartbeat(observedHeartbeat);
unsubscribeFirst();
assert.equal(firstEvents.some((event) => event.event === "project.messages.updated"), false);
assert.deepEqual(
firstEvents.map((event) => event.event),
["devices.updated", "conversation.updated"],
);
const repeatedEvents: Array<{ event: string; payload: { projectId?: string } }> = [];
const unsubscribeRepeated = subscribeBossEvents((event, payload) => {
repeatedEvents.push({ event, payload });
});
await upsertDeviceHeartbeat(observedHeartbeat);
unsubscribeRepeated();
assert.deepEqual(repeatedEvents.map((event) => event.event), ["devices.updated"]);
});
test("device heartbeats publish one conversation refresh when import candidates change", async () => {
await setup();

View File

@@ -575,6 +575,27 @@ rl.on("line", (line) => {
}
if (message.method === "thread/resume") {
if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ACTIVE_TURN_ON_RESUME === "1") {
send({
id: message.id,
result: {
thread: {
id: message.params?.threadId ?? "thread-fixture",
name: "fixture thread",
turns: [
{
id: "active-turn-from-resume",
status: "inProgress",
startedAt: 1780852200,
completedAt: null,
items: [],
},
],
},
},
});
return;
}
send({
id: message.id,
result: {

View File

@@ -1355,6 +1355,49 @@ test("codex app-server runner steers an active turn when a target turn id is pre
}
});
test("codex app-server runner steers an active resumed turn instead of starting a competing turn", async () => {
const previousActiveTurn = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ACTIVE_TURN_ON_RESUME;
const previousSteer = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ACTIVE_TURN_ON_RESUME = "1";
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-auto-steer-active-turn",
taskType: "conversation_reply",
targetCodexThreadRef: "active-thread-from-resume",
targetCodexFolderRef: repoRoot,
mirrorBossUserMessageToCodexDesktop: true,
executionPrompt: "手机端补充:继续下一步",
});
assert.equal(result.status, "completed");
assert.equal(result.threadId, "active-thread-from-resume");
assert.equal(result.turnId, "active-turn-from-resume");
assert.equal(result.turnControl, "steer");
assert.equal(result.replyBody, "STEERED:手机端补充:继续下一步");
assert.equal(result.threadHistorySync, undefined);
} finally {
if (previousActiveTurn === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ACTIVE_TURN_ON_RESUME;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ACTIVE_TURN_ON_RESUME = previousActiveTurn;
}
if (previousSteer === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER = previousSteer;
}
}
});
test("codex app-server runner interrupts the active turn when the task is canceled while running", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT = "1";

View File

@@ -0,0 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
test("master agent complete route does not block completion response on Telegram delivery", async () => {
const source = await readFile(
new URL("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts", import.meta.url),
"utf8",
);
assert.doesNotMatch(
source,
/await\s+deliverTelegramReplyForCompletedTask/,
"task completion must not wait for Telegram delivery before returning to local-agent",
);
assert.match(
source,
/void\s+deliverTelegramReplyForCompletedTask/,
"expected Telegram delivery to be scheduled in the background after the task is persisted",
);
});

View File

@@ -120,6 +120,79 @@ test("expired task after turn start is timed out instead of duplicated", async (
assert.equal(task?.recoverable, false);
});
test("recoverable codex app-server runtime failure requeues conversation reply", async () => {
await setup();
const state = await data.readState();
state.masterAgentTasks.unshift(
makeQueuedTask("task-runtime-retry", {
projectId: "project-juyuwan",
targetProjectId: "project-juyuwan",
targetThreadId: "thread-juyuwan",
status: "running",
phase: "awaiting_reply",
claimedAt: "2026-06-07T06:05:16.000Z",
lastProgressAt: "2026-06-07T06:10:00.000Z",
leaseExpiresAt: "2026-06-07T06:20:16.000Z",
attemptCount: 1,
maxAttempts: 2,
}),
);
await data.writeState(state);
const completed = await data.completeMasterAgentTask({
taskId: "task-runtime-retry",
deviceId: "mac-studio",
status: "failed",
errorMessage: "CODEX_APP_SERVER_TURN_INTERRUPTED",
});
assert.equal(completed.status, "queued");
assert.equal(completed.phase, "recoverable_failed");
assert.equal(completed.recoverable, true);
assert.equal(completed.errorMessage, "CODEX_APP_SERVER_TURN_INTERRUPTED");
assert.equal(completed.attemptCount, 1);
assert.equal(completed.completedAt, undefined);
assert.equal(completed.claimedAt, undefined);
assert.ok(completed.nextRetryAt);
const claimed = await data.claimNextMasterAgentTask("mac-studio");
assert.equal(claimed?.taskId, "task-runtime-retry");
assert.equal(claimed?.attemptCount, 2);
});
test("recoverable codex app-server runtime failure becomes terminal after max attempts", async () => {
await setup();
const state = await data.readState();
state.masterAgentTasks.unshift(
makeQueuedTask("task-runtime-terminal", {
projectId: "project-juyuwan",
targetProjectId: "project-juyuwan",
targetThreadId: "thread-juyuwan",
status: "running",
phase: "awaiting_reply",
claimedAt: "2026-06-07T06:05:16.000Z",
lastProgressAt: "2026-06-07T06:10:00.000Z",
leaseExpiresAt: "2026-06-07T06:20:16.000Z",
attemptCount: 2,
maxAttempts: 2,
}),
);
await data.writeState(state);
const completed = await data.completeMasterAgentTask({
taskId: "task-runtime-terminal",
deviceId: "mac-studio",
status: "failed",
errorMessage: "CODEX_APP_SERVER_TIMEOUT",
});
assert.equal(completed.status, "failed");
assert.equal(completed.phase, "terminal_failed");
assert.equal(completed.recoverable, false);
assert.equal(completed.errorMessage, "CODEX_APP_SERVER_TIMEOUT");
assert.equal(completed.completedAt !== undefined, true);
});
test("codex app server health distinguishes available, degraded, and unavailable", async () => {
await setup();
assert.equal(data.resolveCodexAppServerHealth(undefined), "unavailable");

View File

@@ -156,3 +156,59 @@ test("local agent infrastructure failures stay out of master agent chat", async
"expected the operational log to remain available outside chat",
);
});
test("读取已有状态时会把历史 Codex App Server 错误码转成人类可读说明", async () => {
await setup();
const state = await readState();
state.projects.push({
id: "project-runtime-error-redaction",
name: "juyuwan",
pinned: false,
systemPinned: false,
deviceIds: ["mac-studio"],
preview: "juyuwan 执行失败CODEX_APP_SERVER_TURN_INTERRUPTED",
updatedAt: "2026-06-07T14:20:00+08:00",
lastMessageAt: "2026-06-07T14:20:00+08:00",
isGroup: false,
threadMeta: {
projectId: "project-runtime-error-redaction",
threadId: "thread-runtime-error-redaction",
threadDisplayName: "juyuwan",
folderName: "juyuwan",
activityIconCount: 0,
updatedAt: "2026-06-07T14:20:00+08:00",
codexThreadRef: "019e9b84-decc-7510-b84f-57c5a27de0e3",
codexFolderRef: "juyuwan",
},
groupMembers: [],
createdByAgent: true,
collaborationMode: "development",
approvalState: "not_required",
unreadCount: 1,
riskLevel: "low",
messages: [
{
id: "msg-runtime-error-redaction",
sender: "ops",
senderLabel: "juyuwan",
body: "juyuwan 执行失败CODEX_APP_SERVER_TURN_INTERRUPTED",
sentAt: "2026-06-07T14:20:00+08:00",
kind: "text",
},
],
goals: [],
versions: [],
});
await writeState(state);
const nextState = await readState();
const project = nextState.projects.find((item) => item.id === "project-runtime-error-redaction");
assert.ok(project, "expected a reloaded project");
const message = project.messages.find((item) => item.id === "msg-runtime-error-redaction");
assert.ok(message, "expected the historical message to remain");
assert.equal(message.body.includes("CODEX_APP_SERVER_TURN_INTERRUPTED"), false);
assert.equal(project.preview.includes("CODEX_APP_SERVER_TURN_INTERRUPTED"), false);
assert.match(message.body, /Codex 桌面线程本轮被中断/);
assert.match(project.preview, /Codex 桌面线程本轮被中断/);
});

View File

@@ -1592,6 +1592,123 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete folds thread commentary
assert.equal(updatedProject?.unreadCount, 1);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete strips already mirrored process text from aggregate thread replies", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
await resetThreadExecutionState(singleProject.id);
await setProjectTakeover(singleProject.id, false);
const sendResponse = await postMessageRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
"POST",
{ body: "只要不对原有项目有任何影响。你按最推荐的方式做。" },
),
{ params: Promise.resolve({ projectId: singleProject.id }) },
);
const sendPayload = (await sendResponse.json()) as { task?: { taskId: string } };
const queuedState = await readState();
const task = queuedState.masterAgentTasks.find(
(item) => item.taskId === sendPayload.task?.taskId,
);
assert.ok(task, "expected a queued conversation_reply task");
const processOne = "我先按非侵入方式收口:不碰原项目代码,只把这次 APP 端问题整理成可复现、可提交的诊断文档。";
const processTwo = "我在按调试流程收证据,但会收在文档里,不会动任何现有项目文件。";
const processThree = "文档已经落下来了,我再做一次范围确认,确保只有独立报告被新增。";
const finalText = "我按非侵入方式处理了,没有碰任何原有项目代码,只新增了一份独立排查文档。";
const aggregateReply = `${processOne}${processTwo}${processThree}${finalText}`;
const requestedAtMs = Date.parse(task.requestedAt);
assert.ok(Number.isFinite(requestedAtMs), "expected task requestedAt to be parseable");
const taskRelativeTime = (offsetMs: number) => new Date(requestedAtMs + offsetMs).toISOString();
const mirroredState = await readState();
const project = mirroredState.projects.find((item) => item.id === singleProject.id);
assert.ok(project, "expected the single-thread project to exist");
project.messages = project.messages.filter(
(message) =>
message.id === task.requestMessageId ||
message.executionProgress?.taskId === task.taskId,
);
project.messages.push(
{
id: "msg-process-one",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processOne,
sentAt: taskRelativeTime(1_000),
kind: "thread_process",
},
{
id: "msg-process-two",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processTwo,
sentAt: taskRelativeTime(2_000),
kind: "thread_process",
},
{
id: "msg-process-three",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: processThree,
sentAt: taskRelativeTime(3_000),
kind: "thread_process",
},
{
id: "msg-final-mirrored",
sender: "device",
senderLabel: project.threadMeta.threadDisplayName,
body: finalText,
sentAt: taskRelativeTime(4_000),
kind: "text",
},
);
project.preview = finalText;
project.lastMessageAt = taskRelativeTime(4_000);
project.unreadCount = 1;
await writeState(mirroredState);
const response = await completeMasterTaskRoute(
await createAuthedRequest(
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
"POST",
{
deviceId: task.deviceId,
status: "completed",
targetProjectId: singleProject.id,
targetThreadId: singleProject.threadMeta.threadId,
replyBody: aggregateReply,
},
),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedProject = nextState.projects.find((item) => item.id === singleProject.id);
const aggregateMessages =
updatedProject?.messages.filter((message) => message.body === aggregateReply) ?? [];
const finalMessages = updatedProject?.messages.filter((message) => message.body === finalText) ?? [];
assert.equal(aggregateMessages.length, 0, "aggregate process+final reply should not be displayed");
assert.equal(finalMessages.length, 1, "already mirrored final result should not be duplicated");
assert.equal(updatedProject?.preview, finalText);
assert.equal(updatedProject?.unreadCount, 1);
const cleanupState = await readState();
const cleanupProject = cleanupState.projects.find((item) => item.id === singleProject.id);
if (cleanupProject) {
cleanupProject.messages = [];
cleanupProject.preview = "测试线程等待继续处理。";
cleanupProject.lastMessageAt = "2026-04-04T11:30:00+08:00";
cleanupProject.unreadCount = 0;
}
await writeState(cleanupState);
});
test("POST /api/v1/master-agent/tasks/[taskId]/complete keeps compact numbered progress updates folded", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
@@ -1649,7 +1766,7 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete keeps compact numbered p
assert.equal(updatedProject?.unreadCount, 0);
});
test("device heartbeat keeps conversation preview on the latest non-process message", async () => {
test("device heartbeat activity does not overwrite conversation preview with desktop process text", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
@@ -1713,12 +1830,14 @@ test("device heartbeat keeps conversation preview on the latest non-process mess
(message) => message.externalMessageId === "codex-thread:preview-keep:2026-04-24T05:41:14.246Z:p1",
);
assert.equal(processMessage?.kind, "thread_process");
assert.equal(processMessage, undefined);
assert.equal(updatedProject?.messages.length, 1);
assert.equal(updatedProject?.preview, "这是上一轮最终结果。");
assert.equal(updatedProject?.unreadCount, 0);
assert.equal(updatedProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-24T05:41:14.246Z");
});
test("device heartbeat keeps conversation preview blank when only process messages are mirrored", async () => {
test("device heartbeat activity clears stale process preview without appending desktop process text", async () => {
await setup();
const singleProject = await ensureSingleThreadProject();
assert.ok(singleProject, "expected a seeded single-thread project");
@@ -1773,9 +1892,11 @@ test("device heartbeat keeps conversation preview blank when only process messag
(message) => message.externalMessageId === "codex-thread:preview-empty:2026-04-24T05:41:14.246Z:p1",
);
assert.equal(processMessage?.kind, "thread_process");
assert.equal(processMessage, undefined);
assert.equal(updatedProject?.messages.length, 0);
assert.equal(updatedProject?.preview, "");
assert.equal(updatedProject?.unreadCount, 0);
assert.equal(updatedProject?.threadMeta.lastObservedCodexActivityAt, "2026-04-24T05:41:14.246Z");
});
test("legacy device process text is reclassified and no longer pollutes preview or unread", async () => {

View File

@@ -12,6 +12,14 @@ let handleTelegramWebhookRequest: (typeof import("../src/lib/telegram-gateway"))
let completeTaskRoute: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/complete/route"))["POST"];
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
async function waitForCondition(predicate: () => boolean | Promise<boolean>, timeoutMs = 1000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 20));
}
}
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-telegram-gateway-"));
@@ -216,11 +224,17 @@ test("Telegram webhook 对需要排队的消息会记录 externalReplyTarget
);
assert.equal(completeResponse.status, 200);
await waitForCondition(() => outboundCalls.length >= 2);
assert.equal(outboundCalls.length, 2);
assert.match(String((outboundCalls[1]?.body as { text: string }).text), /已经整理好迁移方案/);
const completedState = await readState();
const completedTask = completedState.masterAgentTasks.find((item) => item.taskId === task?.taskId);
let completedState = await readState();
let completedTask = completedState.masterAgentTasks.find((item) => item.taskId === task?.taskId);
await waitForCondition(async () => {
completedState = await readState();
completedTask = completedState.masterAgentTasks.find((item) => item.taskId === task?.taskId);
return completedTask?.externalReplyTarget?.deliveredAt?.includes("T") === true;
});
assert.equal(completedTask?.externalReplyTarget?.deliveredAt?.includes("T"), true);
} finally {
globalThis.fetch = originalFetch;