yoi/web/workspace/src/lib/workspace-console/model.test.ts

323 lines
8.3 KiB
TypeScript

import type { Event } from "$lib/generated/protocol";
import { projectConsole, segmentsToText, workerConsoleHref } from "./model.ts";
declare const Deno: {
test(name: string, fn: () => void): void;
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function assertEquals<T>(actual: T, expected: T): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) {
throw new Error(`Expected ${expectedJson}, got ${actualJson}`);
}
}
Deno.test("workerConsoleHref encodes runtime and worker target authority", () => {
assert(
workerConsoleHref({
runtime_id: "local runtime",
worker_id: "worker/one",
}, "workspace-1") ===
"/w/workspace-1/runtimes/local%20runtime/workers/worker%2Fone/console",
"href should contain encoded runtime_id and worker_id segments",
);
});
Deno.test("segmentsToText preserves protocol segment semantics", () => {
const text = segmentsToText([
{ kind: "text", content: "hello" },
{ kind: "file_ref", path: "/tmp/example.md" },
{ kind: "knowledge_ref", slug: "design-note" },
{ kind: "workflow_invoke", slug: "ticket-review" },
]);
assert(text.includes("hello"), "text segment should render content");
assert(
text.includes("@file /tmp/example.md"),
"file ref should render as a file reference",
);
assert(
text.includes("@knowledge design-note"),
"knowledge ref should render as a knowledge reference",
);
assert(
text.includes("/ticket-review"),
"workflow invocation should render as slash command",
);
});
Deno.test("projectConsole projects visible protocol rows", () => {
const projection = projectConsole([
{
cursor: "10",
event: {
event: "user_message",
data: { segments: [{ kind: "text", content: "input" }] },
} satisfies Event,
},
{
cursor: "11",
event: {
event: "text_delta",
data: { text: "stream" },
} satisfies Event,
},
{
cursor: "12",
event: {
event: "thinking_done",
data: { text: "reasoning" },
} satisfies Event,
},
{
cursor: "13",
event: {
event: "tool_result",
data: {
id: "tool-1",
summary: "read file",
output: "content",
is_error: false,
},
} satisfies Event,
},
{
cursor: "14",
event: {
event: "usage",
data: { input_tokens: 12, output_tokens: 5 },
} satisfies Event,
},
{
cursor: "15",
event: {
event: "error",
data: { code: "invalid_request", message: "bad frame" },
} satisfies Event,
},
]);
assert(
projection.lines.some((line) =>
line.source === "event" && line.kind === "user"
),
"user protocol row expected",
);
assert(
projection.lines.some((line) =>
line.source === "event" && line.kind === "assistant"
),
"assistant protocol row expected",
);
assert(
projection.lines.some((line) => line.kind === "thinking"),
"thinking event row expected",
);
assert(
projection.lines.some((line) => line.kind === "tool"),
"tool event row expected",
);
assert(
!projection.lines.some((line) => line.kind === "usage"),
"usage should update the summary without rendering a console row",
);
assert(
projection.lines.some((line) => line.kind === "error" && line.error),
"error event row expected",
);
assert(
projection.usage === "input 12 · output 5 · cache unknown",
"usage summary should be retained",
);
});
Deno.test("projectConsole groups tool call lifecycle into one Call block", () => {
const projection = projectConsole([
{
cursor: "40",
event: {
event: "tool_call_start",
data: { id: "call-1", name: "Bash" },
} satisfies Event,
},
{
cursor: "41",
event: {
event: "tool_call_args_delta",
data: { id: "call-1", json: '{"command":"pw' },
} satisfies Event,
},
{
cursor: "42",
event: {
event: "tool_call_args_delta",
data: { id: "call-1", json: 'd"}' },
} satisfies Event,
},
{
cursor: "43",
event: {
event: "tool_call_done",
data: { id: "call-1", name: "Bash", arguments: '{"command":"pwd"}' },
} satisfies Event,
},
{
cursor: "44",
event: {
event: "tool_result",
data: {
id: "call-1",
summary: "command completed",
output: "/repo\n",
is_error: false,
},
} satisfies Event,
},
]);
const toolLines = projection.lines.filter((line) => line.kind === "tool");
assertEquals(toolLines.length, 1);
assertEquals(toolLines[0].title, "Call · Bash");
assert(
!toolLines[0].streaming,
"completed tool call should not remain streaming",
);
assert(
toolLines[0].body.includes("$ pwd"),
"Bash command should be summarized",
);
assert(
toolLines[0].body.includes("/repo"),
"tool result should be folded into the Call block",
);
assert(
toolLines[0].detail?.includes("id: call-1"),
"call id should remain in detail",
);
});
Deno.test("projectConsole keeps streaming tool call updates in the same Call block", () => {
const projection = projectConsole([
{
cursor: "45",
event: {
event: "tool_call_start",
data: { id: "call-2", name: "Read" },
} satisfies Event,
},
{
cursor: "46",
event: {
event: "tool_call_args_delta",
data: { id: "call-2", json: '{"file_path":"/tmp/a.md"}' },
} satisfies Event,
},
]);
const toolLines = projection.lines.filter((line) => line.kind === "tool");
assertEquals(toolLines.length, 1);
assertEquals(toolLines[0].title, "Call · Read");
assert(toolLines[0].streaming, "streaming tool call should remain streaming");
assert(
toolLines[0].body.includes("/tmp/a.md") &&
toolLines[0].body.includes("streaming args"),
"Read call should render the current argument stream and state",
);
});
Deno.test("projectConsole preserves in-progress assistant protocol stream", () => {
const projection = projectConsole([
{
cursor: "13",
event: { event: "text_delta", data: { text: "new" } } satisfies Event,
},
]);
assert(
projection.lines.some((line) =>
line.source === "event" && line.kind === "assistant" &&
line.body === "new" && line.streaming
),
"in-progress assistant stream should remain visible",
);
});
Deno.test("projectConsole keeps protocol lifecycle events out of the console surface", () => {
const projection = projectConsole([
{
cursor: "30",
event: { event: "status", data: { status: "running" } } satisfies Event,
},
{
cursor: "31",
event: { event: "llm_call_end", data: { llm_call: 0 } } satisfies Event,
},
{
cursor: "32",
event: {
event: "turn_end",
data: { turn: 0, result: "finished" },
} satisfies Event,
},
{
cursor: "33",
event: { event: "run_end", data: { result: "finished" } } satisfies Event,
},
{
cursor: "34",
event: {
event: "system_item",
data: { item: { kind: "note", content: "internal" } },
} satisfies Event,
},
]);
assertEquals(projection.lines, []);
assertEquals(projection.status, "running");
});
Deno.test("projectConsole uses snapshot for state without rendering it as console output", () => {
const projection = projectConsole([
{
cursor: "20",
event: {
event: "snapshot",
data: {
entries: [{ role: "user" }],
greeting: {
worker_name: "Worker",
cwd: "/repo",
provider: "provider",
model: "model",
scope_summary: "bounded",
tools: ["Read"],
context_window: 100,
context_tokens: 20,
},
status: "running",
in_flight: {
blocks: [
{ kind: "text", text: "partial" },
],
},
},
} satisfies Event,
},
]);
assertEquals(projection.status, "running");
assertEquals(
projection.lines.map((line) =>
`${line.kind}:${line.body}:${line.streaming}`
),
["in_flight:partial:true"],
);
});