mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
301 lines
8.7 KiB
TypeScript
301 lines
8.7 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { findEvidence } from "#src/presentation/prompt-payload";
|
|
import {
|
|
buildToolAskPayload,
|
|
type ToolAskFacts,
|
|
} from "#src/presentation/tool-ask-payload";
|
|
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
|
import type { PermissionCheckResult } from "#src/types";
|
|
import {
|
|
makePermissionCheckResult,
|
|
makeToolPreviewFormatter,
|
|
} from "#test/helpers/presentation-fixtures";
|
|
|
|
function makeFormatter(lookup?: ToolInputFormatterLookup) {
|
|
return makeToolPreviewFormatter({}, lookup);
|
|
}
|
|
|
|
function makeMcpLookup(preview: string): ToolInputFormatterLookup {
|
|
return { get: (name) => (name === "mcp" ? () => preview : undefined) };
|
|
}
|
|
|
|
function toolResult(
|
|
toolName: string,
|
|
overrides: Partial<PermissionCheckResult> = {},
|
|
): PermissionCheckResult {
|
|
return makePermissionCheckResult(toolName, overrides);
|
|
}
|
|
|
|
function mcpResult(
|
|
target: string,
|
|
overrides: Partial<PermissionCheckResult> = {},
|
|
): PermissionCheckResult {
|
|
return makePermissionCheckResult("mcp", { target, ...overrides });
|
|
}
|
|
|
|
/** Build a payload the way the per-tool gate does, defaulting the surface. */
|
|
function buildPayload(
|
|
facts: Omit<ToolAskFacts, "surface" | "agentName"> & {
|
|
surface?: string;
|
|
agentName?: string | null;
|
|
},
|
|
) {
|
|
return buildToolAskPayload({
|
|
agentName: null,
|
|
surface: facts.check.toolName,
|
|
...facts,
|
|
});
|
|
}
|
|
|
|
describe("buildToolAskPayload", () => {
|
|
describe("the invariant core", () => {
|
|
test("carries the gate surface, matched rule, and offending command", () => {
|
|
const payload = buildPayload({
|
|
check: toolResult("bash", {
|
|
command: "rm -rf foo",
|
|
matchedPattern: "rm *",
|
|
commandContext: "command_substitution",
|
|
}),
|
|
surface: "bash",
|
|
});
|
|
|
|
expect(payload.kind).toBe("bash");
|
|
expect(payload.request).toEqual({
|
|
requester: { agentName: null, forwarded: false, sessionId: null },
|
|
surface: "bash",
|
|
toolName: "bash",
|
|
invokedToolName: null,
|
|
value: "rm -rf foo",
|
|
matchedPattern: "rm *",
|
|
commandContext: "command_substitution",
|
|
executedUnit: null,
|
|
});
|
|
});
|
|
|
|
test("carries the executed unit of a wrapper (#713)", () => {
|
|
const payload = buildPayload({
|
|
check: toolResult("bash", {
|
|
command: "xargs grep foo",
|
|
matchedPattern: "<indirection-bash-wrapper>",
|
|
executedUnit: "grep foo",
|
|
}),
|
|
surface: "bash",
|
|
});
|
|
|
|
expect(payload.request.executedUnit).toBe("grep foo");
|
|
});
|
|
|
|
test("names the invoked tool when a shell alias re-exposes bash (#574)", () => {
|
|
const payload = buildPayload({
|
|
check: toolResult("bash", { command: "ls" }),
|
|
surface: "bash",
|
|
invokedToolName: "exec_command",
|
|
});
|
|
|
|
expect(payload.request.toolName).toBe("bash");
|
|
expect(payload.request.invokedToolName).toBe("exec_command");
|
|
});
|
|
|
|
test("omits the invoked tool when it repeats the gated one", () => {
|
|
const payload = buildPayload({
|
|
check: toolResult("read"),
|
|
invokedToolName: "read",
|
|
});
|
|
|
|
expect(payload.request.invokedToolName).toBeNull();
|
|
});
|
|
|
|
test("leaves the value empty for a bash check with no resolved command", () => {
|
|
expect(buildPayload({ check: toolResult("bash") }).request.value).toBe(
|
|
"",
|
|
);
|
|
});
|
|
|
|
test("lands the annotations slot empty", () => {
|
|
expect(buildPayload({ check: toolResult("read") }).annotations).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("the bash evidence", () => {
|
|
test("names the agent when one is known", () => {
|
|
expect(
|
|
buildPayload({
|
|
check: toolResult("read"),
|
|
agentName: "my-agent",
|
|
input: { path: "/src" },
|
|
formatter: makeFormatter(),
|
|
}).request.requester.agentName,
|
|
).toBe("my-agent");
|
|
});
|
|
|
|
test("carries the enclosing command when the gated unit is only part of it", () => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: toolResult("bash", { command: "rm -rf ." }),
|
|
input: { command: 'echo "hello" && rm -rf .' },
|
|
formatter: makeFormatter(),
|
|
}),
|
|
"full command",
|
|
),
|
|
).toEqual({
|
|
label: "full command",
|
|
text: 'echo "hello" && rm -rf .',
|
|
detail: null,
|
|
});
|
|
});
|
|
|
|
test("omits the enclosing command when it is the gated unit", () => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: toolResult("bash", { command: "git push" }),
|
|
input: { command: "git push" },
|
|
formatter: makeFormatter(),
|
|
}),
|
|
"full command",
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
test.each([
|
|
["input is undefined", undefined],
|
|
["input has no command field", { unrelated: "value" }],
|
|
["input command is empty", { command: "" }],
|
|
])("omits the enclosing command when %s", (_case, input) => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: toolResult("bash", { command: "git push" }),
|
|
input,
|
|
formatter: makeFormatter(),
|
|
}),
|
|
"full command",
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
test("adds no input preview, since the command is the value", () => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: toolResult("bash", { command: "git status" }),
|
|
input: { command: "git status" },
|
|
formatter: makeFormatter(),
|
|
}),
|
|
"input",
|
|
),
|
|
).toBeUndefined();
|
|
});
|
|
test("carries a registered shell alias preview as authorization evidence", () => {
|
|
const formatter = makeFormatter({
|
|
get: (name) =>
|
|
name === "ssh_bash"
|
|
? () => "SSH target 'packaging-server' in remote cwd '/srv/build'"
|
|
: undefined,
|
|
});
|
|
const payload = buildPayload({
|
|
check: toolResult("bash", { command: "rm -rf dist" }),
|
|
surface: "bash",
|
|
invokedToolName: "ssh_bash",
|
|
input: { command: "rm -rf dist" },
|
|
formatter,
|
|
});
|
|
|
|
expect(findEvidence(payload, "input")).toEqual({
|
|
label: "input",
|
|
text: "SSH target 'packaging-server' in remote cwd '/srv/build'",
|
|
detail: null,
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe("mcp", () => {
|
|
test("makes the target the decision-relevant value", () => {
|
|
const payload = buildPayload({
|
|
check: mcpResult("server:query", { matchedPattern: "server:*" }),
|
|
formatter: makeFormatter(),
|
|
});
|
|
|
|
expect(payload.kind).toBe("mcp");
|
|
expect(payload.request.value).toBe("server:query");
|
|
expect(payload.request.matchedPattern).toBe("server:*");
|
|
});
|
|
|
|
test("carries the argument summary a registered formatter produced", () => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: mcpResult("exa:search"),
|
|
input: { tool: "exa:search", arguments: { query: "typescript" } },
|
|
formatter: makeFormatter(makeMcpLookup('with query: "typescript"')),
|
|
}),
|
|
"input",
|
|
),
|
|
).toEqual({
|
|
label: "input",
|
|
text: 'with query: "typescript"',
|
|
detail: null,
|
|
});
|
|
});
|
|
|
|
test("carries no evidence when the registered formatter declines", () => {
|
|
const noArgsLookup: ToolInputFormatterLookup = {
|
|
get: (name) => (name === "mcp" ? () => undefined : undefined),
|
|
};
|
|
|
|
expect(
|
|
buildPayload({
|
|
check: mcpResult("exa:search"),
|
|
input: { tool: "exa:search" },
|
|
formatter: makeFormatter(noArgsLookup),
|
|
}).evidence,
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("carries no evidence when no formatter is provided", () => {
|
|
expect(
|
|
buildPayload({
|
|
check: mcpResult("exa:search"),
|
|
input: { tool: "exa:search", arguments: { query: "test" } },
|
|
}).evidence,
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("generic tools", () => {
|
|
test("carries the real input preview as evidence", () => {
|
|
expect(
|
|
findEvidence(
|
|
buildPayload({
|
|
check: toolResult("read"),
|
|
input: { path: "/src/foo.ts" },
|
|
formatter: makeFormatter(),
|
|
}),
|
|
"input",
|
|
)?.text,
|
|
).toContain("path '/src/foo.ts'");
|
|
});
|
|
|
|
test("carries no evidence when the formatter produces nothing", () => {
|
|
expect(
|
|
buildPayload({
|
|
check: toolResult("task"),
|
|
input: {},
|
|
formatter: makeFormatter(),
|
|
}).evidence,
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("carries no evidence when no formatter is provided", () => {
|
|
expect(
|
|
buildPayload({ check: toolResult("task"), input: { path: "/src" } })
|
|
.evidence,
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
});
|