0914379f49
- Updated task call and result rendering to process shared context with the Markdown renderer, so context sections are now displayed with proper Markdown formatting. - Stopped shimmer animation on pending bash/eval/task blocks once async state is `running`, preventing the committed frame from freezing a transient dark border segment. - Adjusted rule path display to fall back to a root-relative path when cwd-relative resolution is unavailable.
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { getThemeByName, setThemeInstance, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
|
import type { TaskParams, TaskToolDetails } from "@oh-my-pi/pi-coding-agent/task";
|
|
import { taskToolRenderer } from "@oh-my-pi/pi-coding-agent/task/render";
|
|
|
|
describe("task renderer: streaming call preview", () => {
|
|
let theme: Theme;
|
|
|
|
beforeAll(async () => {
|
|
resetSettingsForTest();
|
|
await Settings.init({ inMemory: true, cwd: process.cwd() });
|
|
const resolved = await getThemeByName("dark");
|
|
expect(resolved).toBeDefined();
|
|
theme = resolved!;
|
|
setThemeInstance(theme);
|
|
});
|
|
|
|
afterAll(() => {
|
|
resetSettingsForTest();
|
|
});
|
|
|
|
function render(args: TaskParams, expanded = false): string {
|
|
const component = taskToolRenderer.renderCall(args, { expanded, isPartial: true }, theme);
|
|
return Bun.stripANSI(component.render(160).join("\n"));
|
|
}
|
|
|
|
function renderCompleted(args: TaskParams): string {
|
|
const details: TaskToolDetails = {
|
|
projectAgentsDir: null,
|
|
totalDurationMs: 12,
|
|
results: [
|
|
{
|
|
index: 0,
|
|
id: "Only",
|
|
agent: args.agent,
|
|
agentSource: "bundled",
|
|
task: "Render the shared context",
|
|
exitCode: 0,
|
|
output: "Done.",
|
|
stderr: "",
|
|
truncated: false,
|
|
durationMs: 12,
|
|
tokens: 1,
|
|
},
|
|
],
|
|
};
|
|
const component = taskToolRenderer.renderResult(
|
|
{ content: [{ type: "text", text: "1 agent completed." }], details },
|
|
{ expanded: false, isPartial: false },
|
|
theme,
|
|
args,
|
|
);
|
|
return Bun.stripANSI(component.render(160).join("\n"));
|
|
}
|
|
|
|
// The preview must surface each agent's id + ui description so the user can
|
|
// see which agents are being dispatched, not a bare "N agents" count.
|
|
it("lists each task's id and description instead of only a count", () => {
|
|
const args: TaskParams = {
|
|
agent: "reviewer",
|
|
tasks: [
|
|
{ id: "ReviewAuth", description: "Audit the auth module", assignment: "..." },
|
|
{ id: "ReviewDb", description: "Audit the db layer", assignment: "..." },
|
|
],
|
|
};
|
|
const out = render(args);
|
|
|
|
expect(out).toContain("ReviewAuth");
|
|
expect(out).toContain("Audit the auth module");
|
|
expect(out).toContain("ReviewDb");
|
|
expect(out).toContain("Audit the db layer");
|
|
// The per-task list stands on its own — neither the redundant "Tasks (N)"
|
|
// section label nor the old flat "N agents" line is drawn.
|
|
expect(out).not.toContain("Tasks (");
|
|
expect(out).not.toContain("2 agents");
|
|
});
|
|
|
|
it("renders a partially-streamed entry without a description and missing trailing entry", () => {
|
|
const args = {
|
|
agent: "task",
|
|
// Trailing entry mimics streaming JSON: id arrived, description not yet,
|
|
// plus a not-yet-materialized slot.
|
|
tasks: [{ id: "First", description: "Do the first thing", assignment: "..." }, { id: "Second" }, undefined],
|
|
} as unknown as TaskParams;
|
|
|
|
const out = render(args);
|
|
|
|
expect(out).toContain("First");
|
|
expect(out).toContain("Do the first thing");
|
|
expect(out).toContain("Second");
|
|
// Missing-id slot falls back to a positional placeholder rather than crashing.
|
|
expect(out).toContain("#3");
|
|
expect(out).not.toContain("Tasks (");
|
|
});
|
|
|
|
it("caps the collapsed list and reports the overflow as agents", () => {
|
|
const tasks = Array.from({ length: 15 }, (_, i) => ({
|
|
id: `Agent${i + 1}`,
|
|
description: `Task ${i + 1}`,
|
|
assignment: "...",
|
|
}));
|
|
const args: TaskParams = { agent: "task", tasks };
|
|
|
|
const collapsed = render(args, false);
|
|
expect(collapsed).toContain("Agent1");
|
|
expect(collapsed).toContain("Agent12");
|
|
expect(collapsed).not.toContain("Agent13");
|
|
expect(collapsed).toContain("3 more agents");
|
|
|
|
const expanded = render(args, true);
|
|
expect(expanded).toContain("Agent13");
|
|
expect(expanded).toContain("Agent15");
|
|
expect(expanded).not.toContain("more agents");
|
|
});
|
|
|
|
it("surfaces the isolation flag in the header bar", () => {
|
|
const args: TaskParams = {
|
|
agent: "task",
|
|
isolated: true,
|
|
tasks: [{ id: "Only", description: "Single task", assignment: "..." }],
|
|
};
|
|
const out = render(args);
|
|
const lines = out.split("\n");
|
|
|
|
expect(out).toContain("Only");
|
|
// Isolation is surfaced as header meta in the frame's top bar (first line),
|
|
// not as a trailing child row under the task list.
|
|
expect(lines[0]).toContain("isolated");
|
|
});
|
|
|
|
it("renders shared context as markdown in call and result frames", () => {
|
|
const args: TaskParams = {
|
|
agent: "task",
|
|
context: ["# Goal", "Fix **rendering**.", "", "# Constraints", "- Keep `task` visible"].join("\n"),
|
|
tasks: [{ id: "Only", description: "Single task", assignment: "..." }],
|
|
};
|
|
|
|
for (const out of [render(args), renderCompleted(args)]) {
|
|
expect(out).toContain("Goal");
|
|
expect(out).toContain("Fix rendering.");
|
|
expect(out).toContain("Constraints");
|
|
expect(out).toContain("Keep task visible");
|
|
expect(out).not.toContain("# Goal");
|
|
expect(out).not.toContain("# Constraints");
|
|
}
|
|
});
|
|
|
|
// Once the tool produces a result, the container suppresses the call entirely
|
|
// via `mergeCallAndResult` and `renderResult` draws each agent. As a safety
|
|
// net, `renderCall` also drops its duplicate per-task preview when a result
|
|
// snapshot is present, so the two never stack.
|
|
it("drops the per-task preview list once a result snapshot exists", () => {
|
|
const args: TaskParams = {
|
|
agent: "reviewer",
|
|
tasks: [
|
|
{ id: "ReviewAuth", description: "Audit the auth module", assignment: "..." },
|
|
{ id: "ReviewDb", description: "Audit the db layer", assignment: "..." },
|
|
],
|
|
};
|
|
const component = taskToolRenderer.renderCall(
|
|
args,
|
|
{ expanded: false, isPartial: true, renderContext: { hasResult: true } },
|
|
theme,
|
|
);
|
|
const out = Bun.stripANSI(component.render(160).join("\n"));
|
|
|
|
expect(out).not.toContain("Audit the auth module");
|
|
expect(out).not.toContain("Audit the db layer");
|
|
expect(out).not.toContain("Tasks (");
|
|
});
|
|
});
|