Files
oh-my-pi/packages/coding-agent/test/session/session-history-format.test.ts
T
can1357 5bce7ed6df feat: added advisory transcript formatting and one-shot benchmark metrics
- Introduced advisory note output as `<advisory>` tags with optional severity and guidance.
- Updated session transcript formatting to `### Session update` and inline watched role labels.
- Added shared `escapeXmlText` utility and escaped XML-sensitive text in advisor outputs.
- Added one-shot success run token metrics and one-shot statistics reporting.
2026-06-16 18:34:50 +02:00

167 lines
5.7 KiB
TypeScript

/**
* Contracts: history:// transcript serializer (rework-contracts.md §5).
*
* - `## user` / `## assistant` headers carry full text.
* - Thinking blocks are elided entirely.
* - Each toolCall collapses with its toolResult into ONE `→ name(…) ⇒ …`
* line (ok and error variants); result bodies are never dumped.
* - Custom messages render as one-liners (`[irc] from → me: …`).
* - No system prompt / tool catalog sections.
*/
import { describe, expect, it } from "bun:test";
import { formatSessionHistoryMarkdown } from "@oh-my-pi/pi-coding-agent/session/session-history-format";
function buildMessages(): unknown[] {
return [
{ role: "user", content: "Please read the config.", timestamp: 1 },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "SECRET-THOUGHT about the approach" },
{ type: "text", text: "Reading it now." },
{ type: "toolCall", id: "tc-1", name: "read", arguments: { path: "src/config.ts" } },
{ type: "toolCall", id: "tc-2", name: "bash", arguments: { command: "bun test" } },
],
api: "anthropic-messages",
provider: "anthropic",
model: "test-model",
usage: {},
stopReason: "toolUse",
timestamp: 2,
},
{
role: "toolResult",
toolCallId: "tc-1",
toolName: "read",
content: [{ type: "text", text: "const a = 1;\nconst b = 2;\nconst c = 3;" }],
isError: false,
timestamp: 3,
},
{
role: "toolResult",
toolCallId: "tc-2",
toolName: "bash",
content: [{ type: "text", text: "FAIL: 1 test failed" }],
isError: true,
timestamp: 4,
},
{
role: "custom",
customType: "irc:incoming",
content: "full rendered irc prompt that must not appear",
details: { from: "Main", message: "status update please" },
timestamp: 5,
},
];
}
describe("formatSessionHistoryMarkdown", () => {
it("renders role headers, collapses tool pairs to one line, and elides thinking", () => {
const output = formatSessionHistoryMarkdown(buildMessages());
expect(output).toContain("## user");
expect(output).toContain("Please read the config.");
expect(output).toContain("## assistant");
expect(output).toContain("Reading it now.");
// Thinking is elided entirely.
expect(output).not.toContain("SECRET-THOUGHT");
// Tool call + result collapse to one line each; bodies are not dumped.
expect(output).toContain("→ read(src/config.ts) ⇒ ok · 3 lines");
expect(output).not.toContain("const a = 1;");
// Error variant carries the first line of the error output.
expect(output).toContain("→ bash(bun test) ⇒ error · 1 line — FAIL: 1 test failed");
// Consumed toolResults do not render a second orphan line.
const toolLines = output.split("\n").filter(line => line.startsWith("→ "));
expect(toolLines).toHaveLength(2);
// Custom messages are one-liners; the rendered prompt body is dropped.
expect(output).toContain("[irc] Main → me: status update please");
expect(output).not.toContain("full rendered irc prompt");
// Concise transcript: no prompt/tool-catalog sections.
expect(output).not.toContain("System Prompt");
expect(output).not.toContain("Available Tools");
});
it("prefixes an H1 title when requested", () => {
const output = formatSessionHistoryMarkdown(buildMessages(), { title: "Spawnling (idle)" });
expect(output.startsWith("# Spawnling (idle)\n")).toBe(true);
});
it("renders watched roles using bold text rather than level-2 headers when watchedRoles is true", () => {
const output = formatSessionHistoryMarkdown(buildMessages(), { watchedRoles: true });
expect(output).toContain("**user**:");
expect(output).toContain("**agent**:");
expect(output).not.toContain("## user");
expect(output).not.toContain("## assistant");
});
it("renders an orphan toolResult (truncated history) as its own line", () => {
const output = formatSessionHistoryMarkdown([
{
role: "toolResult",
toolCallId: "tc-orphan",
toolName: "search",
content: [{ type: "text", text: "one match" }],
isError: false,
timestamp: 1,
},
]);
expect(output).toContain("→ search() ⇒ ok · 1 line");
});
it("renders tool intent comments immediately before tool call lines when includeToolIntent is true", () => {
const messages = [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "tc-intent",
name: "read",
arguments: { path: "src/config.ts", _i: "reading config file" },
},
{
type: "toolCall",
id: "tc-long-intent",
name: "read",
arguments: {
path: "src/config.ts",
_i: "reading config file with a very very long and descriptive intent that will exceed the maximum length limit of eighty characters",
},
},
],
},
{
role: "toolResult",
toolCallId: "tc-intent",
toolName: "read",
content: [{ type: "text", text: "ok" }],
isError: false,
},
{
role: "toolResult",
toolCallId: "tc-long-intent",
toolName: "read",
content: [{ type: "text", text: "ok" }],
isError: false,
},
];
const outputWithIntent = formatSessionHistoryMarkdown(messages, { includeToolIntent: true });
expect(outputWithIntent).toContain("// reading config file\n→ read(src/config.ts) ⇒ ok · 1 line");
// The long intent should be flattened to one line and truncated to 80 characters (including ellipsis).
expect(outputWithIntent).toContain(
"// reading config file with a very very long and descriptive intent that will exce…\n→ read(src/config.ts) ⇒ ok · 1 line",
);
const outputWithoutIntent = formatSessionHistoryMarkdown(messages);
expect(outputWithoutIntent).not.toContain("// reading config file");
expect(outputWithoutIntent).toContain("→ read(src/config.ts) ⇒ ok · 1 line");
});
});