Files
harness-tools/extensions/pi-workflow/tool.test.ts
T

297 lines
7.9 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import type {
ExtensionAPI,
ExtensionContext,
} from "@earendil-works/pi-coding-agent";
import type { WorkflowCommand, WorkflowView } from "./engine.ts";
import {
compactWorkflowDetails,
registerPiWorkflowTool,
workflowCommandFromTool,
} from "./tool.ts";
function workflowView(overrides: Partial<WorkflowView> = {}): WorkflowView {
return {
runId: "run-1",
revision: 4,
status: "waiting",
workspace: {
state: "ready",
sourceRoot: "/repo",
root: "/repo/.worktrees/run-1",
branch: "feature/search",
baseHead: "abc123",
},
uiImpactEvidence: [],
affectedUiSurfaces: [],
stages: [],
assignments: [],
mutatorProfileNames: [],
reviewCycles: 0,
verificationCycles: 0,
...overrides,
background: overrides.background ?? false,
};
}
test("tool commands derive active tools for feature and analysis scripts", () => {
assert.deepEqual(
workflowCommandFromTool(
{
action: "start",
request: "Build search",
featureName: "search-page",
idempotencyKey: "feature-key",
workflow: {
mode: "feature-script",
source: "return null",
args: '{"scope":"routes"}',
insertionPoint: "before-design",
},
},
["read", "grep", "bash"],
),
{
action: "start",
request: "Build search",
featureName: "search-page",
idempotencyKey: "feature-key",
script: {
source: "return null",
args: { scope: "routes" },
insertionPoint: "before-design",
activeToolNames: ["read", "grep", "bash"],
},
},
);
const analysis = workflowCommandFromTool(
{
action: "start",
request: "Audit persistence",
workflow: {
mode: "analysis-script",
source: "return await assign('review', 'Inspect')",
},
idempotencyKey: "analysis-key",
},
["read", "ls"],
);
assert.equal(analysis.action, "start-analysis");
assert.deepEqual(
analysis.action === "start-analysis" ? analysis.activeToolNames : [],
["read", "ls"],
);
});
test("tool commands reject open or incomplete action shapes", () => {
assert.throws(
() =>
workflowCommandFromTool(
{
action: "start",
request: "Build search",
featureName: "search-page",
idempotencyKey: "feature-key",
workflow: { mode: "feature-script", source: "return null" },
},
["read"],
),
/requires workflow.source and workflow.insertionPoint/,
);
assert.throws(
() =>
workflowCommandFromTool(
{
action: "decide",
runId: "run-1",
gateId: "gate-1",
expectedRevision: 2,
idempotencyKey: "decision-key",
decision: undefined,
},
["read"],
),
/decision is required/,
);
});
test("compact tool details expose only valid continuation intents", () => {
const details = compactWorkflowDetails(
"inspect",
workflowView({
openGate: {
id: "gate-1",
kind: "implementation-context",
status: "open",
revision: 4,
openedAt: 1,
},
planArtifact: { path: "/state/plan.md", revision: 2, digest: "digest" },
implementationContext: {
gateId: "gate-1",
runRevision: 3,
planRevision: 2,
preparationId: "prep-1",
activeParentSessionId: "session-1",
strategy: "fresh-session",
status: "handoff-pending",
method: "fresh-session",
},
}),
);
assert.deepEqual(details.continuation, {
command: "/workflow continue",
runId: "run-1",
});
assert.deepEqual(details.openGate?.payload, {
preparationId: "prep-1",
planRevision: 2,
contextRevision: 3,
strategy: "fresh-session",
status: "handoff-pending",
method: "fresh-session",
outcome: undefined,
error: undefined,
});
assert.equal(
compactWorkflowDetails(
"inspect",
workflowView({
openGate: {
id: "gate-1",
kind: "implementation-context",
status: "open",
revision: 4,
openedAt: 1,
},
planArtifact: { path: "/state/plan.md", revision: 2, digest: "digest" },
implementationContext: {
gateId: "gate-1",
runRevision: 3,
planRevision: 2,
preparationId: "prep-1",
activeParentSessionId: "session-1",
strategy: "fresh-session",
status: "handoff-pending",
method: "fresh-session",
},
}),
"session-2",
).continuation,
undefined,
);
assert.equal(
compactWorkflowDetails("inspect", workflowView()).continuation,
undefined,
);
});
test("tool cancellation aborts its active Workflow Run before rejecting", async () => {
let definition: Parameters<ExtensionAPI["registerTool"]>[0] | undefined;
const commands: WorkflowCommand[] = [];
let finishDecision = (_view: WorkflowView) => {};
const decision = new Promise<WorkflowView>((resolve) => {
finishDecision = resolve;
});
const pi = {
registerTool(candidate: Parameters<ExtensionAPI["registerTool"]>[0]) {
definition = candidate;
},
getActiveTools: () => ["read"],
} as unknown as ExtensionAPI;
registerPiWorkflowTool(pi, {
store: {
async loadRun() {
return { runId: "run-1", revision: 4, status: "running" };
},
} as never,
async engineForContext() {
return {
async execute(command: WorkflowCommand) {
commands.push(structuredClone(command));
if (command.action === "abort") {
const aborted = workflowView({ status: "aborted", revision: 5 });
finishDecision(aborted);
return aborted;
}
return decision;
},
} as never;
},
});
assert.ok(definition);
const controller = new AbortController();
const operation = definition.execute(
"cancel-call",
{
action: "decide",
runId: "run-1",
gateId: "gate-1",
expectedRevision: 4,
idempotencyKey: "decision-key",
decision: { kind: "recover", resolution: "inspect" },
},
controller.signal,
undefined,
{
sessionManager: { getSessionId: () => "session-1" },
} as unknown as ExtensionContext,
);
controller.abort(new Error("cancelled by test"));
await assert.rejects(operation, /cancelled by test/);
assert.equal(commands.some((command) => command.action === "abort"), true);
});
test("registered tool executes through the context engine and injects host tools", async () => {
let definition: Parameters<ExtensionAPI["registerTool"]>[0] | undefined;
const commands: WorkflowCommand[] = [];
const pi = {
registerTool(candidate: Parameters<ExtensionAPI["registerTool"]>[0]) {
definition = candidate;
},
getActiveTools() {
return ["read", "smart_compact"];
},
} as unknown as ExtensionAPI;
registerPiWorkflowTool(pi, {
store: {} as never,
async engineForContext() {
return {
async execute(command: WorkflowCommand) {
commands.push(structuredClone(command));
return workflowView();
},
} as never;
},
});
assert.equal(definition?.name, "pi_workflow");
assert.ok(definition);
await definition.execute(
"call-1",
{
action: "decide",
runId: "run-1",
gateId: "gate-1",
expectedRevision: 4,
idempotencyKey: "context-report",
decision: {
kind: "report-context-preparation",
preparationId: "prep-1",
planRevision: 2,
method: "smart_compact",
outcome: "succeeded",
},
},
new AbortController().signal,
undefined,
{
sessionManager: { getSessionId: () => "session-1" },
} as unknown as ExtensionContext,
);
assert.deepEqual(
commands[0]?.action === "decide" ? commands[0].activeToolNames : undefined,
["read", "smart_compact"],
);
});