Files
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

563 lines
15 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AgentActivityTurn } from "@tutti-os/agent-activity-core";
import type { AgentTranscriptRowVM } from "../contracts/agentTranscriptRowVM";
import type { AgentTranscriptTurnGroup } from "./agentTranscriptModel";
import {
buildAgentTurnWorkSectionModel as buildOptionalAgentTurnWorkSectionModel,
formatAgentTurnDuration,
resolveAgentTurnTiming
} from "./agentTurnWorkSectionModel";
describe("agentTurnWorkSectionModel", () => {
it("uses canonical turn timestamps for live and settled elapsed time", () => {
expect(
resolveAgentTurnTiming(canonicalTurn({ phase: "running" }), true)
).toEqual({ kind: "live", startedAtUnixMs: 5_000 });
expect(
resolveAgentTurnTiming(canonicalTurn({ phase: "running" }), false)
).toBeNull();
expect(
resolveAgentTurnTiming(canonicalTurn({ phase: "waiting" }), true)
).toEqual({ kind: "live", startedAtUnixMs: 5_000 });
expect(
resolveAgentTurnTiming(canonicalTurn({ phase: "submitted" }), true)
).toEqual({ kind: "live", startedAtUnixMs: 5_000 });
expect(
resolveAgentTurnTiming(canonicalTurn({ phase: "settling" }), true)
).toEqual({ kind: "live", startedAtUnixMs: 5_000 });
expect(
resolveAgentTurnTiming(
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 127_999
}),
false
)
).toEqual({ kind: "settled", elapsedSeconds: 122 });
expect(
resolveAgentTurnTiming(
canonicalTurn({ startedAtUnixMs: Number.NaN }),
true
)
).toBeNull();
expect(
resolveAgentTurnTiming(
canonicalTurn({ phase: "settled", settledAtUnixMs: 4_999 }),
false
)
).toBeNull();
});
it("counts settled turn time from client submission", () => {
const prompt = {
...message("Please fix it", null),
occurredAtUnixMs: 2_000,
sourceTimelineItems: [
{
id: 1,
agentSessionId: "session-1",
eventId: "user-1",
actorType: "user",
actorId: "user",
itemType: "message",
payload: { clientSubmittedAtUnixMs: 1_000 },
occurredAtUnixMs: 2_000
}
]
};
const reply = message("Final answer", "Final answer", true);
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow({ occurredAtUnixMs: 2_000, messages: [prompt] }),
assistantRow({ occurredAtUnixMs: 10_000, messages: [reply] })
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
startedAtUnixMs: 6_000,
settledAtUnixMs: 13_000
})
);
expect(model.timing).toEqual({ kind: "settled", elapsedSeconds: 12 });
});
it("formats seconds and minute boundaries without a zero-second suffix", () => {
expect(formatAgentTurnDuration(45)).toEqual({
kind: "seconds",
seconds: 45
});
expect(formatAgentTurnDuration(60)).toEqual({
kind: "minutes",
minutes: 1
});
expect(formatAgentTurnDuration(127)).toEqual({
kind: "minutes-seconds",
minutes: 2,
seconds: 7
});
});
it("does not create a disclosure model without canonical timing", () => {
expect(
buildOptionalAgentTurnWorkSectionModel(turnGroup([userRow()]), null)
).toBeNull();
expect(
buildOptionalAgentTurnWorkSectionModel(
turnGroup([userRow()]),
canonicalTurn({ phase: "running" }),
false
)
).toBeNull();
});
it("keeps the final assistant copy target visible and moves work around it", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
assistantRow({
id: "assistant-row",
messages: [
message("draft", null),
message("final", "Final answer", true),
message("epilogue", null)
],
thinking: [thinking("Inspecting files")]
}),
toolRow(),
summaryRow()
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
}),
false,
{ collapseIntermediateAssistantReplies: true }
);
expect(model.leadingRows).toHaveLength(1);
expect(model.sections.map((section) => section.kind)).toEqual([
"work",
"visible",
"work",
"visible"
]);
expect(model.collapseEligible).toBe(true);
const finalRow = model.sections[1]?.rows[0]?.row;
expect(finalRow?.kind).toBe("message");
if (finalRow?.kind === "message") {
expect(finalRow.id).toBe("assistant-row");
expect(finalRow.messages.map((item) => item.body)).toEqual(["final"]);
expect(finalRow.thinking).toEqual([]);
}
expect(model.sections[0]?.rows[0]?.renderKey).toBe(
"assistant-row:turn-work-before"
);
expect(model.sections[1]?.rows[0]?.renderKey).toBe(
"assistant-row:turn-final"
);
expect(model.sections[2]?.rows[0]?.renderKey).toBe(
"assistant-row:turn-work-after"
);
const afterFinalRow = model.sections[2]?.rows[0]?.row;
expect(afterFinalRow?.kind).toBe("message");
if (afterFinalRow?.kind === "message") {
expect(afterFinalRow.messages.map((item) => item.body)).toEqual([
"epilogue"
]);
}
expect(model.sections[3]?.rows.map(({ row }) => row.id)).toEqual([
"summary"
]);
});
it("uses an explicit final-text marker instead of copy availability", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
toolRow(),
assistantRow({
messages: [message("Visible final answer", null, true)]
})
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
}),
false,
{ collapseIntermediateAssistantReplies: true }
);
expect(model.collapseEligible).toBe(true);
expect(model.sections).toHaveLength(2);
expect(model.sections[1]?.rows[0]?.row.kind).toBe("message");
});
it("preserves interleaved user and assistant chronology in ordered sections", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
assistantRow({
id: "assistant-1",
messages: [message("Earlier answer", null)]
}),
userRow({
id: "user-2",
messages: [message("Follow-up", null)],
occurredAtUnixMs: 9_000
}),
toolRow(),
assistantRow({
id: "assistant-2",
messages: [message("Final answer", "Final answer", true)]
})
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
}),
false,
{ collapseIntermediateAssistantReplies: true }
);
expect(model.leadingRows.map(({ row }) => row.id)).toEqual(["user-row"]);
expect(
model.sections.map((section) => ({
kind: section.kind,
rowIds: section.rows.map(({ row }) => row.id)
}))
).toEqual([
{ kind: "work", rowIds: ["assistant-1"] },
{ kind: "visible", rowIds: ["user-2"] },
{ kind: "work", rowIds: ["tools"] },
{ kind: "visible", rowIds: ["assistant-2"] }
]);
});
it("collapses all assistant work before the final answer for Tutti-created conversations", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
assistantRow({
id: "assistant-progress",
messages: [
{
...message("Compacting context", null),
presentationKind: "specific-progress"
},
message("Earlier answer", null)
],
thinking: [thinking("Inspecting files")]
}),
assistantRow({
id: "assistant-final",
messages: [message("Final answer", "Final answer", true)]
})
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
}),
false,
{ collapseIntermediateAssistantReplies: true }
);
expect(
model.sections.map((section) => ({
kind: section.kind,
bodies: section.rows.flatMap(({ row }) =>
row.kind === "message"
? [
...row.thinking.map((item) => item.body),
...row.messages.map((item) => item.body)
]
: []
)
}))
).toEqual([
{
kind: "work",
bodies: ["Inspecting files", "Compacting context", "Earlier answer"]
},
{
kind: "visible",
bodies: ["Final answer"]
}
]);
expect(model.collapseEligible).toBe(true);
});
it("preserves ordinary assistant replies for imported conversation history", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
assistantRow({
id: "assistant-earlier",
messages: [message("Earlier answer", null)]
}),
toolRow(),
assistantRow({
id: "assistant-final",
messages: [message("Final answer", "Final answer", true)]
})
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
}),
false,
{ collapseIntermediateAssistantReplies: false }
);
expect(
model.sections.map((section) => ({
kind: section.kind,
bodies: section.rows.flatMap(({ row }) =>
row.kind === "message" ? row.messages.map((item) => item.body) : []
)
}))
).toEqual([
{ kind: "visible", bodies: ["Earlier answer"] },
{ kind: "work", bodies: [] },
{ kind: "visible", bodies: ["Final answer"] }
]);
});
it("fails open when no visible final text is explicitly marked", () => {
const copyOnly = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
toolRow(),
assistantRow({ messages: [message("Copy only", "Copy only")] })
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
})
);
const blankMarked = buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
toolRow(),
assistantRow({ messages: [message(" ", null, true)] })
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
})
);
expect(copyOnly.collapseEligible).toBe(false);
expect(blankMarked.collapseEligible).toBe(false);
});
it("does not offer auto-collapse for abnormal or artifact-first turns", () => {
const rows = [userRow(), toolRow(), assistantRow(), summaryRow()];
expect(
buildAgentTurnWorkSectionModel(
turnGroup(rows),
canonicalTurn({
phase: "settled",
outcome: "failed",
settledAtUnixMs: 15_000
})
).collapseEligible
).toBe(false);
expect(
buildAgentTurnWorkSectionModel(
turnGroup([...rows, generatedImageRow()]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
})
).collapseEligible
).toBe(false);
expect(
buildAgentTurnWorkSectionModel(
turnGroup([
userRow(),
assistantRow({
messages: [
{
...message("Config invalid", null),
visibleError: {
code: "invalid_config",
phase: null,
provider: "codex",
detail: "Config invalid",
retryable: false
}
},
message("final", "Final answer", true)
]
})
]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
})
).collapseEligible
).toBe(false);
});
it("shows timing without a disclosure when a completed turn has only a final answer", () => {
const model = buildAgentTurnWorkSectionModel(
turnGroup([userRow(), assistantRow()]),
canonicalTurn({
phase: "settled",
outcome: "completed",
settledAtUnixMs: 15_000
})
);
expect(model.timing).toEqual({ kind: "settled", elapsedSeconds: 10 });
expect(model.collapseEligible).toBe(false);
expect(model.sections).toHaveLength(1);
expect(model.sections[0]?.kind).toBe("visible");
});
});
function canonicalTurn(
overrides: Partial<AgentActivityTurn> = {}
): AgentActivityTurn {
return {
agentSessionId: "session-1",
origin: "user_prompt",
phase: "running",
startedAtUnixMs: 5_000,
turnId: "turn-1",
updatedAtUnixMs: 6_000,
...overrides
};
}
function buildAgentTurnWorkSectionModel(
...args: Parameters<typeof buildOptionalAgentTurnWorkSectionModel>
): NonNullable<ReturnType<typeof buildOptionalAgentTurnWorkSectionModel>> {
const model = buildOptionalAgentTurnWorkSectionModel(...args);
expect(model).not.toBeNull();
return model!;
}
function turnGroup(rows: AgentTranscriptRowVM[]): AgentTranscriptTurnGroup {
return {
key: "turn-1",
turnId: "turn-1",
rows: rows.map((row, rowIndex) => ({ row, rowIndex }))
};
}
function userRow(
overrides: Partial<Extract<AgentTranscriptRowVM, { kind: "message" }>> = {}
): AgentTranscriptRowVM {
return {
kind: "message",
id: "user-row",
turnId: "turn-1",
speaker: "user",
messages: [message("Please fix it", null)],
thinking: [],
occurredAtUnixMs: 5_000,
...overrides
};
}
function assistantRow(
overrides: Partial<Extract<AgentTranscriptRowVM, { kind: "message" }>> = {}
): AgentTranscriptRowVM {
return {
kind: "message",
id: "assistant-row",
turnId: "turn-1",
speaker: "assistant",
messages: [message("Final answer", "Final answer", true)],
thinking: [],
occurredAtUnixMs: 14_000,
...overrides
};
}
function message(
body: string,
copyText: string | null,
isTurnFinalText = false
) {
return {
kind: "message-content" as const,
id: `message:${body}`,
turnId: "turn-1",
body,
presentationKind: "content" as const,
copyText,
...(isTurnFinalText ? { isTurnFinalText: true as const } : {}),
occurredAtUnixMs: 14_000
};
}
function thinking(body: string) {
return {
kind: "thinking-content" as const,
id: `thinking:${body}`,
turnId: "turn-1",
body,
occurredAtUnixMs: 7_000
};
}
function toolRow(): AgentTranscriptRowVM {
return {
kind: "tool-group",
id: "tools",
turnId: "turn-1",
grouped: true,
calls: [],
entries: [],
occurredAtUnixMs: 8_000
};
}
function summaryRow(): AgentTranscriptRowVM {
return {
kind: "turn-summary",
id: "summary",
turnId: "turn-1",
files: [],
fileCount: 0,
modifiedCount: 0,
createdCount: 0,
occurredAtUnixMs: 15_000
};
}
function generatedImageRow(): AgentTranscriptRowVM {
return {
kind: "generated-image",
id: "image",
turnId: "turn-1",
sourceCallId: "image-call",
uri: "file:///tmp/image.png",
mimeType: "image/png",
prompt: "A test image",
occurredAtUnixMs: 12_000
};
}