Files
my-pi/pi-permission-system/test/presentation/dialog-renderer.test.ts

646 lines
18 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
completeViewBudget,
type DialogBudget,
renderPromptDialog,
} from "#src/presentation/dialog-renderer";
import { makePromptPayload } from "#test/helpers/prompt-details-fixtures";
const WIDE = completeViewBudget(200);
/** The rendered lines for a payload built from the shared structural fixture. */
function render(
overrides: Parameters<typeof makePromptPayload>[0],
): readonly string[] {
return renderPromptDialog(makePromptPayload(overrides), WIDE).lines;
}
/** A payload whose request facts override the fixture's defaults. */
function requestFacts(
overrides: Partial<ReturnType<typeof makePromptPayload>["request"]>,
) {
return { ...makePromptPayload().request, ...overrides };
}
describe("renderPromptDialog", () => {
describe("the invariant core", () => {
it("renders a bash ask as one aligned fact per line", () => {
expect(
render({
kind: "bash",
request: requestFacts({
requester: {
agentName: "scout",
forwarded: false,
sessionId: null,
},
surface: "bash",
toolName: "bash",
value: "cat /etc/hosts",
matchedPattern: "*",
}),
}),
).toEqual([
"agent : scout",
"tool : bash",
"rule : *",
"command : cat /etc/hosts",
]);
});
it("names the gate surface when it differs from the tool", () => {
expect(
render({
kind: "external_directory",
request: requestFacts({
surface: "external_directory",
toolName: "write",
value: "/etc/hosts",
matchedPattern: "*",
}),
}),
).toEqual([
"tool : write",
"surface : external_directory",
"rule : *",
"path : /etc/hosts",
]);
});
it("omits an agent line for an unnamed local requester, and a surface the value label already names", () => {
expect(
render({
kind: "path",
request: requestFacts({
surface: "path",
toolName: "read",
value: "/tmp/x",
matchedPattern: null,
}),
}),
).toEqual(["tool : read", "path : /tmp/x"]);
});
it("omits the value line when the tool line already carries it", () => {
expect(
render({
kind: "tool",
request: requestFacts({
surface: "fetch",
toolName: "fetch",
value: "fetch",
matchedPattern: "*",
}),
}),
).toEqual(["tool : fetch", "rule : *"]);
});
it("labels an MCP ask's value as the target", () => {
expect(
render({
kind: "mcp",
request: requestFacts({
surface: "mcp__github__create_issue",
toolName: "mcp__github__create_issue",
value: "github:create_issue",
matchedPattern: "mcp__github__*",
}),
}),
).toEqual([
"tool : mcp__github__create_issue",
"rule : mcp__github__*",
"target : github:create_issue",
]);
});
it("renders a skill ask without repeating the surface", () => {
expect(
render({
kind: "skill",
request: requestFacts({
surface: "skill",
toolName: null,
value: "deploy",
matchedPattern: null,
}),
}),
).toEqual(["skill : deploy"]);
});
it("renders the executed unit of an unstrippable wrapper", () => {
expect(
render({
kind: "bash",
request: requestFacts({
surface: "bash",
toolName: "bash",
value: "xargs grep foo",
matchedPattern: "<indirection-bash-wrapper>",
executedUnit: "grep foo",
}),
}),
).toEqual([
"tool : bash",
"rule : <indirection-bash-wrapper>",
"command : xargs grep foo",
"runs : grep foo",
]);
});
it("names the nested context the offending unit ran in", () => {
expect(
render({
kind: "bash",
request: requestFacts({
surface: "bash",
toolName: "bash",
value: "rm -rf /tmp/x",
matchedPattern: "rm *",
commandContext: "command_substitution",
}),
}),
).toEqual([
"tool : bash",
"rule : rm *",
"command : rm -rf /tmp/x",
"context : command substitution",
]);
});
it("names the invoked tool when a shell alias re-exposes bash", () => {
expect(
render({
kind: "bash",
request: requestFacts({
surface: "bash",
toolName: "bash",
invokedToolName: "exec_command",
value: "ls",
matchedPattern: "*",
}),
}),
).toEqual([
"tool : bash (invoked as exec_command)",
"rule : *",
"command : ls",
]);
});
});
describe("evidence", () => {
it("renders each entry under the core, sharing the label column", () => {
expect(
render({
kind: "bash",
request: requestFacts({
surface: "bash",
toolName: "bash",
value: "rm -rf build",
matchedPattern: "rm *",
}),
evidence: [
{
label: "full command",
text: "npm run clean && rm -rf build",
detail: null,
},
],
}),
).toEqual([
"tool : bash",
"rule : rm *",
"command : rm -rf build",
"full command : npm run clean && rm -rf build",
]);
});
it("keeps an entry's detail on the entry's own line", () => {
expect(
render({
kind: "bash_external_directory",
request: requestFacts({
surface: "external_directory",
toolName: "bash",
value: "cat /etc/hosts /tmp/x",
matchedPattern: "*",
}),
evidence: [
{ label: "working directory", text: "/repo", detail: null },
{
label: "external path",
text: "/etc/hosts",
detail: "/private/etc/hosts",
},
{ label: "external path", text: "/tmp/x", detail: null },
],
}),
).toEqual([
"tool : bash",
"surface : external_directory",
"rule : *",
"command : cat /etc/hosts /tmp/x",
"working directory : /repo",
"external path : /etc/hosts → /private/etc/hosts",
"external path : /tmp/x",
]);
});
it("renders a skill read's path", () => {
expect(
render({
kind: "skill_read",
request: requestFacts({
surface: "skill",
toolName: null,
value: "deploy",
matchedPattern: null,
}),
evidence: [
{
label: "read path",
text: "/skills/deploy/SKILL.md",
detail: null,
},
],
}),
).toEqual(["skill : deploy", "read path : /skills/deploy/SKILL.md"]);
});
});
describe("the per-field width cap", () => {
const NARROW: DialogBudget = {
maxRows: Number.POSITIVE_INFINITY,
fieldMaxWidth: 12,
width: 200,
};
/** A bash ask whose command is the pathological field. */
function bashAsk(command: string, fullCommand?: string) {
return makePromptPayload({
kind: "bash",
request: {
...makePromptPayload().request,
toolName: "bash",
surface: "bash",
value: command,
matchedPattern: "*",
},
evidence:
fullCommand === undefined
? []
: [{ label: "full command", text: fullCommand, detail: null }],
});
}
it("clips a core field and marks it, without stating a count", () => {
const view = renderPromptDialog(
bashAsk("echo the quick brown fox"),
NARROW,
);
expect(view.lines).toEqual([
"tool : bash",
"rule : *",
"command : echo the qui…",
]);
expect(view.elided).toBe(true);
});
it("clips an evidence field the same way", () => {
const view = renderPromptDialog(
bashAsk("echo hi", "echo hi && echo there"),
NARROW,
);
expect(view.lines).toEqual([
"tool : bash",
"rule : *",
"command : echo hi",
"full command : echo hi && e…",
]);
expect(view.elided).toBe(true);
});
it("reproduces the field verbatim under the complete view", () => {
const view = renderPromptDialog(
bashAsk("echo the quick brown fox"),
completeViewBudget(200),
);
expect(view.lines).toContain("command : echo the quick brown fox");
expect(view.elided).toBe(false);
});
it("indents a multi-line field under its label", () => {
const view = renderPromptDialog(
bashAsk("cat <<'EOF'\nfirst line\nsecond line\nEOF"),
completeViewBudget(200),
);
expect(view.lines).toEqual([
"tool : bash",
"rule : *",
"command : cat <<'EOF'",
" first line",
" second line",
" EOF",
]);
});
});
describe("the row budget", () => {
/** A tool ask with `count` evidence entries, each one row wide. */
function askWithEvidence(count: number) {
return makePromptPayload({
kind: "tool",
request: {
...makePromptPayload().request,
surface: "fetch",
toolName: "fetch",
value: "fetch",
matchedPattern: "*",
},
evidence: Array.from({ length: count }, (_, index) => ({
label: "input",
text: `entry ${index}`,
detail: null,
})),
});
}
it("drops evidence that does not fit and marks the drop", () => {
const view = renderPromptDialog(askWithEvidence(6), {
maxRows: 5,
fieldMaxWidth: 200,
width: 200,
});
expect(view.lines).toEqual([
"tool : fetch",
"rule : *",
"input : entry 0",
"input : entry 1",
"…",
]);
expect(view.elided).toBe(true);
});
it("marks nothing when every entry fits", () => {
const view = renderPromptDialog(askWithEvidence(2), {
maxRows: 5,
fieldMaxWidth: 200,
width: 200,
});
expect(view.lines).toHaveLength(4);
expect(view.elided).toBe(false);
});
it("counts rows after wrapping to the width", () => {
const payload = makePromptPayload({
kind: "tool",
request: {
...makePromptPayload().request,
surface: "fetch",
toolName: "fetch",
value: "fetch",
matchedPattern: "*",
},
evidence: [{ label: "input", text: "a".repeat(60), detail: null }],
});
// The same entry is one row at a wide width and four once wrapped, so
// the identical budget admits it only in the first case.
expect(
renderPromptDialog(payload, {
maxRows: 5,
fieldMaxWidth: 200,
width: 200,
}),
).toEqual({
lines: ["tool : fetch", "rule : *", `input : ${"a".repeat(60)}`],
elided: false,
});
const wrapped = renderPromptDialog(payload, {
maxRows: 5,
fieldMaxWidth: 200,
width: 20,
});
expect(wrapped.lines).toEqual(["tool : fetch", "rule : *", "…"]);
expect(wrapped.elided).toBe(true);
});
it("renders the whole core when it alone exceeds the budget", () => {
const view = renderPromptDialog(askWithEvidence(3), {
maxRows: 1,
fieldMaxWidth: 200,
width: 200,
});
expect(view.lines).toEqual(["tool : fetch", "rule : *"]);
expect(view.elided).toBe(true);
});
it("bounds the reported forwarded here-string ask (#710)", () => {
const body = Array.from(
{ length: 200 },
() => "- a finding line about some module in the codebase",
).join("\n");
const command = `@'\n${body}\n'@ | Out-File -FilePath report.md`;
const budget = { maxRows: 24, fieldMaxWidth: 400, width: 120 };
const view = renderPromptDialog(
makePromptPayload({
kind: "forwarded",
request: {
...makePromptPayload().request,
requester: {
agentName: "scout",
forwarded: true,
sessionId: "abc123",
},
surface: "bash",
toolName: null,
value: command,
matchedPattern: null,
},
evidence: [
{
label: "requested",
text: `Subagent 'scout' requested bash command '${command}'.`,
detail: null,
},
],
}),
budget,
);
expect(view.lines.length).toBeLessThanOrEqual(budget.maxRows);
expect(view.lines[0]).toBe("subagent : scout · session abc123");
expect(view.lines[1]).toBe("surface : bash");
expect(view.lines[2]).toBe("command : @'");
expect(view.elided).toBe(true);
});
it("bounds the same forwarded here-string ask carrying the child's own payload (#745)", () => {
// The same ask, arriving as the child built it rather than as a relayed
// sentence: `kind: "bash"` with the child's real evidence entries. A
// different input to the same budget, so the row bound is measured here
// rather than inferred from the `kind: "forwarded"` pin above.
const body = Array.from(
{ length: 200 },
() => "- a finding line about some module in the codebase",
).join("\n");
const command = `@'\n${body}\n'@ | Out-File -FilePath report.md`;
const budget = { maxRows: 24, fieldMaxWidth: 400, width: 120 };
const view = renderPromptDialog(
makePromptPayload({
kind: "bash",
request: {
...makePromptPayload().request,
requester: {
agentName: "scout",
forwarded: true,
sessionId: "abc123",
},
surface: "bash",
toolName: "bash",
value: command,
matchedPattern: "*",
},
evidence: [
{ label: "full command", text: command, detail: null },
{ label: "working directory", text: "/repo", detail: null },
],
}),
budget,
);
expect(view.lines.length).toBeLessThanOrEqual(budget.maxRows);
expect(view.elided).toBe(true);
});
});
describe("highlighting the flagged element", () => {
/** A visible stand-in for the theme's warning colour. */
const mark = (text: string) => `[${text}]`;
it("paints the flagged value and its whole-token occurrences in evidence", () => {
expect(
renderPromptDialog(
makePromptPayload({
kind: "bash",
request: {
...makePromptPayload().request,
surface: "bash",
toolName: "bash",
value: "ls",
matchedPattern: "*",
},
evidence: [
{
label: "full command",
text: "lsof | ls && /usr/bin/lsblk",
detail: null,
},
],
}),
completeViewBudget(200),
mark,
).lines,
).toEqual([
"tool : bash",
"rule : *",
"command : [ls]",
"full command : lsof | [ls] && /usr/bin/lsblk",
]);
});
it("paints each escaping path rather than the command that referenced them", () => {
expect(
renderPromptDialog(
makePromptPayload({
kind: "bash_external_directory",
request: {
...makePromptPayload().request,
surface: "external_directory",
toolName: "bash",
value: "cat /etc/hosts /etc/hostsbackup",
matchedPattern: "*",
},
evidence: [
{ label: "working directory", text: "/repo", detail: null },
{ label: "external path", text: "/etc/hosts", detail: null },
],
}),
completeViewBudget(200),
mark,
).lines,
).toEqual([
"tool : bash",
"surface : external_directory",
"rule : *",
"command : cat [/etc/hosts] /etc/hostsbackup",
"working directory : /repo",
"external path : [/etc/hosts]",
]);
});
it("leaves the text untouched when no paint is supplied", () => {
expect(
render({
kind: "bash",
request: requestFacts({
surface: "bash",
toolName: "bash",
value: "ls",
matchedPattern: "*",
}),
}),
).toEqual(["tool : bash", "rule : *", "command : ls"]);
});
});
describe("a forwarded ask", () => {
it("names the requesting subagent and its session", () => {
expect(
render({
kind: "forwarded",
request: requestFacts({
requester: {
agentName: "scout",
forwarded: true,
sessionId: "abc123",
},
surface: "bash",
toolName: null,
value: "cat /etc/hosts",
matchedPattern: null,
}),
}),
).toEqual([
"subagent : scout · session abc123",
"surface : bash",
"command : cat /etc/hosts",
]);
});
it("renders a version-skewed request that names neither agent nor session", () => {
expect(
render({
kind: "forwarded",
request: requestFacts({
requester: { agentName: "", forwarded: true, sessionId: "" },
surface: "read",
toolName: null,
value: "/tmp/x",
matchedPattern: null,
}),
}),
).toEqual(["subagent : unknown", "surface : read", "value : /tmp/x"]);
});
});
});