202 lines
6.8 KiB
TypeScript
Vendored
202 lines
6.8 KiB
TypeScript
Vendored
import { strict as assert } from "node:assert";
|
|
import { test } from "vitest";
|
|
import { buildAiAgentStepItems, toolCallStepKey, upsertAgentStep, type AiAgentStepItem } from "../../apps/desktop/src/lib/ai/aiAgentStepPresentation.ts";
|
|
import type { AiAgentPlan } from "../../apps/desktop/src/lib/ai/aiAgentPlan.ts";
|
|
|
|
test("presents auto-execute agent plans as completed generation, safety, and execution steps", () => {
|
|
const plan: AiAgentPlan = {
|
|
executableSql: "SELECT count(*) FROM users",
|
|
handoffSql: "SELECT count(*) FROM users",
|
|
steps: [
|
|
{ kind: "generate_sql", status: "done", sql: "SELECT count(*) FROM users" },
|
|
{
|
|
kind: "risk_check",
|
|
status: "done",
|
|
action: "auto_execute",
|
|
environment: "non_production",
|
|
category: "read",
|
|
reasons: [],
|
|
},
|
|
{ kind: "execute_sql", status: "pending", sql: "SELECT count(*) FROM users" },
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(buildAiAgentStepItems(plan), [
|
|
{ key: "generated", labelKey: "ai.agentSteps.generated", tone: "success" },
|
|
{
|
|
key: "safe",
|
|
labelKey: "ai.agentSteps.safe",
|
|
titleKey: "ai.agentStepTitles.riskCheck",
|
|
titleParams: { action: "auto_execute", category: "read", environment: "non_production", reasons: "-" },
|
|
tone: "success",
|
|
},
|
|
{ key: "autoExecute", labelKey: "ai.agentSteps.autoExecute", tone: "active" },
|
|
]);
|
|
});
|
|
|
|
test("presents blocked and confirmation plans with warning tones", () => {
|
|
const blocked: AiAgentPlan = {
|
|
handoffSql: "DROP TABLE users",
|
|
steps: [
|
|
{ kind: "generate_sql", status: "done", sql: "DROP TABLE users" },
|
|
{
|
|
kind: "risk_check",
|
|
status: "done",
|
|
action: "block",
|
|
environment: "non_production",
|
|
category: "dangerous",
|
|
reasons: [],
|
|
},
|
|
{ kind: "execute_sql", status: "skipped", reason: "blocked_by_policy" },
|
|
],
|
|
};
|
|
const confirm: AiAgentPlan = {
|
|
handoffSql: "INSERT INTO users(name) VALUES ('a')",
|
|
steps: [
|
|
{ kind: "generate_sql", status: "done", sql: "INSERT INTO users(name) VALUES ('a')" },
|
|
{
|
|
kind: "risk_check",
|
|
status: "done",
|
|
action: "confirm",
|
|
environment: "production",
|
|
category: "low_risk_write",
|
|
reasons: [],
|
|
},
|
|
{ kind: "execute_sql", status: "skipped", reason: "requires_confirmation" },
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
buildAiAgentStepItems(blocked).map((item) => [item.labelKey, item.tone, item.titleKey, item.titleParams]),
|
|
[
|
|
["ai.agentSteps.generated", "success", undefined, undefined],
|
|
["ai.agentSteps.blocked", "danger", "ai.agentStepTitles.riskCheck", { action: "block", category: "dangerous", environment: "non_production", reasons: "-" }],
|
|
["ai.agentSteps.skipped", "muted", "ai.agentStepTitles.blocked", undefined],
|
|
],
|
|
);
|
|
assert.deepEqual(
|
|
buildAiAgentStepItems(confirm).map((item) => [item.labelKey, item.tone, item.titleKey, item.titleParams]),
|
|
[
|
|
["ai.agentSteps.generated", "success", undefined, undefined],
|
|
["ai.agentSteps.needsConfirm", "warning", "ai.agentStepTitles.riskCheck", { action: "confirm", category: "low_risk_write", environment: "production", reasons: "-" }],
|
|
["ai.agentSteps.skipped", "muted", "ai.agentStepTitles.requiresConfirmation", undefined],
|
|
],
|
|
);
|
|
});
|
|
|
|
test("includes risk reasons in risk check titles", () => {
|
|
const plan: AiAgentPlan = {
|
|
handoffSql: "INSERT INTO users(name) VALUES ('a'); UPDATE users SET name='b' WHERE id=1",
|
|
steps: [
|
|
{
|
|
kind: "risk_check",
|
|
status: "done",
|
|
action: "confirm",
|
|
environment: "non_production",
|
|
category: "write",
|
|
reasons: ["multi_statement"],
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(buildAiAgentStepItems(plan)[0], {
|
|
key: "needsConfirm",
|
|
labelKey: "ai.agentSteps.needsConfirm",
|
|
titleKey: "ai.agentStepTitles.riskCheck",
|
|
titleParams: {
|
|
action: "confirm",
|
|
category: "write",
|
|
environment: "non_production",
|
|
reasons: "multi_statement",
|
|
},
|
|
tone: "warning",
|
|
});
|
|
});
|
|
|
|
test("presents no-sql and no-intent plans as muted skipped states", () => {
|
|
const noSql: AiAgentPlan = {
|
|
steps: [
|
|
{ kind: "generate_sql", status: "skipped", reason: "no_sql" },
|
|
{ kind: "execute_sql", status: "skipped", reason: "no_sql" },
|
|
],
|
|
};
|
|
const noIntent: AiAgentPlan = {
|
|
steps: [
|
|
{ kind: "generate_sql", status: "done", sql: "SELECT count(*) FROM users" },
|
|
{ kind: "execute_sql", status: "skipped", reason: "no_execution_intent" },
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
buildAiAgentStepItems(noSql).map((item) => item.labelKey),
|
|
["ai.agentSteps.noSql", "ai.agentSteps.skipped"],
|
|
);
|
|
assert.deepEqual(
|
|
buildAiAgentStepItems(noIntent).map((item) => item.labelKey),
|
|
["ai.agentSteps.generated", "ai.agentSteps.notRequested"],
|
|
);
|
|
});
|
|
|
|
test("uses stable tool step keys only for trustworthy tool call ids", () => {
|
|
assert.equal(toolCallStepKey("call-1", 1, "tool_call_start"), "tool-call-1");
|
|
assert.equal(toolCallStepKey("call-1", 2, "tool_call_end"), "tool-call-1");
|
|
assert.equal(toolCallStepKey("", 1, "tool_call_start"), "tool-tool_call_start-1");
|
|
assert.equal(toolCallStepKey("cli-tool-call", 2, "tool_call_end"), "tool-tool_call_end-2");
|
|
});
|
|
|
|
test("upserts tool call steps and preserves start args when final result arrives", () => {
|
|
const steps: AiAgentStepItem[] = [
|
|
{ key: "context", labelKey: "ai.agentSteps.contextCompacted", tone: "active" },
|
|
{
|
|
key: "tool-call-1",
|
|
labelKey: "ai.agentSteps.callingTool",
|
|
tone: "active",
|
|
toolName: "list_tables",
|
|
toolArgs: { schema: "public" },
|
|
},
|
|
];
|
|
|
|
upsertAgentStep(steps, {
|
|
key: "tool-call-1",
|
|
labelKey: "ai.agentSteps.toolDone",
|
|
tone: "success",
|
|
toolName: "list_tables",
|
|
toolResult: "users\norders",
|
|
isError: false,
|
|
});
|
|
|
|
assert.deepEqual(steps, [
|
|
{ key: "context", labelKey: "ai.agentSteps.contextCompacted", tone: "active" },
|
|
{
|
|
key: "tool-call-1",
|
|
labelKey: "ai.agentSteps.toolDone",
|
|
tone: "success",
|
|
toolName: "list_tables",
|
|
toolArgs: { schema: "public" },
|
|
toolResult: "users\norders",
|
|
isError: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("upserts with splice so array identity and ordering are preserved", () => {
|
|
const steps: AiAgentStepItem[] = [
|
|
{ key: "before", labelKey: "before", tone: "muted" },
|
|
{ key: "tool-call-1", labelKey: "ai.agentSteps.callingTool", tone: "active" },
|
|
{ key: "after", labelKey: "after", tone: "muted" },
|
|
];
|
|
const sameArray = steps;
|
|
|
|
upsertAgentStep(steps, { key: "tool-call-1", labelKey: "ai.agentSteps.toolError", tone: "danger", isError: true });
|
|
|
|
assert.equal(steps, sameArray);
|
|
assert.deepEqual(
|
|
steps.map((step) => [step.key, step.labelKey, step.tone]),
|
|
[
|
|
["before", "before", "muted"],
|
|
["tool-call-1", "ai.agentSteps.toolError", "danger"],
|
|
["after", "after", "muted"],
|
|
],
|
|
);
|
|
});
|