mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
646 lines
18 KiB
TypeScript
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"]);
|
|
});
|
|
});
|
|
});
|