pi-mono/packages/coding-agent/test/export-html-system-messages.test.ts
Petr Houška 9359adf0c6 fix(coding-agent): render system messages in HTML export
Render the leading system message as the system prompt and tool list,
and later system messages as named section and tool updates. The header
prompt/tools from agent state remain as a fallback for sessions without
system entries. CLI --export now shows the prompt and tools too (#3464).

closes #11261
2026-10-22 20:37:03 +02:00

83 lines
3.6 KiB
TypeScript

import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { getSystemMessageText, renderSystemMessageUpdate, type SystemMessage } from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
interface SystemHelpers {
getSystemMessageText(message: SystemMessage): string;
renderSystemMessageUpdate(message: SystemMessage): string;
isLeadingSystemEntry(entry: { id: string; parentId: string | null }): boolean;
describeSystemUpdate(message: SystemMessage): string;
}
const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8");
function loadSystemHelpers(entries: unknown[]): SystemHelpers {
const start = templateJs.indexOf("function systemContentText(content)");
const end = templateJs.indexOf("const hasSystemEntries");
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
const context: { entries: unknown[]; helpers?: SystemHelpers } = { entries };
runInNewContext(
`const byId = new Map(entries.map(entry => [entry.id, entry]));
${templateJs.slice(start, end)}
helpers = { getSystemMessageText, renderSystemMessageUpdate, isLeadingSystemEntry, describeSystemUpdate };`,
context,
);
return context.helpers!;
}
const initial: SystemMessage = {
role: "system",
content: "You are a helpful assistant.",
sections: { rules: "<rules>\nbe concise\n</rules>", project_context: "<project_context>\nv1\n</project_context>" },
toolsAdded: [{ name: "read", description: "Read a file", parameters: { type: "object", properties: {} } as never }],
timestamp: 1,
};
const update: SystemMessage = {
role: "system",
content: [{ type: "text", text: "Extra instructions" }],
sections: { project_context: "<project_context>\nv2\n</project_context>", rules: null },
toolsAdded: [
{ name: "bash", description: "Run a command", parameters: { type: "object", properties: {} } as never },
],
toolsRemoved: [{ name: "read" }],
timestamp: 2,
};
const entries = [
{ type: "message", id: "s1", parentId: null, message: initial },
{ type: "message", id: "u1", parentId: "s1", message: { role: "user", content: "hi" } },
{ type: "message", id: "s2", parentId: "u1", message: update },
];
// Regression tests for #11261: system messages were stored in the export but never rendered.
describe("export HTML system messages", () => {
it("renders system message text the same way the model receives it", () => {
const helpers = loadSystemHelpers(entries);
expect(helpers.getSystemMessageText(initial)).toBe(getSystemMessageText(initial));
expect(helpers.renderSystemMessageUpdate(update)).toBe(renderSystemMessageUpdate(update));
});
it("renders the first system entry on a branch as the prompt and later ones as updates", () => {
const helpers = loadSystemHelpers(entries);
expect(helpers.isLeadingSystemEntry(entries[0])).toBe(true);
expect(helpers.isLeadingSystemEntry(entries[2])).toBe(false);
expect(helpers.describeSystemUpdate(update)).toBe("instructions, project_context, -rules, tools +bash -read");
});
it("treats a compaction snapshot as earlier system state", () => {
const helpers = loadSystemHelpers([
{ type: "compaction", id: "c1", parentId: null, systemMessage: initial },
{ type: "message", id: "s2", parentId: "c1", message: update },
]);
expect(helpers.isLeadingSystemEntry({ id: "s2", parentId: "c1" })).toBe(false);
});
it("renders system entries in the transcript", () => {
expect(templateJs).toMatch(/if \(msg\.role === 'system'\) \{\s*let html = '';/);
expect(templateJs).toContain("renderSystemPromptBlock('System Prompt', text)");
expect(templateJs).toContain("renderToolsList('Available Tools', msg.toolsAdded)");
});
});