Files
paseo/packages/app/src/plugins/timeline/model.test.ts
T
Mohamed Boudra e34aea206e feat(plugins): add live timeline rows and client slash commands
Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer.

Related to #3940 and #3952.
2026-09-02 13:12:09 +02:00

211 lines
5.5 KiB
TypeScript

import { QueryClient } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";
import type { InstalledPlugin } from "../types";
import { transformTimelineItem } from "./model";
function plugin(input: {
id: string;
transform: InstalledPlugin["timelineTransformers"][number]["transform"];
}): InstalledPlugin {
return {
id: input.id,
serverId: "host-1",
clientBundle: "bundle",
queryClient: new QueryClient(),
cleanup: () => {},
surfaces: [],
sidebarItems: [],
workspacePanels: [],
commandCenterItems: [],
clientSide: null,
attachmentSources: [],
themes: [],
timelineTransformers: [
{
id: "report",
query: { itemType: "tool_call" },
transform: input.transform as Extract<
InstalledPlugin["timelineTransformers"][number],
{ query: { itemType: "tool_call" } }
>["transform"],
},
],
timelineRenderers: [],
};
}
const toolCall = {
type: "tool_call" as const,
callId: "call-1",
name: "shell",
detail: { type: "unknown" as const, input: null, output: null },
status: "completed" as const,
error: null,
};
describe("plugin timeline transforms", () => {
it("adds installation and source identity to plain transformed items", () => {
const transformed = transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [
plugin({
id: "reports",
transform: () => ({
items: [
{
type: "plugin" as const,
kind: "test-report",
version: 1,
data: { passed: 4 },
},
],
}),
}),
],
});
expect(transformed).toEqual([
{
type: "plugin",
id: "agent_tool_call-1/0",
pluginId: "reports",
kind: "test-report",
version: 1,
data: { passed: 4 },
},
]);
});
it("keeps the source item when no transformer returns a result", () => {
expect(
transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [plugin({ id: "reports", transform: () => undefined })],
}),
).toBeUndefined();
});
it("accepts an empty replacement and stops after the first result", () => {
const second = vi.fn(() => ({ items: [] }));
const transformed = transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [
plugin({ id: "first", transform: () => ({ items: [] }) }),
plugin({ id: "second", transform: second }),
],
});
expect(transformed).toEqual([]);
expect(second).not.toHaveBeenCalled();
});
it("rejects non-JSON data without breaking the timeline", () => {
const warning = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const transformed = transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [
plugin({
id: "broken",
transform: () => ({
items: [
{
type: "plugin" as const,
kind: "bad-data",
version: 1,
data: { value: Number.NaN },
},
],
}),
}),
],
});
expect(transformed).toBeUndefined();
expect(warning).toHaveBeenCalledOnce();
warning.mockRestore();
});
it("rejects class instances disguised as JSON data", () => {
const warning = vi.spyOn(console, "warn").mockImplementation(() => undefined);
const transformed = transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [
plugin({
id: "broken",
transform: () => ({
items: [
{
type: "plugin" as const,
kind: "bad-data",
version: 1,
data: { createdAt: new Date() } as never,
},
],
}),
}),
],
});
expect(transformed).toBeUndefined();
expect(warning).toHaveBeenCalledOnce();
warning.mockRestore();
});
it("takes a JSON snapshot of transformed data", () => {
const data = { passed: 4 };
const transformed = transformTimelineItem({
item: toolCall,
phase: "complete",
sourceId: "agent_tool_call-1",
plugins: [
plugin({
id: "reports",
transform: () => ({
items: [{ type: "plugin" as const, kind: "test-report", version: 1, data }],
}),
}),
],
});
data.passed = 0;
expect(transformed?.[0]?.data).toEqual({ passed: 4 });
});
it("passes phase and preserves explicit output identity", () => {
const transform = vi.fn(() => ({
items: [
{
type: "plugin" as const,
id: "summary",
kind: "test-report",
version: 1,
data: { passed: 4 },
},
],
}));
const transformed = transformTimelineItem({
item: { ...toolCall, status: "running" },
phase: "streaming",
sourceId: "agent_tool_call-1",
plugins: [plugin({ id: "reports", transform })],
});
expect(transform).toHaveBeenCalledWith({
item: { ...toolCall, status: "running" },
phase: "streaming",
});
expect(transformed?.[0]?.id).toBe("summary");
});
});