Files
oh-my-pi/packages/coding-agent/test/tools/edit-renderer.test.ts
T
can1357 ebd2f77770 feat: implemented canonical § hashlines with ≔/""/" ops in hashline parser
- Changed hashline format to canonical `§` section headers and `"`/`"`/`≔` operations across grammar, parser, and docs.
- Reworked range and op parsing so single anchors are valid, legacy `-`/`-=` ops error, and empty `≔` payloads now delete ranges.
- Removed legacy `HL_EDIT_SEP`, `$HSEP$`, and `hsep` plumbing, adopting raw payload lines.
- Updated execution, input, renderer, and streaming flows to use `HL_FILE_PREFIX` and `HL_OP_CHARS` helpers.
- Updated session-stats parsing to `§`/`≔`/`"`/`"` format, patch envelopes, and bumped parser versions.
- Removed the hashline-separator benchmark script and its PI_HL_SEP job orchestration.
2026-05-22 17:46:53 +09:00

150 lines
5.5 KiB
TypeScript

import { beforeAll, describe, expect, it } from "bun:test";
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { editToolRenderer } from "@oh-my-pi/pi-coding-agent/edit/renderer";
import { ToolExecutionComponent } from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution";
import * as themeModule from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import type { TUI } from "@oh-my-pi/pi-tui";
beforeAll(async () => {
resetSettingsForTest();
await Settings.init({ inMemory: true, cwd: process.cwd() });
});
async function getUiTheme() {
await themeModule.initTheme(false, undefined, undefined, "dark", "light");
const theme = await themeModule.getThemeByName("dark");
expect(theme).toBeDefined();
return theme!;
}
describe("editToolRenderer", () => {
it("shows the target path from partial JSON while edit args stream", async () => {
const uiTheme = await getUiTheme();
const component = editToolRenderer.renderCall(
{
edits: [{}],
__partialJson: '{"edits":[{"path":"packages/coding-agent/src/edit/renderer.ts","old_text":"before',
},
{ expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "replace" } },
uiTheme,
);
const rendered = Bun.stripANSI(component.render(160).join("\n"));
expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts");
});
it("uses hashline input headers for streaming call path without apply_patch errors", async () => {
const uiTheme = await getUiTheme();
const component = editToolRenderer.renderCall(
{
input: "§packages/coding-agent/src/edit/renderer.ts\n»EOF\n// preview",
},
{ expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } },
uiTheme,
);
const rendered = Bun.stripANSI(component.render(160).join("\n"));
expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts");
expect(rendered).not.toContain("The first line of the patch must be");
});
it("shows hashline envelope input while preview diff is not computable yet", async () => {
await getUiTheme();
const uiStub = { requestRender() {} } as unknown as TUI;
const hashlineTool = { name: "edit", label: "Edit", mode: "hashline" } as unknown as AgentTool;
const component = new ToolExecutionComponent(
"edit",
{
input: ["*** Begin Patch", "§crates/pi-natives/src/shell.rs", "»EOF", "pub fn streaming_preview() {"].join(
"\n",
),
},
{},
hashlineTool,
uiStub,
);
const rendered = Bun.stripANSI(component.render(160).join("\n"));
expect(rendered).toContain("crates/pi-natives/src/shell.rs");
expect(rendered).toContain("»EOF");
expect(rendered).toContain("pub fn streaming_preview() {");
expect(rendered).not.toContain("*** Begin Patch");
});
it("recognizes compact and quoted hashline input headers", async () => {
const uiTheme = await getUiTheme();
const compactComponent = editToolRenderer.renderCall(
{
input: "§foo bar.ts\n»BOF\n// preview",
},
{ expanded: true, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } },
uiTheme,
);
const quotedComponent = editToolRenderer.renderCall(
{
input: "§'baz qux.ts'\n»BOF\n// preview",
},
{ expanded: false, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } },
uiTheme,
);
const compactRendered = Bun.stripANSI(compactComponent.render(160).join("\n"));
const quotedRendered = Bun.stripANSI(quotedComponent.render(160).join("\n"));
expect(compactRendered).toContain("foo bar.ts");
expect(quotedRendered).toContain("baz qux.ts");
});
it("strips canonical `§` and longer `§` runs from hashline input headers", async () => {
const uiTheme = await getUiTheme();
// Canonical `§PATH` form — the parser strips the marker and the
// renderer keeps the title clean.
const canonical = editToolRenderer.renderCall(
{
input: "§packages/coding-agent/src/slash-commands/builtin-registry.ts\n»BOF\n// preview",
},
{ expanded: true, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } },
uiTheme,
);
// Even longer runs should still produce the clean path.
const triple = editToolRenderer.renderCall(
{ input: "§§§a/b/c.ts\n»BOF\n// preview" },
{ expanded: true, isPartial: true, spinnerFrame: 0, renderContext: { editMode: "hashline" } },
uiTheme,
);
const canonicalRendered = Bun.stripANSI(canonical.render(160).join("\n"));
const tripleRendered = Bun.stripANSI(triple.render(160).join("\n"));
expect(canonicalRendered).toContain("packages/coding-agent/src/slash-commands/builtin-registry.ts");
expect(canonicalRendered).not.toMatch(/§packages\/coding-agent/);
expect(tripleRendered).toContain("a/b/c.ts");
expect(tripleRendered).not.toMatch(/§+a\/b\/c\.ts/);
});
it("uses hashline input headers for completed single-file result path", async () => {
const uiTheme = await getUiTheme();
const component = editToolRenderer.renderResult(
{
content: [{ type: "text", text: "Updated packages/coding-agent/src/edit/renderer.ts" }],
details: {
diff: "+1|// preview",
op: "update",
},
},
{ expanded: false, isPartial: false, renderContext: { editMode: "hashline" } },
uiTheme,
{
input: "§packages/coding-agent/src/edit/renderer.ts\n»EOF\n// preview",
},
);
const rendered = Bun.stripANSI(component.render(160).join("\n"));
expect(rendered).toContain("packages/coding-agent/src/edit/renderer.ts");
expect(rendered).not.toContain(" …");
});
});