Files
pikasTech-HWLAB/internal/workbench/workbench.test.ts
T
root f07669fe40
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success
fix: derive Workbench rail titles from Kafka index
2026-07-20 11:10:13 +02:00

445 lines
19 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type { WorkbenchApplication, WorkbenchTemporalGateway } from "./contracts.ts";
import { createWorkbenchDispatcher } from "./dispatcher.ts";
import { createCloudWorkbenchApplication } from "./cloud-application.ts";
import { createWorkbenchHttpApp } from "./http.ts";
import { runWorkbenchCli } from "../../tools/src/workbench-cli.ts";
describe("Workbench dispatcher", () => {
test("submits through Temporal without synthesizing terminal state", async () => {
const calls: string[] = [];
const application: WorkbenchApplication = {
async health() { return { status: "ready" }; },
async createSession() { return { session: { sessionId: "ses_test" } }; },
async admitTurn(input) { calls.push("admit"); return input; },
async dispatchTurn() { throw new Error("API dispatcher must not dispatch directly"); },
async cancelTurn() { throw new Error("API dispatcher must not cancel directly"); }
};
const temporal: WorkbenchTemporalGateway = {
async submit(input) { calls.push("temporal"); return { workflowId: `workbench-turn-${input.traceId}`, workflowRunId: "run-1", reused: false }; },
async cancel() { return { workflowId: "cancel", workflowRunId: "run-2", reused: false }; },
async close() {}
};
const dispatch = createWorkbenchDispatcher({ application, temporal, mode: "temporal" });
const result = await dispatch({ operation: "turn.submit", actor: { id: "usr_test" }, traceId: "trc_test", params: { sessionId: "ses_test", message: "hi" } });
expect(calls).toEqual(["admit", "temporal"]);
expect(result).toMatchObject({ ok: true, mode: "temporal", data: { status: "running", traceId: "trc_test", terminalAuthority: "hwlab.event.v1", resultSynthesized: false } });
});
test("cancel is a durable Temporal command", async () => {
const application = stubApplication();
const temporal: WorkbenchTemporalGateway = {
async submit() { throw new Error("not used"); },
async cancel(input) { return { workflowId: `workbench-cancel-${input.traceId}`, workflowRunId: "run-cancel", reused: false }; },
async close() {}
};
const result = await createWorkbenchDispatcher({ application, temporal, mode: "temporal" })({ operation: "turn.cancel", actor: { id: "usr_test" }, traceId: "trc_test" });
expect(result).toMatchObject({ ok: true, data: { status: "cancel_requested", resultSynthesized: false } });
});
});
describe("Workbench Cloud application adapter", () => {
test("persists admission before dispatch through distinct internal routes", async () => {
const calls: Array<{ path: string; authorization: string | null }> = [];
const application = createCloudWorkbenchApplication({
baseUrl: "http://cloud.internal",
authorization: "unified-internal-key",
fetchImpl: (async (url: string | URL | Request, init?: RequestInit) => {
calls.push({ path: new URL(String(url)).pathname, authorization: new Headers(init?.headers).get("authorization") });
return Response.json({ ok: true, accepted: true, status: "admitted" });
}) as typeof fetch
});
const input = { actor: { id: "usr_test" }, traceId: "trc_test", sessionId: "ses_test", params: { sessionId: "ses_test", message: "hi" } };
await application.admitTurn(input);
await application.dispatchTurn(input);
expect(calls).toEqual([
{ path: "/v1/internal/workbench/admit", authorization: "Bearer unified-internal-key" },
{ path: "/v1/agent/chat", authorization: "Bearer unified-internal-key" }
]);
});
test("preserves an existing Bearer authorization scheme", async () => {
let authorization: string | null = null;
const application = createCloudWorkbenchApplication({
baseUrl: "http://cloud.internal",
authorization: "Bearer unified-internal-key",
fetchImpl: (async (_url: string | URL | Request, init?: RequestInit) => {
authorization = new Headers(init?.headers).get("authorization");
return Response.json({ ok: true, session: { sessionId: "ses_test" } });
}) as typeof fetch
});
await application.createSession({ actor: { id: "usr_test" }, params: {} });
expect(authorization).toBe("Bearer unified-internal-key");
});
});
describe("Workbench native HTTP adapter", () => {
test("native session list exposes the first user preview for summary-only rail loading", async () => {
const app = createWorkbenchHttpApp({
async dispatch() { return { ok: true }; },
async snapshot() {
return {
sessions: {
ses_title: {
sessionId: "ses_title",
updatedAt: "2026-07-20T01:00:00.000Z",
messages: [{ role: "user", content: "persisted native title", createdAt: "2026-07-20T00:59:00.000Z" }]
},
ses_empty: { sessionId: "ses_empty", updatedAt: "2026-07-20T00:00:00.000Z", messages: [] }
},
turns: {}
};
}
});
const response = await app.fetch(new Request("http://native.test/v1/workbench/sessions"));
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
sessions: [
{ sessionId: "ses_title", firstUserMessagePreview: "persisted native title" },
{ sessionId: "ses_empty", firstUserMessagePreview: null }
]
});
});
test("AgentRun native session list uses Kafka index summaries instead of admission messages", async () => {
const app = createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: {
workbenchSessionSummaries() {
return { ok: true, sessions: [{ sessionId: "ses_title", firstUserMessagePreview: "Kafka title", lastUserMessageAt: "2026-07-20T01:00:00.000Z" }] };
}
},
async dispatch() { return { ok: true }; },
async snapshot() {
return {
sessions: {
ses_title: {
sessionId: "ses_title",
updatedAt: "2026-07-20T00:00:00.000Z",
messages: [{ role: "user", content: "admission snapshot title" }]
}
},
turns: {}
};
}
});
const response = await app.fetch(new Request("http://native.test/v1/workbench/sessions"));
expect(await response.json()).toMatchObject({
sessions: [{ sessionId: "ses_title", firstUserMessagePreview: "Kafka title", lastUserMessageAt: "2026-07-20T01:00:00.000Z" }]
});
});
test("authorizes only the Workbench navigation entry", async () => {
const app = createWorkbenchHttpApp({
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
for (const path of ["/auth/session", "/auth/bootstrap"]) {
const response = await app.fetch(new Request(`http://native.test${path}`));
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
authenticated: true,
access: { nav: { profileId: "workbench-native", allowedIds: ["workbench.code"] } }
});
}
});
test("serves read-only empty states for Workbench auxiliary panels", async () => {
const app = createWorkbenchHttpApp({
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
for (const path of ["/v1/caserun/cases", "/v1/hwpod/specs?probe=1", "/v1/hwpod-node-ops"]) {
const response = await app.fetch(new Request(`http://native.test${path}`));
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ ok: true, status: "unavailable", mode: "native-test" });
}
const performance = await app.fetch(new Request("http://native.test/v1/web-performance", { method: "POST", body: "{}" }));
expect(performance.status).toBe(202);
expect(await performance.json()).toMatchObject({ ok: true, status: "accepted", persisted: false, mode: "native-test" });
});
test("L0 CLI observes AgentRun-derived events only through the Kafka SSE route", async () => {
const subscribers = new Set<(envelope: any) => void>();
const bridge = {
capabilities: { liveKafkaSse: true },
ready: Promise.resolve(),
subscribeLiveHwlabEvents(listener: (envelope: any) => void) { subscribers.add(listener); return () => subscribers.delete(listener); }
};
const app = createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: bridge,
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request) });
try {
const pending = runWorkbenchCli([
"events", "inspect",
"--session-id", "ses_l0_sse",
"--trace-id", "trc_l0_sse",
"--over-api",
"--api-url", `http://127.0.0.1:${server.port}`,
"--timeout-ms", "1000"
], {});
for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(5);
for (const listener of subscribers) listener({ schema: "hwlab.event.v1", sessionId: "ses_l0_sse", traceId: "trc_l0_sse", event: { type: "backend", eventType: "backend", label: "agentrun:backend:command-created" } });
expect(await pending).toMatchObject({
ok: true,
operation: "events.inspect",
connected: true,
connectedContract: {
deliverySemantics: "live-only",
realtimeSource: "hwlab.event.v1",
replay: false,
liveOnly: true,
refreshReplay: null
},
eventCount: 1,
eventTypes: ["backend"]
});
} finally {
server.stop(true);
await app.close();
}
});
test("L0 CLI replays retained Kafka events before the same live SSE stream", async () => {
const sessionId = "ses_l0_refresh";
const traceId = "trc_l0_refresh";
const bridge = {
capabilities: { liveKafkaSse: true, kafkaRefreshReplay: true },
refreshReplay: { groupIdPrefix: "hwlab-l0-refresh", timeoutMs: 1000, scanLimit: 100, matchedEventLimit: 20, liveBufferLimit: 20 },
ready: Promise.resolve(),
subscribeLiveHwlabEvents() { return () => {}; },
async queryHwlabEventRetention(options: any) {
expect(options).toMatchObject({ sessionId, traceId, partitionKey: sessionId, fromBeginning: true });
return {
topic: "hwlab.event.v1",
events: Array.from({ length: 100 }, (_, index) => ({
topic: "hwlab.event.v1",
partition: 0,
offset: String(index),
value: {
schema: "hwlab.event.v1",
eventType: "hwlab.trace.event.projected",
eventId: `hwlab:evt_l0_refresh_${index}`,
sourceEventId: `evt_l0_refresh_${index}`,
sessionId,
hwlabSessionId: sessionId,
traceId,
event: { type: "backend", eventType: "backend", sessionId, traceId, sourceEventId: `evt_l0_refresh_${index}` }
}
})),
completionReason: "end-offset",
completion: { reason: "end-offset", complete: true, barrierReached: true, retentionStartVerified: true },
reachedEndOffsets: true,
endOffsetsAvailable: true,
endOffsets: [{ partition: 0, startOffset: "0", endOffset: "100" }],
scannedCount: 100,
parsedCount: 100,
matchedCount: 100,
filterRejectedCount: 0,
keyRejectedCount: 0,
partitionKeyScoped: true,
targetPartition: 0,
timing: { endOffsetSnapshotMs: 0, groupOffsetsMs: 0, consumerConnectMs: 0, consumerSubscribeMs: 0, scanMs: 0, cleanupMs: 0, indexLookupMs: 1, totalMs: 1 },
index: { hit: true, source: "process-session-index", indexedEventCount: 300, indexedSessionCount: 3, incompleteSessionCount: 0, valuesRedacted: true }
};
}
};
const app = createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: bridge,
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request) });
try {
const result = await runWorkbenchCli([
"events", "inspect",
"--session-id", sessionId,
"--trace-id", traceId,
"--over-api",
"--api-url", `http://127.0.0.1:${server.port}`,
"--timeout-ms", "1000"
], {});
expect(result).toMatchObject({
ok: true,
connected: true,
connectedContract: {
deliverySemantics: "kafka-retention-then-live",
realtimeSource: "hwlab.event.v1",
replay: true,
liveOnly: false,
refreshReplay: {
phase: "live",
counts: { replayed: 100 },
query: {
complete: true,
scannedCount: 100,
parsedCount: 100,
matchedCount: 100,
filterRejectedCount: 0,
keyRejectedCount: 0,
partitionKeyScoped: true,
targetPartition: 0,
index: { hit: true, source: "process-session-index", indexedEventCount: 300, indexedSessionCount: 3, incompleteSessionCount: 0, blocking: false },
timing: { scanMs: 0, indexLookupMs: 1, totalMs: 1 }
}
}
},
eventCount: 100,
eventTypes: ["backend"],
eventTypeCounts: { backend: 100 }
});
expect(JSON.stringify(result).length).toBeLessThan(10_000);
} finally {
server.stop(true);
await app.close();
}
});
test("L0 CLI waits for explicit Kafka event semantics in one SSE window", async () => {
const subscribers = new Set<(envelope: any) => void>();
const bridge = {
capabilities: { liveKafkaSse: true },
ready: Promise.resolve(),
subscribeLiveHwlabEvents(listener: (envelope: any) => void) { subscribers.add(listener); return () => subscribers.delete(listener); }
};
const app = createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: bridge,
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request) });
try {
const pending = runWorkbenchCli([
"events", "inspect",
"--session-id", "ses_l0_semantics",
"--trace-id", "trc_l0_semantics",
"--over-api",
"--api-url", `http://127.0.0.1:${server.port}`,
"--timeout-ms", "1000",
"--wait-for", "user,backend,assistant,terminal,final"
], {});
for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(5);
const events = [
{ type: "user", eventType: "user", createdAt: "2026-07-19T16:00:00.000Z" },
{ type: "backend", eventType: "backend", createdAt: "2026-07-19T16:00:02.000Z" },
{ type: "assistant", eventType: "assistant", createdAt: "2026-07-19T16:01:02.000Z", final: true, replyAuthority: true, finalResponse: { text: "done" } },
{ type: "result", eventType: "terminal", createdAt: "2026-07-19T16:01:03.000Z", terminal: true, terminalStatus: "completed", status: "completed" }
];
for (const event of events) {
for (const listener of subscribers) listener({ schema: "hwlab.event.v1", sessionId: "ses_l0_semantics", traceId: "trc_l0_semantics", event });
}
expect(await pending).toMatchObject({
ok: true,
eventCount: 4,
eventTypes: ["user", "backend", "assistant", "terminal"],
waitFor: ["user", "backend", "assistant", "terminal", "final"],
observedSemantics: ["user", "backend", "assistant", "terminal", "final"],
missingSemantics: [],
semanticCounts: { user: 1, backend: 1, assistant: 1, terminal: 1, final: 1 },
terminalStatuses: ["completed"],
timing: {
clock: "agentrun-source-event-created-at",
phasesMs: {
firstEventToBackend: 2000,
admissionAndDispatch: null,
runnerProvisioning: null,
runtimePreparation: null,
providerExecution: null,
backendToAssistant: 60000,
assistantToTerminal: 1000,
firstEventToTerminal: 63000,
firstEventToFinal: 62000
},
slowestGap: {
durationMs: 60000,
toIndex: 2,
toEventType: "assistant"
},
valuesPrinted: false
}
});
} finally {
server.stop(true);
await app.close();
}
});
test("L0 CLI reports missing Kafka event semantics on timeout", async () => {
const subscribers = new Set<(envelope: any) => void>();
const app = createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: {
capabilities: { liveKafkaSse: true },
ready: Promise.resolve(),
subscribeLiveHwlabEvents(listener: (envelope: any) => void) { subscribers.add(listener); return () => subscribers.delete(listener); }
},
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
});
const server = Bun.serve({ port: 0, fetch: (request) => app.fetch(request) });
try {
const pending = runWorkbenchCli([
"events", "inspect",
"--trace-id", "trc_l0_missing_terminal",
"--over-api",
"--api-url", `http://127.0.0.1:${server.port}`,
"--timeout-ms", "250",
"--wait-for", "assistant,terminal"
], {});
for (let index = 0; index < 20 && subscribers.size === 0; index += 1) await Bun.sleep(2);
for (const listener of subscribers) listener({ schema: "hwlab.event.v1", traceId: "trc_l0_missing_terminal", event: { type: "assistant", eventType: "assistant" } });
expect(await pending).toMatchObject({
ok: false,
status: "event-timeout",
observedSemantics: ["assistant"],
missingSemantics: ["terminal"],
terminalStatuses: [],
error: { code: "workbench_kafka_sse_event_missing" }
});
} finally {
server.stop(true);
await app.close();
}
});
test("L0 SSE exposes Kafka startup failure without waiting for a read model", async () => {
const startupError = Object.assign(new Error("Kafka broker unavailable"), { code: "kafka_broker_unavailable" });
const response = await createWorkbenchHttpApp({
mode: "agentrun-native",
kafkaEventBridge: {
capabilities: { liveKafkaSse: true },
liveReady: Promise.reject(startupError),
subscribeLiveHwlabEvents() { return () => {}; }
},
async dispatch() { return { ok: true }; },
async snapshot() { return { sessions: {}, turns: {} }; }
}).fetch(new Request("http://localhost/v1/workbench/events?traceId=trc_l0_kafka_error"));
expect(response.status).toBe(200);
const body = await response.text();
expect(body).toContain("event: workbench.connected");
expect(body).toContain("event: workbench.error");
expect(body).toContain("kafka_broker_unavailable");
});
});
function stubApplication(): WorkbenchApplication {
return {
async health() { return {}; },
async createSession() { return {}; },
async admitTurn(input) { return input; },
async dispatchTurn() { return {}; },
async cancelTurn() { return {}; }
};
}