import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stripVTControlCharacters } from "node:util";
import type {
AgentToolResult,
ExtensionAPI,
Theme,
ToolDefinition,
} from "@earendil-works/pi-coding-agent";
import {
createEventBus,
initTheme,
ToolExecutionComponent,
} from "@earendil-works/pi-coding-agent";
import { Box, type TUI, visibleWidth } from "@earendil-works/pi-tui";
import { parse } from "yaml";
import { createToolRenderContext } from "../../../test/support/tool-render-context.ts";
import { CHILD_AGENT_PROCESS_ENV } from "../../shared/child-agent-environment.ts";
import { SUBAGENT_WORKFLOW_IDS_ENV } from "../../shared/subagent-environment.ts";
import { createToolPresentationRegistry } from "../run-subagent/tool-rendering.ts";
import workflowExtension from "./index.ts";
import { WORKFLOW_STATE_JOURNAL_VERSION } from "./workflow.ts";
const PLAIN_THEME = {
bold: (value: string) => value,
fg: (_color: string, value: string) => value,
} as Theme;
const MARKED_THEME = {
bold: (value: string) => `${value}`,
fg: (color: string, value: string) => `<${color}>${value}${color}>`,
} as Theme;
/** Matches Pi's standard summary for wrapped lines hidden in collapsed mode. */
const HIDDEN_LINE_HINT =
/^\.\.\. \(\d+ more lines?, \d+ total, .* to expand\)$/;
/** Matches the collapsed hint for six single-line stage attributes. */
const GOAL_STAGE_HINT = /^\.\.\. \(2 more lines, 6 total,/;
/** Matches the collapsed hint for five single-line stage attributes. */
const LEGACY_STAGE_HINT = /^\.\.\. \(1 more line, 5 total,/;
interface WorkflowRenderingFixture {
readonly api: ExtensionAPI;
readonly handlers: ReadonlyMap<
string,
readonly ((event: unknown, context: unknown) => unknown)[]
>;
readonly tools: readonly ToolDefinition[];
}
interface ExecutedTool {
readonly args: Record;
readonly definition: ToolDefinition;
readonly result: AgentToolResult;
readonly isError: boolean;
}
const temporaryDirectories: string[] = [];
const originalSuiteDirectory = process.env["PI_AGENT_SUITE_DIR"];
const originalChildMarker = process.env[CHILD_AGENT_PROCESS_ENV];
const originalChildWorkflowIds = process.env[SUBAGENT_WORKFLOW_IDS_ENV];
/** Creates one workflow catalog whose descriptions are the user-visible names. */
async function createWorkflowSuite(
workflowDescription = "Delivery process",
implementationDescription = "Implementation stage",
reviewDescription = "Review stage",
): Promise {
const root = await mkdtemp(join(tmpdir(), "pi-workflow-rendering-"));
temporaryDirectories.push(root);
const workflowDirectory = join(root, "workflow", "workflows");
await mkdir(workflowDirectory, { recursive: true });
await writeFile(
join(workflowDirectory, "delivery.yaml"),
`description: ${JSON.stringify(workflowDescription)}\nstages:\n - id: implementation\n description: ${JSON.stringify(implementationDescription)}\n prompt: Implement the change\n initial: true\n - id: review\n description: ${JSON.stringify(reviewDescription)}\n prompt: Review the change\n final: true\ntransitions:\n - from: implementation\n to: review\n type: advance\n`,
);
process.env["PI_AGENT_SUITE_DIR"] = root;
}
/** Creates the smallest Pi runtime that preserves lifecycle and presentation events. */
async function createFixture(
branch: readonly unknown[] = [],
): Promise {
const handlers = new Map<
string,
Array<(event: unknown, context: unknown) => unknown>
>();
const tools: ToolDefinition[] = [];
const activeTools: string[] = [];
let thinkingLevel = "medium";
const events = createEventBus();
const api = {
events,
on(
eventName: string,
handler: (event: unknown, context: unknown) => unknown,
) {
const registered = handlers.get(eventName) ?? [];
registered.push(handler);
handlers.set(eventName, registered);
},
registerTool(definition: ToolDefinition) {
tools.push(definition);
activeTools.push(definition.name);
},
registerFlag(): void {},
getFlag(): undefined {
return undefined;
},
getActiveTools() {
return [...activeTools];
},
setActiveTools(names: string[]) {
activeTools.splice(0, activeTools.length, ...names);
},
getThinkingLevel() {
return thinkingLevel;
},
setThinkingLevel(level: string) {
thinkingLevel = level;
},
appendEntry(): void {},
sendMessage(): void {},
} as unknown as ExtensionAPI;
const renderingModel = {
provider: "test",
id: "current",
api: "test-api",
baseUrl: "https://example.test",
reasoning: true,
name: "test/current",
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 100_000,
maxTokens: 8_192,
};
await workflowExtension(api);
for (const sessionStart of handlers.get("session_start") ?? []) {
await sessionStart({ type: "session_start" }, {
mode: "rpc",
hasUI: true,
model: renderingModel,
modelRegistry: {
find(provider: string, id: string) {
return provider === "test" && id === "current"
? renderingModel
: undefined;
},
},
sessionManager: {
getBranch: () => branch,
buildSessionProjection: () => ({ entries: [] }),
},
shutdown: () => {},
} as never);
}
return { api, handlers, tools };
}
/** Runs extension handlers in registration order and merges tool-result overrides. */
async function applyToolResultHandlers(
fixture: WorkflowRenderingFixture,
event: Record,
): Promise> {
let current = { ...event };
for (const handler of fixture.handlers.get("tool_result") ?? []) {
const override = await handler(current, {});
if (typeof override === "object" && override !== null) {
current = { ...current, ...override };
}
}
return {
content: current["content"] as AgentToolResult["content"],
details: current["details"],
};
}
/** Executes one workflow tool through the same start/result event boundaries as Pi. */
async function executeTool(
fixture: WorkflowRenderingFixture,
name:
| "workflow_activate"
| "workflow_get_stage"
| "workflow_edit_stage"
| "workflow_transition"
| "workflow_create"
| "workflow_exit",
args: Record,
): Promise {
const definition = fixture.tools.find((candidate) => candidate.name === name);
if (definition === undefined) {
throw new Error(`missing ${name} definition`);
}
const toolCallId = `${name}-call`;
for (const handler of fixture.handlers.get("tool_execution_start") ?? []) {
await handler(
{ type: "tool_execution_start", toolCallId, toolName: name, args },
{},
);
}
let baseResult: AgentToolResult;
let isError = false;
try {
baseResult = await definition.execute(
toolCallId,
args,
undefined,
undefined,
{
sessionManager: { buildSessionProjection: () => ({ entries: [] }) },
} as never,
);
} catch (error) {
isError = true;
baseResult = {
content: [
{
type: "text",
text: error instanceof Error ? error.message : String(error),
},
],
details: {},
};
}
const result = await applyToolResultHandlers(fixture, {
type: "tool_result",
toolCallId,
toolName: name,
input: args,
content: baseResult.content,
details: baseResult.details,
isError,
});
return { args, definition, result, isError };
}
/** Resolves the renderer used by the subagent session screen. */
function resolveSessionDefinition(
fixture: WorkflowRenderingFixture,
name:
| "workflow_activate"
| "workflow_get_stage"
| "workflow_edit_stage"
| "workflow_transition"
| "workflow_create"
| "workflow_exit",
): ToolDefinition {
const resolution = createToolPresentationRegistry(
"/tmp",
fixture.api.events,
).resolve(name);
if (
resolution.category !== "package" ||
resolution.definition === undefined
) {
throw new Error(`${name} did not resolve as package presentation`);
}
return resolution.definition;
}
/** Renders a completed tool row in the selected mode after restoring persisted evidence. */
function renderCompletedTool(
definition: ToolDefinition,
execution: ExecutedTool,
theme: Theme = PLAIN_THEME,
width = 100,
expanded = false,
): { readonly call: readonly string[]; readonly result: readonly string[] } {
if (
definition.renderCall === undefined ||
definition.renderResult === undefined
) {
throw new Error(`${definition.name} semantic renderers are missing`);
}
const context = createToolRenderContext({
args: execution.args,
expanded,
isError: execution.isError,
});
const call = definition.renderCall(execution.args, theme, context);
const result = definition.renderResult(
execution.result,
{ expanded, isPartial: false },
theme,
context,
);
return {
call: call.render(width).map((line) => line.trimEnd()),
result: result.render(width).map((line) => line.trimEnd()),
};
}
/** Builds one complete dynamic definition whose identity is visible before execution. */
function createArguments(
id: string,
description: string,
): Record {
return {
id,
description,
prompt: "Follow delivery rules",
stages: [
{
id: "implementation",
description: "Implementation stage",
goal: "The implementation passes its checks",
prompt: "Implement the change",
model: { thinking: "medium" },
initial: true,
},
{
id: "review",
description: "Review stage",
goal: "The reviewer has approved the change",
prompt: "Review the change",
model: { thinking: "medium" },
final: true,
},
],
transitions: [{ from: "implementation", to: "review", type: "advance" }],
};
}
/** Initializes theme and isolates main-agent fixtures from ambient child policy. */
beforeEach(() => {
delete process.env[CHILD_AGENT_PROCESS_ENV];
delete process.env[SUBAGENT_WORKFLOW_IDS_ENV];
initTheme(undefined, false);
});
afterEach(async () => {
if (originalSuiteDirectory === undefined) {
delete process.env["PI_AGENT_SUITE_DIR"];
} else {
process.env["PI_AGENT_SUITE_DIR"] = originalSuiteDirectory;
}
if (originalChildMarker === undefined) {
delete process.env[CHILD_AGENT_PROCESS_ENV];
} else {
process.env[CHILD_AGENT_PROCESS_ENV] = originalChildMarker;
}
if (originalChildWorkflowIds === undefined) {
delete process.env[SUBAGENT_WORKFLOW_IDS_ENV];
} else {
process.env[SUBAGENT_WORKFLOW_IDS_ENV] = originalChildWorkflowIds;
}
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
describe("workflow semantic tool rendering", () => {
test("renders streamed kind YAML consistently on both screens and expansion modes", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const main = fixture.tools.find(({ name }) => name === "workflow_create");
const child = resolveSessionDefinition(fixture, "workflow_create");
if (!main?.renderCall || !child.renderCall) {
throw new Error("missing create renderer");
}
for (const expanded of [false, true]) {
const args = { id: "kind-stream", kind: "review<&>" };
const lines = main
.renderCall(
args,
PLAIN_THEME,
createToolRenderContext({ args, expanded, isError: false }),
)
.render(100)
.map((line) => line.trimEnd());
expect(
child
.renderCall(
args,
PLAIN_THEME,
createToolRenderContext({ args, expanded, isError: false }),
)
.render(100)
.map((line) => line.trimEnd()),
).toEqual(lines);
const marker = expanded ? "--- Content ---" : "Content:";
expect(
parse(lines.slice(lines.indexOf(marker) + 1).join("\n")).kind,
).toBe(args.kind);
}
});
test.each([
true,
false,
])("renders streamed nested=%s as YAML on both screens", async (nested) => {
await createWorkflowSuite();
const fixture = await createFixture();
const active = fixture.tools.find(({ name }) => name === "workflow_create");
if (!active?.renderCall) {
throw new Error("missing create renderer");
}
const child = resolveSessionDefinition(fixture, "workflow_create");
if (!child.renderCall) {
throw new Error("missing child create renderer");
}
for (const expanded of [false, true]) {
const args = { id: "nested-stream", nested };
const context = createToolRenderContext({
args,
expanded,
isError: false,
});
const mainCall = active.renderCall(args, PLAIN_THEME, context);
const childCall = child.renderCall(
args,
PLAIN_THEME,
createToolRenderContext({ args, expanded, isError: false }),
);
const lines = mainCall.render(100).map((line) => line.trimEnd());
expect(childCall.render(100).map((line) => line.trimEnd())).toEqual(
lines,
);
const marker = expanded ? "--- Content ---" : "Content:";
const parsed = parse(lines.slice(lines.indexOf(marker) + 1).join("\n"));
expect(parsed.nested).toBe(nested);
for (const width of [18, 40]) {
const shell = new Box(1, 1);
shell.addChild(mainCall);
for (const line of shell.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
}
});
/** Keeps unconditional and legacy evidence limited to transition endpoints. */
test.each(
[false, true].flatMap((legacy) =>
[false, true].flatMap((replayed) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({
legacy,
replayed,
subagent,
expanded,
})),
),
),
),
)("omits unconditional transition ground %j", async ({
legacy,
replayed,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const workflow = createArguments("unconditional", "Unconditional");
if (legacy) {
for (const stage of workflow["stages"] as Record[]) {
delete stage["goal"];
}
}
const fixture = await createFixture(
legacy
? [
{
type: "custom",
customType: "workflow-state",
data: {
kind: "created",
workflow,
route: ["implementation"],
journalVersion: WORKFLOW_STATE_JOURNAL_VERSION,
},
},
]
: [],
);
if (!legacy) {
await executeTool(fixture, "workflow_create", workflow);
}
const transition = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
expect(transition.isError).toBe(false);
const screen = replayed ? await createFixture() : fixture;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_transition")
: screen.tools.find(({ name }) => name === "workflow_transition");
const component = new ToolExecutionComponent(
"workflow_transition",
"unconditional",
transition.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.setArgsComplete();
component.setExpanded(expanded);
component.updateResult({
...JSON.parse(JSON.stringify(transition.result)),
isError: false,
});
const lines = component
.render(120)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toEqual(
expanded
? [
"workflow_transition · unconditional",
"--- From ---",
"implementation · Implementation stage",
"--- To ---",
"review · Review stage",
]
: [
"workflow_transition · unconditional",
"From: implementation · Implementation stage",
"To: review · Review stage",
],
);
});
/** Shows the selected edge condition on the first live or replay Pi render. */
test.each(
[false, true].flatMap((replayed) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({ replayed, subagent, expanded })),
),
),
)("renders selected transition ground %j", async ({
replayed,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const fixture = await createFixture();
const workflow = createArguments("ground-choice", "Ground choice");
const selected = { id: "shared", when: "A finding needs uncovered units." };
workflow["transitions"] = [
{
from: "implementation",
to: "review",
type: "advance",
grounds: [
{ id: "shared", when: "An unrelated edge condition." },
{ id: "other", when: "Another advance condition." },
],
},
{
from: "review",
to: "implementation",
type: "rework",
grounds: [
{ id: "first", when: "A different rework condition." },
{ ...selected, when: ` ${selected.when}\n ` },
],
},
];
await executeTool(fixture, "workflow_create", workflow);
await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
ground: "other",
});
const args = {
basis: "The selected condition holds.",
stageId: "implementation",
ground: selected.id,
};
const tool = fixture.tools.find(
({ name }) => name === "workflow_transition",
);
if (tool?.renderCall === undefined || tool.renderResult === undefined) {
throw new Error("missing transition renderers");
}
const context = createToolRenderContext({ args, expanded, isError: false });
const call = tool.renderCall(args, PLAIN_THEME, context);
const groundRow = expanded
? `${selected.id} · ${selected.when}`
: `Ground: ${selected.when}`;
expect(call.render(120).map((line) => line.trimEnd())).toContain(groundRow);
const transition = await executeTool(fixture, "workflow_transition", args);
tool.renderResult(
transition.result,
{ expanded, isPartial: false },
PLAIN_THEME,
context,
);
expect(call.render(120).map((line) => line.trimEnd())).toContain(groundRow);
expect(transition.isError).toBe(false);
const hooked = renderCompletedTool(
transition.definition,
transition,
PLAIN_THEME,
120,
expanded,
);
expect(hooked.call).toContain(
expanded
? `${selected.id} · ${selected.when}`
: `Ground: ${selected.when}`,
);
// Replace the active definition to prove rendering uses captured evidence.
await executeTool(
fixture,
"workflow_create",
createArguments("replacement", "Replacement"),
);
const screen = replayed ? await createFixture() : fixture;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_transition")
: screen.tools.find(({ name }) => name === "workflow_transition");
const component = new ToolExecutionComponent(
"workflow_transition",
"ground-choice",
transition.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
component.updateResult({
...JSON.parse(JSON.stringify(transition.result)),
isError: false,
});
for (const width of [8, 22, 120]) {
for (const line of component.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
const lines = component
.render(120)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toEqual(
expanded
? [
"workflow_transition · ground-choice",
"--- From ---",
"review · Review stage",
"--- To ---",
"implementation · Implementation stage",
"--- Ground ---",
`${selected.id} · ${selected.when}`,
]
: [
"workflow_transition · ground-choice",
"From: review · Review stage",
"To: implementation · Implementation stage",
`Ground: ${selected.when}`,
],
);
expect(transition.result.details).toMatchObject({
from: { id: "review" },
to: { id: "implementation" },
ground: selected,
});
});
/** Shows the sole ground of a transition whose call names no ground. */
test.each([
false,
true,
])("renders the sole transition ground when the call omits it, expanded %p", async (expanded) => {
await createWorkflowSuite();
const fixture = await createFixture();
const workflow = createArguments("sole-ground", "Sole ground");
const sole = { id: "ready", when: "The implementation is ready." };
workflow["transitions"] = [
{
from: "implementation",
to: "review",
type: "advance",
grounds: [sole],
},
];
await executeTool(fixture, "workflow_create", workflow);
const transition = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
expect(transition.isError).toBe(false);
expect(transition.result.details).toMatchObject({ ground: sole });
const hooked = renderCompletedTool(
transition.definition,
transition,
PLAIN_THEME,
120,
expanded,
);
expect(hooked.call).toContain(
expanded ? `${sole.id} · ${sole.when}` : `Ground: ${sole.when}`,
);
});
/**
* Proves that the rows of a bound transition and of its exit show the nested workflow and the outcome.
* Input and expected output: review is bound to check; the request shows check, and exits show allowed or refused.
* Edge case: the same rows appear in compact and expanded mode from saved evidence.
*/
test("renders the nested workflow of a bound transition and the outcome of its exit", async () => {
await createWorkflowSuite();
const directory = join(
process.env["PI_AGENT_SUITE_DIR"] ?? "",
"workflow",
"workflows",
);
await writeFile(
join(directory, "bound.yaml"),
"version: 3\ndescription: Bound process\nprompt: Follow bound rules\nstages:\n - id: implementation\n description: Implementation stage\n goal: Implemented\n prompt: Implement\n initial: true\n - id: review\n description: Review stage\n goal: Reviewed\n prompt: Review\n final: true\ntransitions:\n - from: implementation\n to: review\n type: advance\n workflow: check\n",
);
await writeFile(
join(directory, "check.yaml"),
"version: 3\nnested: true\ndescription: Check process\nprompt: Follow check rules\nstages:\n - id: start\n description: Start stage\n goal: Started\n prompt: Start\n initial: true\n - id: done\n description: Done stage\n goal: Done\n prompt: Finish\n final: true\ntransitions:\n - from: start\n to: done\n type: advance\n",
);
const fixture = await createFixture();
await executeTool(fixture, "workflow_activate", { workflowId: "bound" });
const render = (execution: ExecutedTool, expanded: boolean) =>
renderCompletedTool(
execution.definition,
execution,
PLAIN_THEME,
120,
expanded,
).call.filter(Boolean);
const request = { basis: "Implemented.", stageId: "review" };
const transition = await executeTool(
fixture,
"workflow_transition",
request,
);
expect(render(transition, false)).toEqual([
"workflow_transition · bound",
"From: implementation · Implementation stage",
"To: review · Review stage",
"Nested workflow: check · Check process",
]);
expect(render(transition, true)).toEqual([
"workflow_transition · bound",
"--- From ---",
"implementation · Implementation stage",
"--- To ---",
"review · Review stage",
"--- Nested workflow ---",
"check · Check process",
]);
const refusal = await executeTool(fixture, "workflow_exit", {
basis: "Not ready.",
allow: false,
});
expect(render(refusal, false)).toEqual([
"workflow_exit · check",
"Check process",
"Transition: review · refused",
]);
await executeTool(fixture, "workflow_transition", request);
const permission = await executeTool(fixture, "workflow_exit", {
basis: "Ready.",
allow: true,
});
expect(render(permission, true)).toEqual([
"workflow_exit · check",
"Check process",
"--- Transition ---",
"review · allowed",
]);
});
test("renders explicit exit identically in main and subagent screens after state removal", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
await executeTool(fixture, "workflow_activate", { workflowId: "delivery" });
const execution = await executeTool(fixture, "workflow_exit", {
basis: "The task is complete.",
});
const active = renderCompletedTool(execution.definition, execution);
const child = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_exit"),
execution,
);
expect(active.call).toEqual([
"workflow_exit · delivery",
"Delivery process",
]);
expect(active.result).toEqual([]);
expect(child).toEqual(active);
expect(execution.result.details).toMatchObject({
toolName: "workflow_exit",
workflow: { id: "delivery" },
});
for (const expanded of [false, true]) {
for (const width of [12, 40]) {
const context = createToolRenderContext({
args: execution.args,
expanded,
isError: false,
});
execution.definition.renderResult?.(
execution.result,
{ expanded, isPartial: false },
PLAIN_THEME,
context,
);
const call = execution.definition.renderCall?.(
execution.args,
PLAIN_THEME,
context,
);
if (call === undefined) {
throw new Error("missing exit renderer");
}
const shell = new Box(1, 1);
shell.addChild(call);
for (const line of shell.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
}
});
/** Proves activation is semantic and its internal success payload stays hidden. */
test("renders activation identically in the active and subagent session screens", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const execution = await executeTool(fixture, "workflow_activate", {
workflowId: "delivery",
});
const active = renderCompletedTool(execution.definition, execution);
const session = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_activate"),
execution,
);
expect(active).toEqual({
call: ["workflow_activate · delivery", "Delivery process"],
result: [],
});
expect(session).toEqual(active);
expect(execution.result.content).toEqual([
{ type: "text", text: '{"success":true}' },
]);
});
/**
* Proves streaming arguments cannot freeze an empty workflow reference in renderer state.
* Input and expected output: an incomplete empty ID followed by complete Coding renders workflow_activate · Coding.
* Edge case: both render calls reuse the same tool-row state, matching Pi streaming behavior.
* Dependencies: Pi ToolRenderContext.argsComplete and the workflow activation renderer.
*/
test("waits for complete activation arguments before caching identity", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const definition = fixture.tools.find(
({ name }) => name === "workflow_activate",
);
if (definition?.renderCall === undefined) {
throw new Error("workflow_activate renderer missing");
}
const context = createToolRenderContext({
args: { workflowId: "" },
expanded: false,
isError: false,
});
context.argsComplete = false;
const partial = definition
.renderCall({ workflowId: "" }, PLAIN_THEME, context)
.render(80);
expect(partial).toEqual(["workflow_activate"]);
context.argsComplete = true;
const completed = definition
.renderCall({ workflowId: "Coding" }, PLAIN_THEME, context)
.render(80)
.map((line) => line.trimEnd());
expect(completed).toEqual(["workflow_activate · Coding"]);
});
/**
* Proves partial workflow fields render as they arrive without persisting incomplete identity.
* Input and expected output: successive incomplete arguments add YAML and the initial stage.
* Edge case: YAML with at most three visual lines has no hidden-content hint.
* Dependencies: current render arguments, shared renderer state, and session reconstruction.
*/
test("streams bounded workflow creation YAML in active and subagent screens", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const activeDefinition = fixture.tools.find(
({ name }) => name === "workflow_create",
);
const sessionDefinition = resolveSessionDefinition(
fixture,
"workflow_create",
);
if (
activeDefinition?.renderCall === undefined ||
sessionDefinition.renderCall === undefined
) {
throw new Error("workflow_create renderer missing");
}
const activeRenderCall = activeDefinition.renderCall;
const sessionRenderCall = sessionDefinition.renderCall;
const activeContext = createToolRenderContext({
args: {},
expanded: false,
isError: false,
});
const sessionContext = createToolRenderContext({
args: {},
expanded: false,
isError: false,
});
activeContext.argsComplete = false;
sessionContext.argsComplete = false;
const renderBoth = (args: Record) => {
const active = activeRenderCall(args, PLAIN_THEME, activeContext)
.render(80)
.map((line) => line.trimEnd());
const session = sessionRenderCall(args, PLAIN_THEME, sessionContext)
.render(80)
.map((line) => line.trimEnd());
expect(session).toEqual(active);
return active;
};
expect(renderBoth({})).toEqual(["workflow_create"]);
expect(
renderBoth({
id: "streaming-delivery",
description: "Streaming delivery process",
}),
).toEqual([
"workflow_create",
"Workflow: streaming-delivery · Streaming delivery process",
"Content:",
"description: Streaming delivery process",
]);
const partialArgs = createArguments(
"streaming-delivery",
"Streaming delivery process",
);
const collapsed = renderBoth(partialArgs);
expect(collapsed.slice(0, 4)).toEqual([
"workflow_create",
"Workflow: streaming-delivery · Streaming delivery process",
"Stage: implementation · Implementation stage",
"Content:",
]);
expect(collapsed.slice(4, -1)).toHaveLength(3);
expect(collapsed.at(-1)).toMatch(HIDDEN_LINE_HINT);
activeContext.expanded = true;
sessionContext.expanded = true;
const expanded = renderBoth(partialArgs);
const contentIndex = expanded.indexOf("--- Content ---");
const content = parse(expanded.slice(contentIndex + 1).join("\n"));
expect(content["description"]).toBe(partialArgs["description"]);
expect(typeof content["prompt"]).toBe("string");
expect(Array.isArray(content["stages"])).toBe(true);
expect(Array.isArray(content["transitions"])).toBe(true);
expect(content).not.toHaveProperty("id");
});
/**
* Proves creation shows complete semantic content in active and reconstructed session rendering.
* Input and expected output: collapsed calls show bounded YAML; expanded calls show catalog-shaped YAML.
* Edge case: an exact catalog collision retains the submitted identity and YAML preview.
* Dependencies: presentation events, package registry reconstruction, Pi keybindings, and create tool execution.
*/
test("renders workflow creation before and after execution", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const created = await executeTool(
fixture,
"workflow_create",
createArguments("dynamic-delivery", "Dynamic delivery process"),
);
const activeCreated = renderCompletedTool(created.definition, created);
const sessionCreated = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_create"),
created,
);
expect(activeCreated.call.slice(0, 4)).toEqual([
"workflow_create",
"Workflow: dynamic-delivery · Dynamic delivery process",
"Stage: implementation · Implementation stage",
"Content:",
]);
expect(activeCreated.call.slice(4, -1)).toHaveLength(3);
expect(activeCreated.call.at(-1)).toMatch(HIDDEN_LINE_HINT);
expect(activeCreated.result).toEqual([]);
expect(sessionCreated).toEqual(activeCreated);
const activeExpanded = renderCompletedTool(
created.definition,
created,
PLAIN_THEME,
100,
true,
);
const sessionExpanded = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_create"),
created,
PLAIN_THEME,
100,
true,
);
expect(activeExpanded.result).toEqual([]);
expect(activeExpanded.call.slice(0, 6)).toEqual([
"workflow_create",
"--- Workflow ---",
"dynamic-delivery · Dynamic delivery process",
"--- Stage ---",
"implementation · Implementation stage",
"--- Content ---",
]);
const expandedContent = parse(activeExpanded.call.slice(6).join("\n"));
expect(expandedContent.description).toBe("Dynamic delivery process");
expect(typeof expandedContent.prompt).toBe("string");
expect(Array.isArray(expandedContent.stages)).toBe(true);
expect(Array.isArray(expandedContent.transitions)).toBe(true);
expect(expandedContent).not.toHaveProperty("id");
expect(sessionExpanded).toEqual(activeExpanded);
const rejected = await executeTool(
fixture,
"workflow_create",
createArguments("delivery", "Conflicting delivery process"),
);
expect(rejected.isError).toBe(true);
const activeRejected = renderCompletedTool(rejected.definition, rejected);
const sessionRejected = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_create"),
rejected,
);
expect(activeRejected.call.slice(0, 4)).toEqual([
"workflow_create",
"Workflow: delivery · Conflicting delivery process",
"Stage: implementation · Implementation stage",
"Content:",
]);
expect(activeRejected.call.slice(4, -1)).toHaveLength(3);
expect(activeRejected.call.at(-1)).toMatch(HIDDEN_LINE_HINT);
expect(activeRejected.result.join("\n")).toContain("Error:");
expect(sessionRejected).toEqual(activeRejected);
});
/**
* Proves compact reference and content labels use semantic title styling.
* Input and expected output: creation and transition calls style labels separately from values.
* Edge case: YAML remains tool output below the styled Content label.
* Dependencies: marked theme rendering and width-aware labeled text.
*/
test("styles compact workflow labels separately from values", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const creation = await executeTool(
fixture,
"workflow_create",
createArguments("styled-delivery", "Styled delivery process"),
);
const transition = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const renderedCreation = renderCompletedTool(
creation.definition,
creation,
MARKED_THEME,
);
const renderedTransition = renderCompletedTool(
transition.definition,
transition,
MARKED_THEME,
);
const expandedCreation = renderCompletedTool(
creation.definition,
creation,
MARKED_THEME,
100,
true,
);
expect(renderedCreation.call.slice(0, 5)).toEqual([
"workflow_create",
"Workflow: styled-delivery · Styled delivery process",
"Stage: implementation · Implementation stage",
"Content:",
"description: Styled delivery process",
]);
expect(renderedTransition.call).toEqual([
"workflow_transition · styled-delivery",
"From: implementation · Implementation stage",
"To: review · Review stage",
]);
expect(expandedCreation.call.slice(0, 6)).toEqual([
"workflow_create",
"--- Workflow ---",
"styled-delivery · Styled delivery process",
"--- Stage ---",
"implementation · Implementation stage",
"--- Content ---",
]);
});
/**
* Proves stage inspection and editing use the same semantic rows in active and reconstructed session screens.
* Input and expected output: get shows the saved stage; edit shows replacement fields in collapsed and expanded modes.
* Edge case: successful TUI result rows hide model-visible get JSON and internal edit success JSON.
* Dependencies: presentation events, package registry reconstruction, dynamic workflow state, and YAML rendering.
*/
test("renders stage get and edit identically in active and subagent screens", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
await executeTool(
fixture,
"workflow_create",
createArguments("dynamic-delivery", "Dynamic delivery process"),
);
const getDefinition = fixture.tools.find(
({ name }) => name === "workflow_get_stage",
);
const renderCall = getDefinition?.renderCall;
if (renderCall === undefined) {
throw new Error("stage inspection renderer is missing");
}
const liveRows = [false, true].map((expanded) =>
renderCall(
{ stageId: "implementation" },
PLAIN_THEME,
createToolRenderContext({
args: { stageId: "implementation" },
expanded,
isError: false,
}),
)
.render(expanded ? 100 : 500)
.map((line) => line.trimEnd()),
);
const get = await executeTool(fixture, "workflow_get_stage", {
stageId: "implementation",
});
const edit = await executeTool(fixture, "workflow_edit_stage", {
stageId: "implementation",
goal: "The implementation passes its checks",
description: "Revised implementation",
prompt: "Follow the corrected implementation requirements.",
model: { thinking: "high" },
});
const getCollapsed = renderCompletedTool(
get.definition,
get,
PLAIN_THEME,
500,
);
const getSessionCollapsed = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_get_stage"),
get,
PLAIN_THEME,
500,
);
expect(getSessionCollapsed).toEqual(getCollapsed);
expect(getCollapsed.call.slice(0, -1)).toEqual([
"workflow_get_stage: implementation",
"Description: Implementation stage",
"Goal: The implementation passes its checks",
expect.stringContaining("Prompt:"),
"Thinking: medium",
]);
expect(getCollapsed.call.at(-1)).toMatch(HIDDEN_LINE_HINT);
expect(getCollapsed.result).toEqual([]);
const getExpanded = renderCompletedTool(
get.definition,
get,
PLAIN_THEME,
100,
true,
);
expect(
renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_get_stage"),
get,
PLAIN_THEME,
100,
true,
),
).toEqual(getExpanded);
expect([getCollapsed.call, getExpanded.call]).toEqual(liveRows);
expect(getExpanded).toEqual({
call: [
"workflow_get_stage: implementation",
"--- Description ---",
"Implementation stage",
"--- Goal ---",
"The implementation passes its checks",
"--- Prompt ---",
expect.any(String),
"--- Thinking ---",
"medium",
"--- Initial ---",
"true",
"--- Final ---",
"false",
],
result: [],
});
const editCollapsed = renderCompletedTool(
edit.definition,
edit,
PLAIN_THEME,
500,
);
expect(
renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_edit_stage"),
edit,
PLAIN_THEME,
500,
),
).toEqual(editCollapsed);
expect(editCollapsed).toEqual({
call: [
"workflow_edit_stage: implementation",
"Description: Implementation stage -> Revised implementation",
"Prompt: Implement the change -> Follow the corrected implementation requirements.",
"Thinking: medium -> high",
],
result: [],
});
const styledEdit = renderCompletedTool(
edit.definition,
edit,
MARKED_THEME,
500,
);
expect(styledEdit.call[1]).toBe(
"Description: Implementation stage -> Revised implementation",
);
expect(styledEdit.call[2]).toContain(
"Prompt:",
);
const editExpanded = renderCompletedTool(
edit.definition,
edit,
PLAIN_THEME,
100,
true,
);
expect(
renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_edit_stage"),
edit,
PLAIN_THEME,
100,
true,
),
).toEqual(editExpanded);
expect(editExpanded).toEqual({
call: [
"workflow_edit_stage: implementation",
"--- Description ---",
"Implementation stage",
"->",
"Revised implementation",
"--- Prompt ---",
"Implement the change",
"->",
"Follow the corrected implementation requirements.",
"--- Thinking ---",
"medium",
"->",
"high",
],
result: [],
});
const styledExpandedEdit = renderCompletedTool(
edit.definition,
edit,
MARKED_THEME,
100,
true,
);
expect(styledExpandedEdit.call[1]).toBe(
"--- Description ---",
);
expect(styledExpandedEdit.call[3]).toBe("->");
const unchanged = await executeTool(fixture, "workflow_edit_stage", {
stageId: "implementation",
goal: "The implementation passes its checks",
description: "Revised implementation",
prompt: "Follow the corrected implementation requirements.",
model: { thinking: "high" },
});
expect(
renderCompletedTool(
unchanged.definition,
unchanged,
PLAIN_THEME,
100,
true,
),
).toEqual({
call: ["workflow_edit_stage: implementation", "No changes."],
result: [],
});
expect(get.result.content).toHaveLength(1);
const stageResult = get.result.content[0];
if (stageResult?.type !== "text") {
throw new Error("stage inspection must return text JSON");
}
expect(JSON.parse(stageResult.text)).toMatchObject({
id: "implementation",
description: "Implementation stage",
goal: "The implementation passes its checks",
model: { thinking: "medium" },
initial: true,
final: false,
});
expect(edit.result.content).toEqual([
{ type: "text", text: '{"success":true}' },
]);
});
/** Checks goal-only change rows in real Pi shells on the first live or replay render. */
test.each(
[false, true].flatMap((replayed) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({ replayed, subagent, expanded })),
),
),
)("renders first goal-only stage edit %j", async ({
replayed,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const fixture = await createFixture();
const args = createArguments("goal-edit", "Goal edit");
await executeTool(fixture, "workflow_create", args);
const stage = (args["stages"] as Record[])[0];
if (stage === undefined) {
throw new Error("missing stage fixture");
}
const replacement = {
stageId: "implementation",
description: stage["description"],
prompt: stage["prompt"],
model: stage["model"],
goal: " Revised outcome\n ",
};
const edit = await executeTool(fixture, "workflow_edit_stage", replacement);
expect(edit.isError).toBe(false);
expect(edit.result.details).toMatchObject({
editedContent: { goal: replacement.goal.trim() },
});
const screen = replayed ? await createFixture() : fixture;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_edit_stage")
: screen.tools.find(({ name }) => name === "workflow_edit_stage");
const component = new ToolExecutionComponent(
"workflow_edit_stage",
"goal-edit",
edit.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
component.updateResult({
...(replayed ? JSON.parse(JSON.stringify(edit.result)) : edit.result),
isError: false,
});
const lines = component
.render(100)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toEqual(
expanded
? [
"workflow_edit_stage: implementation",
"--- Goal ---",
String(stage["goal"]),
"->",
"Revised outcome",
]
: [
"workflow_edit_stage: implementation",
`Goal: ${String(stage["goal"])} -> Revised outcome`,
],
);
for (const line of component.render(100)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(100);
}
const unchanged = await executeTool(
fixture,
"workflow_edit_stage",
replacement,
);
expect(unchanged.isError).toBe(false);
expect(
renderCompletedTool(
unchanged.definition,
unchanged,
PLAIN_THEME,
100,
expanded,
).call,
).toEqual(["workflow_edit_stage: implementation", "No changes."]);
});
/** Checks stage attributes on the first render after result delivery. */
test.each(
[false, true].flatMap((format2) =>
[false, true].flatMap((replayed) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({
format2,
replayed,
subagent,
expanded,
})),
),
),
),
)("renders first stage inspection %j", async ({
format2,
replayed,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const workflow = createArguments("saved-delivery", "Saved delivery");
if (format2) {
workflow["version"] = 2;
} else {
for (const stage of workflow["stages"] as Record[]) {
delete stage["goal"];
}
}
const fixture = await createFixture([
{
type: "custom",
customType: "workflow-state",
data: {
kind: "created",
workflow,
route: ["implementation"],
journalVersion: WORKFLOW_STATE_JOURNAL_VERSION,
},
},
]);
const get = await executeTool(fixture, "workflow_get_stage", {
stageId: "implementation",
});
expect(get.isError).toBe(false);
// A replay screen has saved evidence but no active workflow to prime its call.
const screen = replayed ? await createFixture() : fixture;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_get_stage")
: screen.tools.find(({ name }) => name === "workflow_get_stage");
expect(definition).toBeDefined();
const component = new ToolExecutionComponent(
"workflow_get_stage",
"first-stage-inspection",
get.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
const result = replayed
? JSON.parse(JSON.stringify(get.result))
: get.result;
component.updateResult({ ...result, isError: false });
// Render immediately after result delivery, without another display update.
const lines = component
.render(100)
.map((line) => stripVTControlCharacters(line).trim());
expect(lines).toContain("workflow_get_stage: implementation");
expect(lines).toContain(
expanded ? "--- Description ---" : "Description: Implementation stage",
);
expect(
lines.some((line) => line.includes(expanded ? "--- Goal ---" : "Goal:")),
).toBe(format2);
if (format2) {
expect(lines).toContain(
expanded
? "The implementation passes its checks"
: "Goal: The implementation passes its checks",
);
}
if (expanded) {
expect(lines).toContain("--- Prompt ---");
expect(lines).toContain("--- Initial ---");
expect(lines).toContain("--- Final ---");
} else {
expect(
lines.some((line) =>
(format2 ? GOAL_STAGE_HINT : LEGACY_STAGE_HINT).test(line),
),
).toBe(true);
}
expect(lines.join("\n")).not.toContain('"id":"implementation"');
for (const line of component.render(100)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(100);
}
});
/** Checks absent thinking through restored execution and independent persisted evidence. */
test.each(
["live", "replay", "synthetic"].flatMap((mode) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({ mode, subagent, expanded })),
),
),
)("renders stage attributes without stored thinking %j", async ({
mode,
subagent,
expanded,
}) => {
const synthetic = mode === "synthetic";
await createWorkflowSuite();
const workflow = createArguments("optional-thinking", "Optional thinking");
for (const stage of workflow["stages"] as Record[]) {
delete stage["model"];
delete stage["goal"];
}
const source = await createFixture([
{
type: "custom",
customType: "workflow-state",
data: {
kind: "created",
workflow,
route: ["implementation"],
journalVersion: WORKFLOW_STATE_JOURNAL_VERSION,
},
},
]);
const execution = await executeTool(source, "workflow_get_stage", {
stageId: "implementation",
});
expect(execution.isError).toBe(false);
expect(
JSON.parse((execution.result.content[0] as { text: string }).text),
).toMatchObject({ model: {} });
const screen = mode === "live" ? source : await createFixture();
const result = synthetic
? {
content: [{ type: "text" as const, text: "{}" }],
details: {
presentationKind: "workflow-tool",
toolName: "workflow_get_stage",
stage: { id: "implementation" },
content: {
id: "implementation",
description: "Implementation stage",
goal: "Saved outcome",
prompt: "Saved instructions",
model: {},
initial: true,
final: false,
},
},
}
: execution.result;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_get_stage")
: screen.tools.find(({ name }) => name === "workflow_get_stage");
const component = new ToolExecutionComponent(
"workflow_get_stage",
"optional-thinking",
execution.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
component.updateResult({
...JSON.parse(JSON.stringify(result)),
isError: false,
});
const lines = component
.render(100)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toContain(
expanded ? "--- Description ---" : "Description: Implementation stage",
);
expect(
lines.some((line) =>
expanded ? line === "--- Prompt ---" : line.startsWith("Prompt:"),
),
).toBe(true);
expect(lines.some((line) => line.includes("Thinking"))).toBe(false);
expect(lines.join("\n")).not.toContain("undefined");
if (synthetic) {
expect(lines).toContain(
expanded ? "--- Goal ---" : "Goal: Saved outcome",
);
} else {
expect(lines.some((line) => line.includes("Goal"))).toBe(false);
}
if (expanded) {
expect(lines).toContain("--- Initial ---");
expect(lines).toContain("--- Final ---");
expect(lines).toContain("true");
expect(lines).toContain("false");
} else {
expect(lines).toContain("Initial: true");
expect(
lines.some((line) =>
synthetic ? LEGACY_STAGE_HINT.test(line) : line === "Final: false",
),
).toBe(true);
}
for (const width of [22, 100]) {
for (const line of component.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
});
/** Keeps known edit values when the saved stage has no thinking override. */
test.each(
[false, true].flatMap((replayed) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({ replayed, subagent, expanded })),
),
),
)("renders stage edit without prior thinking %j", async ({
replayed,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const workflow = createArguments(
"optional-edit-thinking",
"Optional edit thinking",
);
for (const stage of workflow["stages"] as Record[]) {
delete stage["model"];
delete stage["goal"];
}
const fixture = await createFixture([
{
type: "custom",
customType: "workflow-state",
data: {
kind: "created",
workflow,
route: ["implementation"],
journalVersion: WORKFLOW_STATE_JOURNAL_VERSION,
},
},
]);
const edit = await executeTool(fixture, "workflow_edit_stage", {
stageId: "implementation",
description: "Revised stage",
prompt: "Revised instructions",
model: { thinking: "high" },
});
expect(edit.isError).toBe(false);
const screen = replayed ? await createFixture() : fixture;
const definition = subagent
? resolveSessionDefinition(screen, "workflow_edit_stage")
: screen.tools.find(({ name }) => name === "workflow_edit_stage");
const component = new ToolExecutionComponent(
"workflow_edit_stage",
"optional-edit-thinking",
edit.args,
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
component.updateResult({
...JSON.parse(JSON.stringify(edit.result)),
isError: false,
});
const lines = component
.render(100)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toContain(
expanded
? "--- Description ---"
: "Description: Implementation stage -> Revised stage",
);
expect(
lines.some((line) =>
expanded ? line === "--- Prompt ---" : line.startsWith("Prompt:"),
),
).toBe(true);
expect(lines).toContain(expanded ? "--- Thinking ---" : "Thinking: high");
if (expanded) {
expect(lines).toContain("high");
}
expect(lines.join("\n")).not.toContain("undefined");
if (expanded) {
expect(lines.filter((line) => line === "->")).toHaveLength(2);
}
for (const width of [22, 100]) {
for (const line of component.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
});
/** Validates optional presentation thinking without weakening other saved attributes. */
test.each([
{ model: {}, accepted: true },
{ model: { thinking: "medium" }, accepted: true },
{ model: { thinking: "" }, accepted: false },
{ model: { thinking: null }, accepted: false },
{ model: { thinking: 1 }, accepted: false },
{ model: { thinking: false }, accepted: false },
{ model: { thinking: [] }, accepted: false },
{ model: null, accepted: false },
{ model: {}, description: "", accepted: false },
])("validates persisted stage attributes %j", async ({
model,
accepted,
description,
}) => {
await createWorkflowSuite();
const fixture = await createFixture();
const definition = resolveSessionDefinition(fixture, "workflow_get_stage");
const component = new ToolExecutionComponent(
"workflow_get_stage",
"validation",
{ stageId: "saved" },
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.setExpanded(true);
component.updateResult({
content: [],
isError: false,
details: {
presentationKind: "workflow-tool",
toolName: "workflow_get_stage",
stage: { id: "saved" },
content: {
id: "saved",
description: description ?? "Saved stage",
prompt: "Saved instructions",
model,
},
},
});
const text = stripVTControlCharacters(component.render(100).join("\n"));
expect(text.includes("--- Description ---")).toBe(accepted);
expect(text.includes("--- Thinking ---")).toBe(
accepted && model !== null && "thinking" in model,
);
if (accepted && model !== null && "thinking" in model) {
expect(text).toContain("medium");
}
});
/** Preserves edit details when replacement thinking is absent. */
test.each(
[undefined, "medium"].flatMap((previousThinking) =>
[false, true].flatMap((subagent) =>
[false, true].map((expanded) => ({
previousThinking,
subagent,
expanded,
})),
),
),
)("renders persisted edit details without replacement thinking %j", async ({
previousThinking,
subagent,
expanded,
}) => {
await createWorkflowSuite();
const fixture = await createFixture();
const content = {
id: "saved",
description: "Saved stage",
prompt: "Saved instructions",
model: {},
};
const definition = subagent
? resolveSessionDefinition(fixture, "workflow_edit_stage")
: fixture.tools.find(({ name }) => name === "workflow_edit_stage");
const component = new ToolExecutionComponent(
"workflow_edit_stage",
"saved-edit",
{ stageId: "saved" },
{},
definition,
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.setExpanded(expanded);
component.updateResult({
content: [],
isError: false,
details: {
presentationKind: "workflow-tool",
toolName: "workflow_edit_stage",
stage: { id: "saved" },
content: {
...content,
model:
previousThinking === undefined
? {}
: { thinking: previousThinking },
},
editedContent: { ...content, description: "Revised stage" },
},
});
const lines = component
.render(100)
.map((line) => stripVTControlCharacters(line).trim())
.filter(Boolean);
expect(lines).toEqual(
expanded
? [
"workflow_edit_stage: saved",
"--- Description ---",
"Saved stage",
"->",
"Revised stage",
]
: [
"workflow_edit_stage: saved",
"Description: Saved stage -> Revised stage",
],
);
for (const width of [22, 100]) {
for (const line of component.render(width)) {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
}
});
/** Proves multiline stage values retain labels and unambiguous edit boundaries. */
test("renders multiline stage values as labeled semantic blocks", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const args = createArguments(
"dynamic-delivery",
"Dynamic delivery process",
);
const implementation = (args["stages"] as Record[])[0];
if (implementation === undefined) {
throw new Error("implementation stage fixture is missing");
}
implementation["goal"] = "Checks pass.\nEvidence is saved.";
implementation["prompt"] = "Inspect current state.\nImplement the change.";
await executeTool(fixture, "workflow_create", args);
const get = await executeTool(fixture, "workflow_get_stage", {
stageId: "implementation",
});
const edit = await executeTool(fixture, "workflow_edit_stage", {
stageId: "implementation",
goal: "Checks pass.\nEvidence is saved.",
description: "Implementation stage",
prompt: "Read corrected requirements.\nImplement the corrected change.",
model: { thinking: "medium" },
});
const getExpanded = renderCompletedTool(
get.definition,
get,
PLAIN_THEME,
100,
true,
);
expect(getExpanded.call).toContain("--- Goal ---");
expect(getExpanded.call).toContain("Checks pass.");
expect(getExpanded.call).toContain("Evidence is saved.");
expect(getExpanded.call).toContain("--- Prompt ---");
expect(getExpanded.call).toContain("Inspect current state.");
expect(getExpanded.call).toContain("Implement the change.");
const editExpanded = renderCompletedTool(
edit.definition,
edit,
PLAIN_THEME,
100,
true,
);
expect(editExpanded.call).toEqual([
"workflow_edit_stage: implementation",
"--- Prompt ---",
"Inspect current state.",
"Implement the change.",
"->",
"Read corrected requirements.",
"Implement the corrected change.",
]);
const getCollapsed = renderCompletedTool(
get.definition,
get,
PLAIN_THEME,
500,
);
expect(getCollapsed.call).toContain(
"Prompt: Inspect current state. Implement the change.",
);
expect(getCollapsed.call).toContain(
"Goal: Checks pass. Evidence is saved.",
);
expect(getCollapsed.call).not.toContain(" Inspect current state.");
const editCollapsed = renderCompletedTool(
edit.definition,
edit,
PLAIN_THEME,
500,
);
expect(editCollapsed.call).toEqual([
"workflow_edit_stage: implementation",
"Prompt: Inspect current state. Implement the change. -> Read corrected requirements. Implement the corrected change.",
]);
});
/** Proves the transition snapshot keeps the source stage after state mutation. */
test("renders transition source and target identically after success", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
await executeTool(fixture, "workflow_activate", { workflowId: "delivery" });
const execution = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const active = renderCompletedTool(execution.definition, execution);
const session = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_transition"),
execution,
);
expect(active).toEqual({
call: [
"workflow_transition · delivery",
"From: implementation · Implementation stage",
"To: review · Review stage",
],
result: [],
});
expect(session).toEqual(active);
// The session overlay uses Pi's public component around the package presentation.
const component = new ToolExecutionComponent(
"workflow_transition",
"replayed-transition",
execution.args,
{},
resolveSessionDefinition(fixture, "workflow_transition"),
{ requestRender(): void {} } as TUI,
"/tmp",
);
component.markExecutionStarted();
component.setArgsComplete();
component.updateResult({ ...execution.result, isError: false });
const componentText = stripVTControlCharacters(
component.render(100).join("\n"),
);
expect(componentText).toContain("workflow_transition · delivery");
expect(componentText).toContain(
"From: implementation · Implementation stage",
);
expect(componentText).toContain("To: review · Review stage");
expect(componentText).not.toContain('{"success":true}');
});
/** Proves both tools normalize, wrap, and summarize long references in collapsed mode. */
test("bounds collapsed workflow references in active and subagent screens", async () => {
await createWorkflowSuite(
"Delivery process with enough detail to wrap while preserving the complete workflow reference for expansion "
.repeat(3)
.trim(),
"Implementation stage with enough detail to wrap while preserving the complete source reference for expansion "
.repeat(3)
.trim(),
"Review stage with enough detail to wrap while preserving the complete target reference for expansion "
.repeat(3)
.trim(),
);
const fixture = await createFixture();
const activation = await executeTool(fixture, "workflow_activate", {
workflowId: "delivery",
});
const transition = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const activeActivation = renderCompletedTool(
activation.definition,
activation,
PLAIN_THEME,
60,
);
const sessionActivation = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_activate"),
activation,
PLAIN_THEME,
60,
);
const activeTransition = renderCompletedTool(
transition.definition,
transition,
PLAIN_THEME,
60,
);
const sessionTransition = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_transition"),
transition,
PLAIN_THEME,
60,
);
expect(sessionActivation).toEqual(activeActivation);
expect(sessionTransition).toEqual(activeTransition);
expect(activeActivation.call).toHaveLength(6);
expect(activeActivation.call[0]).toBe("workflow_activate · delivery");
expect(activeActivation.call[1]).toStartWith("Delivery");
expect(activeActivation.call[5]).toMatch(HIDDEN_LINE_HINT);
expect(activeTransition.call).toHaveLength(11);
expect(activeTransition.call[0]).toBe("workflow_transition · delivery");
expect(activeTransition.call[1]).toStartWith("From: implementation ·");
expect(activeTransition.call[5]).toMatch(HIDDEN_LINE_HINT);
expect(activeTransition.call[6]).toStartWith("To: review · Review stage");
expect(activeTransition.call[10]).toMatch(HIDDEN_LINE_HINT);
expect(activeActivation.call.join("\n")).not.toContain(" ");
expect(activeTransition.call.join("\n")).not.toContain(" ");
});
/** Proves both tools expose every reference under semantic section headings when expanded. */
test("renders complete expanded workflow references in active and subagent screens", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
const activation = await executeTool(fixture, "workflow_activate", {
workflowId: "delivery",
});
const transition = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const activeActivation = renderCompletedTool(
activation.definition,
activation,
PLAIN_THEME,
100,
true,
);
const sessionActivation = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_activate"),
activation,
PLAIN_THEME,
100,
true,
);
const activeTransition = renderCompletedTool(
transition.definition,
transition,
PLAIN_THEME,
100,
true,
);
const sessionTransition = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_transition"),
transition,
PLAIN_THEME,
100,
true,
);
expect(activeActivation).toEqual({
call: ["workflow_activate · delivery", "Delivery process"],
result: [],
});
expect(sessionActivation).toEqual(activeActivation);
expect(activeTransition).toEqual({
call: [
"workflow_transition · delivery",
"--- From ---",
"implementation · Implementation stage",
"--- To ---",
"review · Review stage",
],
result: [],
});
expect(sessionTransition).toEqual(activeTransition);
});
/** Proves failed transitions retain semantic rows and use the approved color roles. */
test("renders transition errors with a bright label and muted evidence", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
await executeTool(fixture, "workflow_activate", { workflowId: "delivery" });
const execution = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "implementation",
});
// Advance live state so the failed row can only retain its source through persisted details.
await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const sessionDefinition = resolveSessionDefinition(
fixture,
"workflow_transition",
);
const rendered = renderCompletedTool(
sessionDefinition,
execution,
MARKED_THEME,
);
expect(rendered.call).toEqual([
"workflow_transition · delivery",
"From: implementation · Implementation stage",
"To: implementation · Implementation stage",
]);
expect(rendered.result).toEqual([
"Error: transition to implementation is not allowed; available transitions: review",
]);
});
/** Proves semantic references, hints, and expanded YAML honor Pi's default tool-shell child width. */
test("keeps workflow rows inside the default shell width", async () => {
await createWorkflowSuite();
const fixture = await createFixture();
await executeTool(fixture, "workflow_activate", { workflowId: "delivery" });
const execution = await executeTool(fixture, "workflow_transition", {
basis: "Stage checks passed.",
stageId: "review",
});
const definition = resolveSessionDefinition(fixture, "workflow_transition");
const rendered = renderCompletedTool(
definition,
execution,
PLAIN_THEME,
22,
);
const creationArgs = createArguments(
"dynamic-delivery",
"Dynamic delivery process",
);
const creation = await executeTool(
fixture,
"workflow_create",
creationArgs,
);
const creationDefinition = resolveSessionDefinition(
fixture,
"workflow_create",
);
const collapsedCreation = renderCompletedTool(
creationDefinition,
creation,
PLAIN_THEME,
22,
);
const expandedCreation = renderCompletedTool(
creationDefinition,
creation,
PLAIN_THEME,
20,
true,
);
const stageGet = await executeTool(fixture, "workflow_get_stage", {
stageId: "implementation",
});
const narrowStageGet = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_get_stage"),
stageGet,
PLAIN_THEME,
20,
true,
);
const stageEdit = await executeTool(fixture, "workflow_edit_stage", {
stageId: "implementation",
goal: "The implementation passes its checks",
description: "A revised implementation description",
prompt: "A revised implementation prompt that wraps",
model: { thinking: "high" },
});
const narrowStageEdit = renderCompletedTool(
resolveSessionDefinition(fixture, "workflow_edit_stage"),
stageEdit,
PLAIN_THEME,
20,
true,
);
const contentIndex = expandedCreation.call.indexOf("--- Content ---");
const yamlLines = expandedCreation.call.slice(contentIndex + 1);
const { description, stages, transitions } = creationArgs;
const parsedContent = parse(yamlLines.join("\n"));
expect(parsedContent["description"]).toEqual(description);
expect(typeof parsedContent["prompt"]).toBe("string");
expect(parsedContent["stages"]).toEqual(stages);
expect(parsedContent["transitions"]).toEqual(transitions);
for (const line of yamlLines) {
expect(visibleWidth(stripVTControlCharacters(line))).toBeLessThanOrEqual(
20,
);
}
const shell = new Box(1, 1);
for (const line of [
...rendered.call,
...rendered.result,
...collapsedCreation.call,
...expandedCreation.call,
...narrowStageGet.call,
...narrowStageEdit.call,
]) {
shell.addChild({ render: () => [line], invalidate: () => {} });
}
for (const line of shell.render(24)) {
expect(visibleWidth(stripVTControlCharacters(line))).toBeLessThanOrEqual(
24,
);
}
});
});