Files
tutti/packages/agent/gui/agent-message-center/workspaceAgentMessageCenterDigest.spec.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

527 lines
15 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type {
AgentActivityMessage,
AgentActivityNeedsAttentionItem
} from "@tutti-os/agent-activity-core";
import type { AgentConversationPromptVM } from "../shared/agentConversation/contracts/agentConversationVM";
import {
buildWorkspaceAgentMessageCenterDigest as buildDigestFromPrecomputed,
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary,
type BuildWorkspaceAgentMessageCenterDigestInput,
type WorkspaceAgentMessageCenterDigest,
type WorkspaceAgentMessageCenterDigestAgentSummary
} from "./workspaceAgentMessageCenterDigest";
describe("buildWorkspaceAgentMessageCenterDigest", () => {
it("uses agent messages for summaries instead of newer user messages", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
role: "assistant",
payload: { text: "Agent summary wins" },
occurredAtUnixMs: 10
}),
message({
messageId: "user-1",
role: "user",
payload: { text: "Newer user prompt loses" },
occurredAtUnixMs: 20
})
],
needsAttention: null,
pendingPrompt: null,
status: "idle"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: "Agent summary wins",
occurredAtUnixMs: 10
});
});
it("uses a precomputed latest agent message when provided", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
latestAgentMessage: {
summary: "Precomputed summary",
occurredAtUnixMs: 42
},
messages: [
message({
messageId: "assistant-1",
role: "assistant",
payload: { text: "Message summary" },
occurredAtUnixMs: 10
})
],
needsAttention: null,
pendingPrompt: null,
status: "working"
});
expect(digest.primary).toMatchObject({
kind: "progress",
summary: "Precomputed summary",
occurredAtUnixMs: 42
});
});
it("treats an explicit null latest agent message as precomputed empty", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
latestAgentMessage: null,
messages: [
message({
messageId: "assistant-1",
role: "assistant",
payload: { text: "Skipped message summary" },
occurredAtUnixMs: 10
})
],
needsAttention: null,
pendingPrompt: null,
status: "idle"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: "Fallback title",
occurredAtUnixMs: null
});
});
it("prefers displayPrompt for message summaries", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
role: "assistant",
payload: {
displayPrompt: "Run Automation",
text: "long automation prompt",
content: "long automation prompt"
},
occurredAtUnixMs: 10
})
],
needsAttention: null,
pendingPrompt: null,
status: "idle"
});
expect(digest.primary).toMatchObject({
summary: "Run Automation"
});
});
it("prioritizes input-required over terminal-looking message summaries", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
payload: { text: "Finished writing files" },
occurredAtUnixMs: 20
})
],
needsAttention: needsAttention({
kind: "question",
summary: "Choose the next step",
occurredAtUnixMs: 30
}),
pendingPrompt: askUserPrompt("Choose the next step"),
status: "working"
});
expect(digest.primary).toMatchObject({
kind: "input-required",
summary: "Choose the next step",
occurredAtUnixMs: 30
});
});
it("uses failed status for the primary kind even when the summary contains an artifact link", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
payload: {
text: "Created [report.md](report.md), then the request failed"
},
occurredAtUnixMs: 40
})
],
needsAttention: null,
pendingPrompt: null,
status: "failed"
});
expect(digest.primary).toMatchObject({
kind: "error",
summary: "Created [report.md](report.md), then the request failed"
});
});
it("skips generic tool-name summaries when choosing the card body", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
kind: "message.assistant",
payload: {
text: "当前会话目录不在沙箱可写根里,我先尝试用标准补丁工具写入。"
},
occurredAtUnixMs: 10
}),
message({
messageId: "approval-1",
kind: "tool_call",
status: "failed",
payload: {
callType: "approval",
toolName: "Approval",
title: "Approval"
},
occurredAtUnixMs: 20
})
],
needsAttention: null,
pendingPrompt: null,
status: "completed"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: "当前会话目录不在沙箱可写根里,我先尝试用标准补丁工具写入。",
occurredAtUnixMs: 10
});
});
it("uses generic tool output when it has a concrete result", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
payload: { text: "I will run the tests." },
occurredAtUnixMs: 10
}),
message({
messageId: "tool-1",
kind: "tool_call",
status: "completed",
payload: {
output: { summary: "53 tests passed" },
title: "Bash",
toolName: "Bash"
},
occurredAtUnixMs: 20
})
],
needsAttention: null,
pendingPrompt: null,
status: "completed"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: "53 tests passed",
occurredAtUnixMs: 20
});
});
it("uses nested MCP approval tool errors as concrete tool results", () => {
const errorText =
"### Error\nError: Browser is already in use for /Users/ccr/.codex/playwright-profile, use --isolated to run multiple instances of the same browser";
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
payload: {
text: "我会尝试关闭占用的 Playwright 会话后重新打开页面。"
},
occurredAtUnixMs: 10
}),
message({
messageId: "toolcall:call_ZYi9QClIkukN1cu3Kv30mQe4",
kind: "tool_call",
status: "failed",
payload: {
callId: "call_ZYi9QClIkukN1cu3Kv30mQe4",
callType: "tool",
error: {
content: [
{
content: { text: errorText, type: "text" },
type: "content"
}
],
isError: true,
stdout: errorText
},
input: {
id: "mcp_tool_call_approval_call_ZYi9QClIkukN1cu3Kv30mQe4",
request: {
_meta: {
codex_approval_kind: "mcp_tool_call"
},
message:
'Allow the playwright MCP server to run tool "browser_close"?'
},
server: "playwright",
tool: "browser_close"
},
name: "Approval",
output: { stdout: errorText },
title: "Approval",
toolName: "Approval"
},
occurredAtUnixMs: 20
})
],
needsAttention: null,
pendingPrompt: null,
status: "completed"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary:
"playwright / browser_close: Error: Browser is already in use for /Users/ccr/.codex/playwright-profile, use --isolated to run multiple instances of the same browser",
occurredAtUnixMs: 20
});
});
it("uses generic tool input targets when no result summary exists", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [
message({
messageId: "assistant-1",
payload: { text: "I will verify the package." },
occurredAtUnixMs: 10
}),
message({
messageId: "tool-1",
kind: "tool_call",
status: "completed",
payload: {
input: { command: "pnpm --filter @tutti-os/agent-gui test" },
title: "Bash",
toolName: "Bash"
},
occurredAtUnixMs: 20
})
],
needsAttention: null,
pendingPrompt: null,
status: "completed"
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: "pnpm --filter @tutti-os/agent-gui test",
occurredAtUnixMs: 20
});
});
it("classifies completed, idle, and canceled sessions as outcome digests", () => {
for (const status of ["completed", "idle", "canceled"] as const) {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [message({ payload: { text: `${status} summary` } })],
needsAttention: null,
pendingPrompt: null,
status
});
expect(digest.primary).toMatchObject({
kind: "outcome",
summary: `${status} summary`
});
}
});
it("classifies working sessions with agent messages as progress digests", () => {
const digest = buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [message({ payload: { text: "Scanning files" } })],
needsAttention: null,
pendingPrompt: null,
status: "working"
});
expect(digest.primary).toMatchObject({
kind: "progress",
summary: "Scanning files"
});
});
it("falls back to needs-attention summary or title when no agent summary exists", () => {
expect(
buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [],
needsAttention: needsAttention({ summary: "Answer required" }),
pendingPrompt: null,
status: "waiting"
}).primary.summary
).toBe("Answer required");
expect(
buildWorkspaceAgentMessageCenterDigest({
fallbackTitle: "Fallback title",
messages: [],
needsAttention: null,
pendingPrompt: null,
status: "idle"
}).primary.summary
).toBe("Fallback title");
});
it("refreshes a cached agent summary when the message payload identity changes", () => {
const item = message({
payload: { text: "First summary" }
});
expect(
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(item)
).toBe("First summary");
item.payload = { text: "Second summary" };
expect(
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(item)
).toBe("Second summary");
});
it("refreshes a cached agent summary when the message version changes", () => {
const payload = { text: "First summary" };
const item = message({ payload });
expect(
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(item)
).toBe("First summary");
payload.text = "Second summary";
item.version += 1;
expect(
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(item)
).toBe("Second summary");
});
});
type DigestSpecInput = Omit<
BuildWorkspaceAgentMessageCenterDigestInput,
"latestAgentMessage"
> & {
latestAgentMessage?: WorkspaceAgentMessageCenterDigestAgentSummary | null;
messages?: readonly AgentActivityMessage[];
};
function buildWorkspaceAgentMessageCenterDigest(
input: DigestSpecInput
): WorkspaceAgentMessageCenterDigest {
const { latestAgentMessage, messages = [], ...digestInput } = input;
return buildDigestFromPrecomputed({
...digestInput,
latestAgentMessage:
latestAgentMessage === undefined
? latestDigestAgentMessage(messages)
: latestAgentMessage
});
}
function latestDigestAgentMessage(
messages: readonly AgentActivityMessage[]
): WorkspaceAgentMessageCenterDigestAgentSummary | null {
let latest: WorkspaceAgentMessageCenterDigestAgentSummary | null = null;
for (const item of messages) {
const summary =
resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(item);
if (!summary) {
continue;
}
const occurredAtUnixMs = messageTimeUnixMs(item);
if (!latest || occurredAtUnixMs >= latest.occurredAtUnixMs) {
latest = { summary, occurredAtUnixMs };
}
}
return latest;
}
function message(
overrides: Partial<AgentActivityMessage>
): AgentActivityMessage {
return {
workspaceId: "workspace-1",
agentSessionId: "session-1",
messageId: "message-1",
version: 1,
turnId: "turn-1",
role: "assistant",
kind: "message.assistant",
status: "running",
payload: {},
occurredAtUnixMs: 1,
...overrides
};
}
function needsAttention(
overrides: Partial<AgentActivityNeedsAttentionItem>
): AgentActivityNeedsAttentionItem {
return {
id: "session-1:message-1",
workspaceId: "workspace-1",
agentSessionId: "session-1",
provider: "codex",
title: "Fallback title",
cwd: "/workspace",
kind: "question",
summary: "Needs attention",
occurredAtUnixMs: 1,
...overrides
};
}
function askUserPrompt(title: string): AgentConversationPromptVM {
return {
kind: "ask-user",
requestId: "request-1",
title,
questions: [
{
id: "response",
header: title,
question: title,
options: [],
multiSelect: false,
answer: null
}
]
};
}
function messageTimeUnixMs(message: AgentActivityMessage): number {
return (
positiveNumber(message.occurredAtUnixMs) ??
positiveNumber(message.completedAtUnixMs) ??
positiveNumber(message.startedAtUnixMs) ??
positiveNumber(message.version) ??
0
);
}
function positiveNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) && value > 0
? value
: null;
}