390 lines
18 KiB
TypeScript
390 lines
18 KiB
TypeScript
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
|
|
// Exercises the browser-side Workbench performance probe without sending network traffic.
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import type { ChatMessage, TraceEvent } from "../src/types/index.ts";
|
|
import {
|
|
acknowledgeWorkbenchVisible,
|
|
drainWorkbenchPerformanceEventsForTest,
|
|
failWorkbenchSubmitJourney,
|
|
finishWorkbenchOpenFullLoad,
|
|
finishWorkbenchSessionSwitchFullLoad,
|
|
markWorkbenchSubmitApiAccepted,
|
|
markWorkbenchTraceEventsReceived,
|
|
markWorkbenchTraceProjected,
|
|
recordWorkbenchApiRequest,
|
|
recordWorkbenchRuntimeDiagnostic,
|
|
resetWorkbenchPerformanceForTest,
|
|
startWorkbenchOpenJourney,
|
|
startWorkbenchSessionSwitch,
|
|
startWorkbenchSubmitJourney
|
|
} from "../src/utils/workbench-performance.ts";
|
|
|
|
test("Workbench performance probe records journeys and strips high-cardinality identifiers", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
startWorkbenchOpenJourney({ route: "/workbench/sessions/ses_secret", cache: "cold", authState: "warm" });
|
|
acknowledgeWorkbenchVisible({ messages: [], activeSessionId: "ses_secret", detailLoading: false });
|
|
finishWorkbenchOpenFullLoad("ok");
|
|
|
|
startWorkbenchSessionSwitch({ sessionId: "ses_secret", source: "rail", targetState: "running", cache: "warm" });
|
|
acknowledgeWorkbenchVisible({ messages: [userMessage("ses_secret")], activeSessionId: "ses_secret", detailLoading: false });
|
|
finishWorkbenchSessionSwitchFullLoad("ses_secret", "ok");
|
|
|
|
const wallBase = Date.now();
|
|
const createdAt = new Date(wallBase - 300).toISOString();
|
|
const appendedAt = new Date(wallBase - 220).toISOString();
|
|
const serverSentAt = new Date(wallBase - 120).toISOString();
|
|
startWorkbenchSubmitJourney({ traceId: "trc_secret", sessionId: "ses_secret", entry: "existing", backend: "codex", transport: "sse" });
|
|
markWorkbenchSubmitApiAccepted("trc_secret");
|
|
markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "sse", serverSentAt, traceSeq: 42, events: [{ type: "assistant_message", eventType: "assistant", backend: "agentrun-v01/codex", createdAt, appendedAt, ts: createdAt, sourceSeq: 42 } as TraceEvent] });
|
|
markWorkbenchTraceProjected("trc_secret");
|
|
acknowledgeWorkbenchVisible({ messages: [agentMessage("ses_secret", "trc_secret")], activeSessionId: "ses_secret", detailLoading: false });
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
const payload = JSON.stringify(events);
|
|
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "workbench_open_first_visible"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "session_switch_first_visible"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.backend === "agentrun-v01/codex"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "created_to_append" && event.eventType === "assistant"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "append_to_sse" && event.eventType === "assistant"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive" && event.eventType === "assistant"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "receive_to_project" && event.eventType === "assistant"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "project_to_paint" && event.eventType === "assistant"));
|
|
assert.ok(events.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "assistant"));
|
|
assert.doesNotMatch(payload, /trc_secret|ses_secret|sessionId|runId|commandId/u);
|
|
});
|
|
|
|
test("Workbench submit failures do not pollute first visible output SLI", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
startWorkbenchSubmitJourney({ traceId: "trc_network", sessionId: "ses_secret", entry: "new", backend: "codex", transport: "sse" });
|
|
failWorkbenchSubmitJourney("trc_network", "network");
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
assert.equal(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
|
|
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_failure" && event.outcome === "network"));
|
|
});
|
|
|
|
test("Workbench runtime diagnostics expose root cause labels without raw identifiers", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
const restoreBrowserRuntime = installBrowserProbeRuntime();
|
|
try {
|
|
recordWorkbenchRuntimeDiagnostic({
|
|
module: "workbench-stream-transport",
|
|
sessionId: "ses_secret",
|
|
traceId: "trc_secret",
|
|
outcome: "network",
|
|
diagnostic: {
|
|
code: "workbench_sse_recovery",
|
|
rootCause: "eventsource-error",
|
|
recoveryAction: "refresh-session-messages,schedule-session-list",
|
|
transportState: "degraded",
|
|
scopedKey: "workbench.realtime|ses_secret|trc_secret"
|
|
}
|
|
});
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
const event = events.find((item) => item.kind === "workbench_ui_event" && item.eventType === "runtime_diagnostic");
|
|
assert.equal(event?.module, "workbench-stream-transport");
|
|
assert.equal(event?.diagnosticCode, "workbench_sse_recovery");
|
|
assert.equal(event?.rootCause, "eventsource-error");
|
|
assert.equal(event?.recoveryAction, "refresh-session-messages,schedule-session-list");
|
|
assert.equal(event?.transportState, "degraded");
|
|
assert.match(event?.scopedKey ?? "", /^scope_[0-9a-f]{8}$/u);
|
|
assert.doesNotMatch(JSON.stringify(events), /ses_secret|trc_secret|workbench\.realtime\|/u);
|
|
} finally {
|
|
resetWorkbenchPerformanceForTest();
|
|
restoreBrowserRuntime();
|
|
}
|
|
});
|
|
|
|
test("Workbench SSE flush diagnostics expose bounded queue cost fields", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
const restoreBrowserRuntime = installBrowserProbeRuntime();
|
|
try {
|
|
recordWorkbenchRuntimeDiagnostic({
|
|
module: "workbench-events",
|
|
sessionId: "ses_secret",
|
|
traceId: "trc_secret",
|
|
outcome: "ok",
|
|
diagnostic: {
|
|
code: "workbench_sse_flush",
|
|
reason: "yield",
|
|
eventCount: 5,
|
|
deliveredCount: 4,
|
|
dropped: 1,
|
|
chunkCount: 2,
|
|
replacedByKey: 1,
|
|
flushDurationMs: 17,
|
|
maxItemsPerChunk: 4,
|
|
maxChunkMs: 8
|
|
}
|
|
});
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
const event = events.find((item) => item.kind === "workbench_ui_event" && item.eventType === "runtime_diagnostic");
|
|
assert.equal(event?.state, "sample");
|
|
assert.equal(event?.reason, "sse_flush");
|
|
assert.equal(event?.valueMs, 17);
|
|
assert.equal(event?.eventCount, 5);
|
|
assert.equal(event?.deliveredCount, 4);
|
|
assert.equal(event?.droppedCount, 1);
|
|
assert.equal(event?.chunkCount, 2);
|
|
assert.equal(event?.replacedByKey, 1);
|
|
assert.equal(event?.flushDurationMs, 17);
|
|
assert.equal(event?.maxItemsPerChunk, 4);
|
|
assert.equal(event?.maxChunkMs, 8);
|
|
assert.doesNotMatch(JSON.stringify(events), /ses_secret|trc_secret/u);
|
|
} finally {
|
|
resetWorkbenchPerformanceForTest();
|
|
restoreBrowserRuntime();
|
|
}
|
|
});
|
|
|
|
test("Workbench SSE flush diagnostics request prompt web-performance delivery", async () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
const timers: { delay: number; handler: () => void }[] = [];
|
|
const posts: { url: string; body: string }[] = [];
|
|
const restoreBrowserRuntime = installBrowserProbeRuntime({
|
|
setTimeout: (handler, timeout) => {
|
|
timers.push({ delay: Number(timeout ?? 0), handler: () => invokeTimerHandler(handler) });
|
|
return timers.length;
|
|
},
|
|
clearTimeout: () => undefined,
|
|
fetch: (input, init) => {
|
|
posts.push({ url: String(input), body: String(init?.body ?? "") });
|
|
return Promise.resolve(new Response(null, { status: 204 }));
|
|
}
|
|
});
|
|
try {
|
|
recordWorkbenchRuntimeDiagnostic({
|
|
module: "workbench-events",
|
|
sessionId: "ses_secret",
|
|
traceId: "trc_secret",
|
|
outcome: "ok",
|
|
diagnostic: {
|
|
code: "workbench_sse_flush",
|
|
eventCount: 3,
|
|
deliveredCount: 2,
|
|
dropped: 1,
|
|
chunkCount: 2,
|
|
flushDurationMs: 9,
|
|
maxItemsPerChunk: 4,
|
|
maxChunkMs: 8
|
|
}
|
|
});
|
|
|
|
const priorityTimer = timers.find((timer) => timer.delay === 250);
|
|
assert.ok(priorityTimer);
|
|
priorityTimer.handler();
|
|
await Promise.resolve();
|
|
|
|
assert.equal(posts.length, 1);
|
|
assert.equal(posts[0]?.url, "/v1/web-performance");
|
|
assert.match(posts[0]?.body ?? "", /workbench_sse_flush/u);
|
|
assert.match(posts[0]?.body ?? "", /"eventCount":3/u);
|
|
assert.match(posts[0]?.body ?? "", /"maxChunkMs":8/u);
|
|
assert.equal(drainWorkbenchPerformanceEventsForTest().length, 0);
|
|
} finally {
|
|
resetWorkbenchPerformanceForTest();
|
|
restoreBrowserRuntime();
|
|
}
|
|
});
|
|
|
|
test("Workbench API timing enriches from ResourceTiming and clears the browser buffer", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
let clearCount = 0;
|
|
let configuredBufferSize = 0;
|
|
const startedAtEpochMs = 1_000_000;
|
|
const entries = [resourceTimingEntry({ name: "https://hwlab.example.test/v1/workbench/sessions/ses_secret/messages?limit=100", startTime: 10, duration: 42, responseStart: 37, responseEnd: 52 })];
|
|
const restoreBrowserRuntime = installBrowserProbeRuntime({
|
|
performance: {
|
|
timeOrigin: startedAtEpochMs - 10,
|
|
now: () => 123,
|
|
getEntriesByType: (type: string) => type === "resource" ? entries : [],
|
|
setResourceTimingBufferSize: (value: number) => {
|
|
configuredBufferSize = value;
|
|
},
|
|
clearResourceTimings: () => {
|
|
clearCount += 1;
|
|
entries.splice(0, entries.length);
|
|
}
|
|
}
|
|
});
|
|
try {
|
|
recordWorkbenchApiRequest({ route: "/v1/workbench/sessions/ses_secret/messages?limit=100", method: "GET", status: 200, outcome: "ok", startedAtEpochMs, endedAtEpochMs: startedAtEpochMs + 60 });
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
const event = events.find((item) => item.kind === "workbench_ui_event" && item.eventType === "api_request");
|
|
assert.equal(configuredBufferSize, 2000);
|
|
assert.equal(clearCount, 1);
|
|
assert.equal(entries.length, 0);
|
|
assert.equal(event?.route, "/v1/workbench/sessions/:id/messages");
|
|
assert.equal(event?.resourceDurationMs, 42);
|
|
assert.equal(event?.resourceRequestWaitMs, 15);
|
|
assert.equal(event?.resourceResponseTransferMs, 15);
|
|
} finally {
|
|
resetWorkbenchPerformanceForTest();
|
|
restoreBrowserRuntime();
|
|
}
|
|
});
|
|
|
|
test("Workbench session switch treats visible empty sessions as successful first paint", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
startWorkbenchSessionSwitch({ sessionId: "ses_empty_visible", source: "rail", targetState: "empty", cache: "cold" });
|
|
acknowledgeWorkbenchVisible({ messages: [], activeSessionId: "ses_empty_visible", detailLoading: false });
|
|
|
|
const events = drainWorkbenchPerformanceEventsForTest();
|
|
const switchEvent = events.find((event) => event.kind === "workbench_journey" && event.journey === "session_switch_first_visible");
|
|
assert.equal(switchEvent?.outcome, "ok");
|
|
assert.equal(switchEvent?.targetState, "empty");
|
|
});
|
|
|
|
test("Workbench submit first visible waits for terminal final or tool output", () => {
|
|
resetWorkbenchPerformanceForTest();
|
|
const wallBase = Date.now();
|
|
const createdAt = new Date(wallBase - 12_000).toISOString();
|
|
const appendedAt = new Date(wallBase - 11_500).toISOString();
|
|
const backendEvent = { label: "agentrun:run:createdAgentRun", backend: "agentrun-v01/codex", createdAt, appendedAt, message: "AgentRun created." } as TraceEvent;
|
|
startWorkbenchSubmitJourney({ traceId: "trc_secret", sessionId: "ses_secret", entry: "existing", backend: "codex", transport: "sse" });
|
|
markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "rest_gap", events: [backendEvent] });
|
|
markWorkbenchTraceProjected("trc_secret");
|
|
acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [backendEvent])], activeSessionId: "ses_secret", detailLoading: false });
|
|
|
|
const backendOnlyEvents = drainWorkbenchPerformanceEventsForTest();
|
|
assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
|
|
assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive"), false);
|
|
assert.ok(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "created_to_append" && event.eventType === "backend" && event.transport === "rest_gap"));
|
|
assert.ok(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "receive_to_project" && event.eventType === "backend" && event.transport === "rest_gap"));
|
|
assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "backend" && event.outcome === "stale"), false);
|
|
|
|
acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [{ type: "assistant_message", status: "running", message: "progress only" } as TraceEvent])], activeSessionId: "ses_secret", detailLoading: false });
|
|
const progressEvents = drainWorkbenchPerformanceEventsForTest();
|
|
assert.equal(progressEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
|
|
|
|
acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [{ type: "tool_call", command: "pwd" } as TraceEvent])], activeSessionId: "ses_secret", detailLoading: false });
|
|
const toolEvents = drainWorkbenchPerformanceEventsForTest();
|
|
assert.ok(toolEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.outcome === "ok"));
|
|
});
|
|
|
|
function userMessage(sessionId: string): ChatMessage {
|
|
return { id: "msg_user", role: "user", title: "User", text: "hello", status: "sent", createdAt: new Date().toISOString(), sessionId };
|
|
}
|
|
|
|
function agentMessage(sessionId: string, traceId: string): ChatMessage {
|
|
return {
|
|
id: "msg_agent",
|
|
role: "agent",
|
|
title: "Code Agent",
|
|
text: "visible",
|
|
status: "completed",
|
|
createdAt: new Date().toISOString(),
|
|
sessionId,
|
|
traceId,
|
|
runnerTrace: { traceId, sessionId, eventCount: 1, events: [{ type: "assistant_message" }] }
|
|
};
|
|
}
|
|
|
|
function traceOnlyAgentMessage(sessionId: string, traceId: string, events: TraceEvent[]): ChatMessage {
|
|
return {
|
|
id: "msg_agent_trace",
|
|
role: "agent",
|
|
title: "Code Agent",
|
|
text: "",
|
|
status: "running",
|
|
createdAt: new Date().toISOString(),
|
|
sessionId,
|
|
traceId,
|
|
runnerTrace: { traceId, sessionId, eventCount: events.length, events }
|
|
};
|
|
}
|
|
|
|
function installBrowserProbeRuntime(options: { performance?: FakePerformance; setTimeout?: BrowserSetTimeout; clearTimeout?: BrowserClearTimeout; fetch?: BrowserFetch } = {}): () => void {
|
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
const previousDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
|
|
const previousPerformance = Object.getOwnPropertyDescriptor(globalThis, "performance");
|
|
const previousFetch = Object.getOwnPropertyDescriptor(globalThis, "fetch");
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
location: { href: "https://hwlab.example.test/workbench", pathname: "/workbench", hash: "" },
|
|
crypto: globalThis.crypto,
|
|
addEventListener: () => undefined,
|
|
setTimeout: options.setTimeout ?? globalThis.setTimeout,
|
|
clearTimeout: options.clearTimeout ?? globalThis.clearTimeout
|
|
}
|
|
});
|
|
Object.defineProperty(globalThis, "document", {
|
|
configurable: true,
|
|
value: {
|
|
visibilityState: "visible",
|
|
addEventListener: () => undefined
|
|
}
|
|
});
|
|
if (options.performance) {
|
|
Object.defineProperty(globalThis, "performance", {
|
|
configurable: true,
|
|
value: options.performance
|
|
});
|
|
}
|
|
if (options.fetch) {
|
|
Object.defineProperty(globalThis, "fetch", {
|
|
configurable: true,
|
|
value: options.fetch
|
|
});
|
|
}
|
|
return () => {
|
|
restoreGlobalProperty("window", previousWindow);
|
|
restoreGlobalProperty("document", previousDocument);
|
|
restoreGlobalProperty("performance", previousPerformance);
|
|
restoreGlobalProperty("fetch", previousFetch);
|
|
};
|
|
}
|
|
|
|
type BrowserSetTimeout = (handler: TimerHandler, timeout?: number, ...arguments_: unknown[]) => number;
|
|
type BrowserClearTimeout = (handle?: number) => void;
|
|
type BrowserFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
interface FakePerformance {
|
|
timeOrigin: number;
|
|
now: () => number;
|
|
getEntriesByType: (type: string) => unknown[];
|
|
setResourceTimingBufferSize?: (value: number) => void;
|
|
clearResourceTimings?: () => void;
|
|
}
|
|
|
|
function invokeTimerHandler(handler: TimerHandler): void {
|
|
if (typeof handler === "function") {
|
|
handler();
|
|
return;
|
|
}
|
|
throw new TypeError("string timer handlers are not supported by this test runtime");
|
|
}
|
|
|
|
function resourceTimingEntry(input: { name: string; startTime: number; duration: number; responseStart: number; responseEnd: number }): PerformanceResourceTiming {
|
|
return {
|
|
name: input.name,
|
|
startTime: input.startTime,
|
|
duration: input.duration,
|
|
fetchStart: input.startTime,
|
|
requestStart: input.startTime + 12,
|
|
responseStart: input.responseStart,
|
|
responseEnd: input.responseEnd,
|
|
transferSize: 1000,
|
|
encodedBodySize: 800,
|
|
decodedBodySize: 1600,
|
|
nextHopProtocol: "h2",
|
|
initiatorType: "fetch",
|
|
serverTiming: []
|
|
} as unknown as PerformanceResourceTiming;
|
|
}
|
|
|
|
function restoreGlobalProperty(key: "window" | "document" | "performance" | "fetch", descriptor: PropertyDescriptor | undefined): void {
|
|
if (descriptor) {
|
|
Object.defineProperty(globalThis, key, descriptor);
|
|
return;
|
|
}
|
|
Reflect.deleteProperty(globalThis, key);
|
|
}
|