murashit_codex-panel/tests/app-server/turn.test.ts
2026-07-09 08:06:57 +09:00

188 lines
6.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
chronologicalTurnTranscriptSummariesFromTurnRecords,
completedTurnTranscriptSummariesFromTurnRecords,
completedTurnTranscriptSummaryFromTurnRecord,
lastAgentMessageTextFromTurnRecord,
type TurnItem,
type TurnRecord,
transcriptEntriesFromTurnRecords,
turnTranscriptAssistantTextFromTurnRecord,
} from "../../src/app-server/protocol/turn";
describe("app-server turn records", () => {
it("projects readable transcript entries without command log items", () => {
expect(
transcriptEntriesFromTurnRecords([
turn([userMessage("u1", " 依頼です "), commandItem("cmd"), agentMessage("a1", "回答です"), planItem("p1", "計画です")], {
startedAt: 10,
completedAt: 12,
}),
]),
).toEqual([
{ kind: "user", text: "依頼です", timestamp: 10 },
{ kind: "assistant", text: "回答です", timestamp: 12 },
{ kind: "plan", text: "計画です", timestamp: 12 },
]);
});
it("builds completed turn transcript summaries from the first user message and last assistant-like item", () => {
expect(
completedTurnTranscriptSummaryFromTurnRecord(
turn([
agentMessage("draft", "途中経過"),
userMessage("u1", "最初の依頼"),
userMessage("u2", "補足"),
agentMessage("a1", "最終回答"),
]),
),
).toEqual({ userText: "最初の依頼", assistantText: "最終回答" });
});
it("does not build completed turn transcript summaries for failed turns", () => {
expect(
completedTurnTranscriptSummaryFromTurnRecord(turn([userMessage("u1", "依頼"), agentMessage("a1", "回答")], { status: "failed" })),
).toBeNull();
});
it("projects completed turn transcript summaries from turn lists without exposing filtering logic to callers", () => {
expect(
completedTurnTranscriptSummariesFromTurnRecords([
turn([userMessage("u1", "依頼"), agentMessage("a1", "回答")], { id: "completed" }),
turn([userMessage("u2", "失敗した依頼"), agentMessage("a2", "失敗した回答")], { id: "failed", status: "failed" }),
turn([commandItem("cmd")], { id: "empty" }),
]),
).toEqual([{ userText: "依頼", assistantText: "回答" }]);
});
it("returns chronological turn transcript summaries and drops turns without transcript text", () => {
expect(
chronologicalTurnTranscriptSummariesFromTurnRecords([
turn([userMessage("u2", "後の依頼"), agentMessage("a2", "後の回答")], { id: "turn-2", startedAt: 20 }),
turn([commandItem("cmd")], { id: "turn-empty", startedAt: 15 }),
turn([userMessage("u1", "先の依頼"), agentMessage("a1", "先の回答")], { id: "turn-1", startedAt: 10 }),
]),
).toEqual([
{ userText: "先の依頼", assistantText: "先の回答" },
{ userText: "後の依頼", assistantText: "後の回答" },
]);
});
it("projects transcript entries from turn lists", () => {
expect(
transcriptEntriesFromTurnRecords([
turn([userMessage("u1", "先の依頼")], { id: "turn-1", startedAt: 10 }),
turn([agentMessage("a1", "後の回答")], { id: "turn-2", startedAt: 20, completedAt: 25 }),
]),
).toEqual([
{ kind: "user", text: "先の依頼", timestamp: 10 },
{ kind: "assistant", text: "後の回答", timestamp: 25 },
]);
});
it("keeps local image attachments out of user transcript text when text is present", () => {
expect(
transcriptEntriesFromTurnRecords([
turn([
{
type: "userMessage",
id: "u1",
clientId: null,
content: [
{ type: "text", text: "![[Codex Attachments/diagram.png]]", text_elements: [] },
{ type: "localImage", path: "Codex Attachments/diagram.png" },
],
},
]),
]),
).toEqual([{ kind: "user", text: "![[Codex Attachments/diagram.png]]", timestamp: null }]);
});
it("keeps image attachment stubs when user text does not contain the image reference", () => {
expect(
transcriptEntriesFromTurnRecords([
turn([
{
type: "userMessage",
id: "u1",
clientId: null,
content: [
{ type: "text", text: "この画像を見てください", text_elements: [] },
{ type: "localImage", path: "Codex Attachments/diagram.png" },
{ type: "image", url: "https://example.com/diagram.png" },
],
},
]),
]),
).toEqual([
{
kind: "user",
text: "この画像を見てください\n[local image] Codex Attachments/diagram.png\n[image] https://example.com/diagram.png",
timestamp: null,
},
]);
});
it("extracts assistant-like transcript text for generated turn consumers", () => {
expect(turnTranscriptAssistantTextFromTurnRecord(turn([userMessage("u1", "依頼"), planItem("p1", "計画")]))).toBe("計画");
});
it("extracts the final non-empty agent message text from a turn", () => {
expect(
lastAgentMessageTextFromTurnRecord(
turn([
agentMessage("a1", '{"replacementText":"first"}'),
agentMessage("a2", " "),
agentMessage("a3", '{"replacementText":"final"}'),
]),
),
).toBe('{"replacementText":"final"}');
});
it("returns null when a turn has no agent message text", () => {
expect(lastAgentMessageTextFromTurnRecord(turn([agentMessage("a1", " ")]))).toBeNull();
});
});
function turn(items: TurnRecord["items"], overrides: Partial<TurnRecord> = {}): TurnRecord {
return {
id: "turn",
items,
itemsView: "full",
status: "completed",
error: null,
startedAt: null,
completedAt: null,
durationMs: null,
...overrides,
};
}
function userMessage(id: string, text: string): TurnItem {
return { type: "userMessage", id, clientId: null, content: [{ type: "text", text, text_elements: [] }] };
}
function agentMessage(id: string, text: string): TurnItem {
return { type: "agentMessage", id, text, phase: "final_answer", memoryCitation: null };
}
function planItem(id: string, text: string): TurnItem {
return { type: "plan", id, text };
}
function commandItem(id: string): TurnItem {
return {
type: "commandExecution",
id,
command: "npm test",
cwd: "/vault",
processId: null,
source: "agent",
status: "completed",
commandActions: [],
aggregatedOutput: "ignored",
exitCode: 0,
durationMs: 1,
};
}