3067 lines
147 KiB
TypeScript
3067 lines
147 KiB
TypeScript
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery.
|
|
// Responsibility: Workbench read model, durable projection diagnostics, and pure-GET regression tests.
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudApiServer } from "./server.ts";
|
|
import { createBackendPerformanceStore } from "./backend-performance.ts";
|
|
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
|
import { classifyWorkbenchReadModelFailure } from "./server-workbench-http.ts";
|
|
import { codeAgentTurnStatusPayload, createCodeAgentChatResultStore } from "./server-code-agent-http.ts";
|
|
import { createWorkbenchTurnProjection, durableTraceStatus, projectionDiagnostics, traceTerminalEvidence } from "./workbench-turn-projection.ts";
|
|
|
|
const ACTOR = { id: "usr_workbench_reader", username: "reader", displayName: "Reader", role: "user", status: "active" };
|
|
|
|
test("workbench projection diagnostics exposes AgentRun refresh errors without terminal inference", () => {
|
|
const traceId = "trc_projection_refresh_degraded";
|
|
const result = {
|
|
traceId,
|
|
status: "running",
|
|
agentRun: { runId: "run_projection_refresh_degraded", commandId: "cmd_projection_refresh_degraded", status: "running" }
|
|
};
|
|
const trace = { traceId, status: "running", events: [], eventCount: 0, updatedAt: "2026-06-18T01:00:00.000Z" };
|
|
const projection = createWorkbenchTurnProjection({ traceId, result, trace });
|
|
const diagnostics = projectionDiagnostics({
|
|
traceId,
|
|
result,
|
|
trace,
|
|
projection,
|
|
refreshError: { code: "agentrun_result_timeout", message: "状态更新超时,AgentRun 结果暂不可见。", retryable: true, timeoutMs: 2500 }
|
|
});
|
|
assert.equal(projection.status, "running");
|
|
assert.equal(diagnostics.projectionStatus, "blocked");
|
|
assert.equal(diagnostics.projectionHealth, "degraded");
|
|
assert.equal(diagnostics.blocker.code, "agentrun_result_timeout");
|
|
assert.equal(diagnostics.blocker.message, "状态更新超时,AgentRun 结果暂不可见。");
|
|
assert.equal(diagnostics.sourceRunId, "run_projection_refresh_degraded");
|
|
assert.equal(diagnostics.sourceCommandId, "cmd_projection_refresh_degraded");
|
|
});
|
|
|
|
test("workbench projection diagnostics keeps projecting health distinct from caught-up", () => {
|
|
const traceId = "trc_projection_health_projecting";
|
|
const result = {
|
|
traceId,
|
|
status: "running",
|
|
agentRun: { runId: "run_projection_health_projecting", commandId: "cmd_projection_health_projecting", status: "running" }
|
|
};
|
|
const trace = {
|
|
traceId,
|
|
status: "running",
|
|
events: [{ traceId, seq: 7, sourceSeq: 7, type: "backend_status", status: "running", createdAt: "2026-06-19T04:40:00.000Z" }],
|
|
eventCount: 1,
|
|
updatedAt: "2026-06-19T04:40:00.000Z"
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result, trace });
|
|
const diagnostics = projectionDiagnostics({ traceId, result, trace, projection });
|
|
assert.equal(projection.status, "running");
|
|
assert.equal(diagnostics.projectionStatus, "projecting");
|
|
assert.equal(diagnostics.projectionHealth, "projecting");
|
|
});
|
|
|
|
test("workbench runtime retryable dependency failures classify as 503 (#2020)", () => {
|
|
const error = new Error("fetch failed");
|
|
error.name = "WorkbenchRuntimeDependencyError";
|
|
error.code = "workbench_runtime_unreachable";
|
|
error.data = {
|
|
serviceId: "hwlab-workbench-runtime",
|
|
retryable: true,
|
|
transient: true,
|
|
valuesRedacted: true
|
|
};
|
|
const classified = classifyWorkbenchReadModelFailure(error);
|
|
assert.equal(classified.statusCode, 503);
|
|
assert.equal(classified.code, "workbench_runtime_unreachable");
|
|
assert.equal(classified.retryable, true);
|
|
assert.equal(classified.transient, true);
|
|
assert.equal(classified.serviceId, "hwlab-workbench-runtime");
|
|
});
|
|
|
|
test("workbench turn projection keeps lastEventAt on the latest event instead of stale admission timing (#1889)", () => {
|
|
const traceId = "trc_turn_timing_latest_event";
|
|
const result = {
|
|
traceId,
|
|
status: "running",
|
|
startedAt: "2026-06-23T21:43:48.740Z",
|
|
lastEventAt: "2026-06-23T21:43:48.740Z",
|
|
timing: {
|
|
startedAt: "2026-06-23T21:43:48.740Z",
|
|
lastEventAt: "2026-06-23T21:43:48.740Z",
|
|
durationMs: null
|
|
}
|
|
};
|
|
const trace = {
|
|
traceId,
|
|
status: "running",
|
|
updatedAt: "2026-06-23T21:44:33.486Z",
|
|
lastEventAt: "2026-06-23T21:44:31.000Z",
|
|
events: [
|
|
{ seq: 1, type: "admitted", createdAt: "2026-06-23T21:43:48.740Z" },
|
|
{ seq: 2, type: "commandExecution", status: "completed", updatedAt: "2026-06-23T21:44:31.000Z" }
|
|
],
|
|
eventCount: 2
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result, trace });
|
|
assert.equal(projection.startedAt, "2026-06-23T21:43:48.740Z");
|
|
assert.equal(projection.lastEventAt, "2026-06-23T21:44:33.486Z");
|
|
assert.equal(projection.timing.lastEventAt, "2026-06-23T21:44:33.486Z");
|
|
});
|
|
|
|
test("workbench turn projection treats retryable provider stream disconnect as active", () => {
|
|
const traceId = "trc_retryable_provider_stream_disconnect";
|
|
const result = {
|
|
traceId,
|
|
status: "canceled",
|
|
failureKind: "provider-stream-disconnected",
|
|
willRetry: true,
|
|
agentRun: {
|
|
runId: "run_retryable_provider_stream_disconnect",
|
|
commandId: "cmd_retryable_provider_stream_disconnect",
|
|
terminalStatus: "cancelled",
|
|
failureKind: "provider-stream-disconnected",
|
|
willRetry: true
|
|
},
|
|
providerTrace: { failureKind: "provider-stream-disconnected", willRetry: true }
|
|
};
|
|
const trace = {
|
|
traceId,
|
|
status: "canceled",
|
|
events: [
|
|
{ seq: 1, type: "error", status: "retrying", errorCode: "provider-stream-disconnected", willRetry: true, terminal: false },
|
|
{ seq: 2, type: "result", status: "canceled", errorCode: "provider-stream-disconnected", willRetry: true, terminal: true }
|
|
],
|
|
eventCount: 2
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result, trace });
|
|
assert.equal(projection.status, "retrying");
|
|
assert.equal(projection.running, true);
|
|
assert.equal(projection.terminal, false);
|
|
assert.equal(projection.finalResponse, null);
|
|
assert.equal(durableTraceStatus(trace.events), "retrying");
|
|
assert.equal(traceTerminalEvidence(trace), null);
|
|
|
|
const completedProjection = createWorkbenchTurnProjection({
|
|
traceId,
|
|
result: {
|
|
...result,
|
|
status: "completed",
|
|
finalResponse: "retry recovered",
|
|
agentRun: { ...result.agentRun, terminalStatus: "completed" }
|
|
},
|
|
trace: {
|
|
...trace,
|
|
status: "completed",
|
|
events: [
|
|
...trace.events,
|
|
{ seq: 3, type: "assistant", status: "completed", message: "retry recovered", terminal: true, replyAuthority: true }
|
|
],
|
|
eventCount: 3
|
|
}
|
|
});
|
|
assert.equal(completedProjection.status, "completed");
|
|
assert.equal(completedProjection.running, false);
|
|
assert.equal(completedProjection.terminal, true);
|
|
assert.equal(completedProjection.finalResponse?.text, "retry recovered");
|
|
});
|
|
|
|
test("workbench turn projection sanitizes legacy agentrun-unreachable into retryable AgentRun transport interruption (#1853)", () => {
|
|
const traceId = "trc_retryable_legacy_agentrun_unreachable";
|
|
const result = {
|
|
traceId,
|
|
status: "failed",
|
|
failureKind: "agentrun-unreachable",
|
|
willRetry: true,
|
|
agentRun: {
|
|
runId: "run_retryable_legacy_agentrun_unreachable",
|
|
commandId: "cmd_retryable_legacy_agentrun_unreachable",
|
|
terminalStatus: "failed",
|
|
failureKind: "agentrun-unreachable",
|
|
willRetry: true
|
|
},
|
|
providerTrace: { failureKind: "agentrun-unreachable", willRetry: true }
|
|
};
|
|
const trace = {
|
|
traceId,
|
|
status: "failed",
|
|
events: [
|
|
{ seq: 1, type: "error", status: "failed", errorCode: "agentrun-unreachable", failureKind: "agentrun-unreachable", willRetry: true, terminal: true }
|
|
],
|
|
eventCount: 1
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result, trace });
|
|
assert.equal(projection.status, "retrying");
|
|
assert.equal(projection.running, true);
|
|
assert.equal(projection.terminal, false);
|
|
assert.equal(projection.finalResponse, null);
|
|
assert.equal(projection.source, "provider-retry");
|
|
assert.equal(projection.terminalEvidence, null);
|
|
assert.equal(durableTraceStatus(trace.events), "retrying");
|
|
assert.equal(traceTerminalEvidence(trace), null);
|
|
});
|
|
|
|
test("workbench turn projection seals final response from authoritative terminal assistant trace event (#1629)", () => {
|
|
const traceId = "trc_workbench_terminal_assistant_final";
|
|
const finalText = [
|
|
"全部六份数据到手!下面是完整的六语言终极性能对比:",
|
|
"",
|
|
"| language | runtime | status |",
|
|
"|---|---|---|",
|
|
"| Lua | LuaJIT | pass |"
|
|
].join("\n");
|
|
const trace = {
|
|
traceId,
|
|
status: "completed",
|
|
events: [
|
|
{ seq: 1, type: "assistant_message", status: "running", message: "正在补 Lua 基准测试。" },
|
|
{ seq: 2, type: "assistant_message", status: "completed", replyAuthority: true, final: true, message: finalText },
|
|
{ seq: 3, type: "result", status: "completed", terminal: true, label: "agentrun:terminal:completed" }
|
|
],
|
|
eventCount: 3
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result: { traceId, status: "completed" }, trace });
|
|
assert.equal(projection.status, "completed");
|
|
assert.equal(projection.terminal, true);
|
|
assert.equal(projection.finalResponse.text, finalText);
|
|
assert.equal(projection.assistantText, finalText);
|
|
assert.equal(projection.terminalEvidence.finalResponse.text, finalText);
|
|
});
|
|
|
|
test("workbench turn projection keeps progress-only assistant trace text out of final response (#1629)", () => {
|
|
const traceId = "trc_workbench_terminal_without_authoritative_final";
|
|
const trace = {
|
|
traceId,
|
|
status: "completed",
|
|
events: [
|
|
{ seq: 1, type: "assistant_message", status: "running", message: "这只是运行中的进度。" },
|
|
{ seq: 2, type: "result", status: "completed", terminal: true, label: "agentrun:terminal:completed" }
|
|
],
|
|
eventCount: 2
|
|
};
|
|
const projection = createWorkbenchTurnProjection({ traceId, result: { traceId, status: "completed" }, trace });
|
|
const diagnostics = projectionDiagnostics({ traceId, result: { traceId, status: "completed" }, trace, projection });
|
|
assert.equal(projection.status, "running");
|
|
assert.equal(projection.running, true);
|
|
assert.equal(projection.terminal, false);
|
|
assert.equal(projection.terminalObserved, true);
|
|
assert.equal(projection.terminalObservedStatus, "completed");
|
|
assert.equal(projection.waitingFor, "final_response");
|
|
assert.equal(projection.finalResponse, null);
|
|
assert.equal(projection.assistantText, null);
|
|
assert.equal(diagnostics.projectionStatus, "projecting");
|
|
assert.equal(diagnostics.projectionHealth, "projecting");
|
|
assert.equal(diagnostics.waitingFor, "final_response");
|
|
});
|
|
|
|
test("code agent turn status keeps completed AgentRun without final response unsealed", () => {
|
|
const traceId = "trc_code_agent_terminal_without_final";
|
|
const payload = codeAgentTurnStatusPayload({
|
|
traceId,
|
|
result: {
|
|
traceId,
|
|
status: "completed",
|
|
agentRun: {
|
|
runId: "run_code_agent_terminal_without_final",
|
|
commandId: "cmd_code_agent_terminal_without_final",
|
|
status: "completed",
|
|
terminalStatus: "completed"
|
|
},
|
|
runnerTrace: {
|
|
traceId,
|
|
status: "completed",
|
|
events: [
|
|
{ seq: 1, type: "assistant_message", status: "running", message: "progress only" },
|
|
{ seq: 2, type: "terminal_status", terminal: true, terminalStatus: "completed" }
|
|
]
|
|
}
|
|
},
|
|
snapshot: null,
|
|
resultPollError: null,
|
|
refreshError: null,
|
|
options: { env: {} }
|
|
});
|
|
assert.equal(payload.status, "running");
|
|
assert.equal(payload.running, true);
|
|
assert.equal(payload.terminal, false);
|
|
assert.equal(payload.terminalObserved, true);
|
|
assert.equal(payload.terminalObservedStatus, "completed");
|
|
assert.equal(payload.terminalSealBlocked, true);
|
|
assert.equal(payload.waitingFor, "final_response");
|
|
assert.equal(payload.finalResponse, null);
|
|
});
|
|
|
|
test("code agent turn status keeps failed AgentRun without final response unsealed", () => {
|
|
const traceId = "trc_code_agent_failed_without_final";
|
|
const payload = codeAgentTurnStatusPayload({
|
|
traceId,
|
|
result: {
|
|
traceId,
|
|
status: "failed",
|
|
agentRun: {
|
|
runId: "run_code_agent_failed_without_final",
|
|
commandId: "cmd_code_agent_failed_without_final",
|
|
status: "failed",
|
|
terminalStatus: "failed"
|
|
},
|
|
runnerTrace: {
|
|
traceId,
|
|
status: "failed",
|
|
events: [
|
|
{ seq: 1, type: "assistant_message", status: "running", message: "progress only" },
|
|
{ seq: 2, type: "terminal_status", terminal: true, terminalStatus: "failed" }
|
|
]
|
|
}
|
|
},
|
|
snapshot: null,
|
|
resultPollError: null,
|
|
refreshError: null,
|
|
options: { env: {} }
|
|
});
|
|
assert.equal(payload.status, "running");
|
|
assert.equal(payload.running, true);
|
|
assert.equal(payload.terminal, false);
|
|
assert.equal(payload.terminalObserved, true);
|
|
assert.equal(payload.terminalObservedStatus, "failed");
|
|
assert.equal(payload.terminalSealBlocked, true);
|
|
assert.equal(payload.waitingFor, "final_response");
|
|
assert.equal(payload.finalResponse, null);
|
|
});
|
|
|
|
test("code agent turn status seals failed AgentRun when final response is authoritative", () => {
|
|
const traceId = "trc_code_agent_failed_with_final";
|
|
const payload = codeAgentTurnStatusPayload({
|
|
traceId,
|
|
result: {
|
|
traceId,
|
|
status: "failed",
|
|
finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected", status: "failed" },
|
|
agentRun: {
|
|
runId: "run_code_agent_failed_with_final",
|
|
commandId: "cmd_code_agent_failed_with_final",
|
|
status: "failed",
|
|
terminalStatus: "failed"
|
|
}
|
|
},
|
|
snapshot: null,
|
|
resultPollError: null,
|
|
refreshError: null,
|
|
options: { env: {} }
|
|
});
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.running, false);
|
|
assert.equal(payload.terminal, true);
|
|
assert.equal(payload.terminalSealBlocked, false);
|
|
assert.equal(payload.finalResponse.text, "Workbench terminal failed: provider-stream-disconnected");
|
|
});
|
|
|
|
test("code agent turn status seals completed AgentRun when finalText is authoritative", () => {
|
|
const traceId = "trc_code_agent_terminal_with_final_text";
|
|
const payload = codeAgentTurnStatusPayload({
|
|
traceId,
|
|
result: {
|
|
traceId,
|
|
status: "completed",
|
|
finalText: "authoritative final",
|
|
agentRun: {
|
|
runId: "run_code_agent_terminal_with_final_text",
|
|
commandId: "cmd_code_agent_terminal_with_final_text",
|
|
status: "completed",
|
|
terminalStatus: "completed"
|
|
}
|
|
},
|
|
snapshot: null,
|
|
resultPollError: null,
|
|
refreshError: null,
|
|
options: { env: {} }
|
|
});
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.running, false);
|
|
assert.equal(payload.terminal, true);
|
|
assert.equal(payload.terminalSealBlocked, false);
|
|
assert.equal(payload.finalResponse.text, "authoritative final");
|
|
});
|
|
|
|
test("workbench trace event page exposes projectedSeq cursor range from durable facts", async () => {
|
|
const traceId = "trc_workbench_trace_mixed_cursor";
|
|
const session = {
|
|
id: "ses_workbench_trace_mixed_cursor",
|
|
projectId: "prj_workbench_authority_session",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_trace_mixed_cursor",
|
|
threadId: "thread-workbench-trace-mixed-cursor",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-19T04:10:17.060Z",
|
|
session: { sessionStatus: "completed", lastTraceId: traceId, messages: [{ role: "user", text: "trace", traceId }] }
|
|
};
|
|
const mixedEvents = [
|
|
{ projectedSeq: 7, source: "agentrun", sourceSeq: 1, type: "backend", status: "running", label: "projected:7", createdAt: "2026-06-19T01:27:27.092Z" },
|
|
{ projectedSeq: 1, source: "agentrun", sourceSeq: 99, type: "backend", status: "running", label: "projected:1", createdAt: "2026-06-19T04:10:15.362Z" },
|
|
{ projectedSeq: 11, source: "agentrun", sourceSeq: 4, type: "backend", status: "running", label: "projected:11", createdAt: "2026-06-19T01:27:34.128Z" },
|
|
{ projectedSeq: 5, source: "agentrun", sourceSeq: 4, type: "backend", status: "running", label: "projected:5", createdAt: "2026-06-19T04:10:15.664Z" },
|
|
{ projectedSeq: 3, source: "agentrun", sourceSeq: 5, type: "backend", status: "running", label: "projected:3", createdAt: "2026-06-19T01:27:34.324Z" },
|
|
{ projectedSeq: 9, source: "agentrun", sourceSeq: 5, type: "backend", status: "running", label: "projected:9", createdAt: "2026-06-19T04:10:16.013Z" },
|
|
{ projectedSeq: 4, type: "result", status: "completed", label: "projected:4", terminal: true, createdAt: "2026-06-19T01:27:34.324Z" },
|
|
{ projectedSeq: 2, type: "request", status: "accepted", label: "projected:2", createdAt: "2026-06-19T04:10:15.362Z" },
|
|
{ projectedSeq: 6, type: "backend", status: "running", label: "projected:6", createdAt: "2026-06-19T04:10:15.664Z" },
|
|
{ projectedSeq: 8, type: "backend", status: "running", label: "projected:8", createdAt: "2026-06-19T04:10:16.013Z" },
|
|
{ projectedSeq: 10, type: "backend", status: "running", label: "projected:10", createdAt: "2026-06-19T04:10:17.060Z" }
|
|
];
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events: mixedEvents, status: "completed" }] });
|
|
const server = createCloudApiServer({ accessController, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=25`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.events.length, mixedEvents.length);
|
|
assert.deepEqual(trace.body.events.map((event) => event.seq), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
|
assert.equal(trace.body.events[0].label, "projected:1");
|
|
assert.equal(trace.body.events.at(-1).label, "projected:11");
|
|
assert.deepEqual(trace.body.range, { afterProjectedSeq: 0, fromProjectedSeq: 1, toProjectedSeq: 11, limit: 25, returned: 11, total: 11 });
|
|
assert.equal(trace.body.nextProjectedSeq, 11);
|
|
assert.equal(trace.body.hasMore, false);
|
|
assert.equal(trace.body.nextCursor, null);
|
|
|
|
const middle = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?cursor=projected:4&limit=3`);
|
|
assert.equal(middle.status, 200);
|
|
assert.deepEqual(middle.body.events.map((event) => event.seq), [5, 6, 7]);
|
|
assert.deepEqual(middle.body.range, { afterProjectedSeq: 4, fromProjectedSeq: 5, toProjectedSeq: 7, limit: 3, returned: 3, total: 11 });
|
|
assert.equal(middle.body.hasMore, true);
|
|
assert.equal(middle.body.nextProjectedSeq, 7);
|
|
assert.equal(middle.body.nextCursor, "projected:7");
|
|
|
|
const empty = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?afterProjectedSeq=999999&limit=5`);
|
|
assert.equal(empty.status, 200);
|
|
assert.deepEqual(empty.body.events, []);
|
|
assert.deepEqual(empty.body.range, { afterProjectedSeq: 999999, fromProjectedSeq: null, toProjectedSeq: null, limit: 5, returned: 0, total: 11 });
|
|
assert.equal(empty.body.nextProjectedSeq, 999999);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace event page pushes projectedSeq cursor into durable trace facts", async () => {
|
|
const traceId = "trc_long_trace_projection_page";
|
|
const session = {
|
|
id: "ses_long_trace_projection_page",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_long_trace_projection_page",
|
|
threadId: "thread-long-trace-projection-page",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-20T02:30:00.000Z",
|
|
session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: "long trace", traceId }] }
|
|
};
|
|
const events = Array.from({ length: 2600 }, (_, index) => ({
|
|
seq: index + 1,
|
|
sourceSeq: index + 1,
|
|
projectedSeq: index + 1,
|
|
type: "backend",
|
|
label: `event:${index + 1}`,
|
|
createdAt: new Date(Date.UTC(2026, 5, 20, 2, 30, index % 60)).toISOString()
|
|
}));
|
|
const queries = [];
|
|
const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, events, status: "running" }], queries });
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?afterProjectedSeq=2500&limit=25`);
|
|
assert.equal(trace.status, 200);
|
|
assert.deepEqual(trace.body.events.map((event) => event.seq), Array.from({ length: 25 }, (_, index) => 2501 + index));
|
|
assert.equal(trace.body.hasMore, true);
|
|
assert.equal(trace.body.nextCursor, "projected:2525");
|
|
|
|
const traceQueries = queries.filter((query) => Array.isArray(query.families) && query.families.includes("traceEvents"));
|
|
assert.equal(traceQueries.length, 1);
|
|
assert.deepEqual(traceQueries[0].families, ["traceEvents"]);
|
|
assert.equal(traceQueries[0].traceId, traceId);
|
|
assert.equal(traceQueries[0].afterProjectedSeq, 2500);
|
|
assert.equal(traceQueries[0].limit, 26);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace events stay visible after session lastTraceId advances", async () => {
|
|
const firstTraceId = "trc_workbench_historical_visible_first";
|
|
const secondTraceId = "trc_workbench_historical_visible_second";
|
|
const session = {
|
|
id: "ses_workbench_historical_visible",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_historical_visible",
|
|
threadId: "thread-workbench-historical-visible",
|
|
lastTraceId: firstTraceId,
|
|
updatedAt: "2026-06-20T10:00:00.000Z",
|
|
session: { sessionStatus: "completed", lastTraceId: firstTraceId, messages: [{ role: "user", text: "first", traceId: firstTraceId, status: "sent" }, { role: "agent", text: "OK", traceId: firstTraceId, status: "completed" }] }
|
|
};
|
|
const facts = buildDurableFactsForSession({
|
|
session,
|
|
status: "completed",
|
|
finalText: "OK",
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "request", status: "accepted", label: "first:accepted", createdAt: "2026-06-20T09:59:58.000Z" },
|
|
{ projectedSeq: 2, sourceSeq: 2, type: "result", status: "completed", label: "first:completed", terminal: true, createdAt: "2026-06-20T10:00:00.000Z" }
|
|
]
|
|
});
|
|
facts.sessions[0].lastTraceId = secondTraceId;
|
|
facts.sessions[0].status = "running";
|
|
const queries = [];
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
queries.push(params);
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const accessController = {
|
|
store: {},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(firstTraceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.sessionId, session.id);
|
|
assert.deepEqual(trace.body.events.map((event) => event.label), ["first:accepted", "first:completed"]);
|
|
assert.ok(queries.some((query) => query.traceId === firstTraceId && query.families?.includes("messages") && query.families?.includes("turns")));
|
|
assert.ok(queries.some((query) => query.sessionId === session.id && query.families?.includes("sessions") && !query.traceId));
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model exposes session, messages, turn, and trace without write repair", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const writes = { recordAgentSessionOwner: 0, updateWorkspace: 0 };
|
|
const traceId = "trc_workbench_read_model";
|
|
const session = {
|
|
id: "ses_workbench_read_model",
|
|
projectId: "prj_workbench_authority_session",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
startedAt: "2026-06-17T00:00:00.000Z",
|
|
endedAt: "2026-06-17T00:00:03.000Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_read_model",
|
|
threadId: "thread-workbench-read-model",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T00:00:03.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
providerProfile: "codex-api",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "ping", traceId, createdAt: "2026-06-17T00:00:00.000Z" },
|
|
{ role: "assistant", text: "pong", traceId, createdAt: "2026-06-17T00:00:03.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const factQueries = [];
|
|
traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" });
|
|
traceStore.append(traceId, { type: "result", status: "completed", label: "result:completed", terminal: true });
|
|
results.set(traceId, {
|
|
status: "completed",
|
|
traceId,
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: session.conversationId,
|
|
sessionId: session.id,
|
|
threadId: session.threadId,
|
|
finalResponse: "pong",
|
|
agentRun: { runId: "run_workbench_read_model", commandId: "cmd_workbench_read_model", status: "completed" }
|
|
});
|
|
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser(input = {}) {
|
|
return [session];
|
|
},
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; },
|
|
async recordAgentSessionOwner() { writes.recordAgentSessionOwner += 1; throw new Error("read model must not write session owner"); },
|
|
async updateWorkspace() { writes.updateWorkspace += 1; throw new Error("read model must not update workspace"); }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T00:00:00.000Z" },
|
|
{ seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T00:00:03.000Z" }
|
|
],
|
|
status: "completed",
|
|
finalText: "pong",
|
|
runId: "run_workbench_read_model",
|
|
commandId: "cmd_workbench_read_model",
|
|
lastProjectedSeq: 2
|
|
}],
|
|
queries: factQueries
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, "/v1/workbench/sessions");
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.contractVersion, "workbench-sessions-v1");
|
|
assert.equal(sessions.body.sessions[0].sessionId, session.id);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.traceId, traceId);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.lastEventAt, "2026-06-17T00:00:03.000Z");
|
|
assert.equal(sessions.body.sessions[0].turnSummary.durationMs, 3000);
|
|
assert.equal(factQueries[0].projectId, undefined);
|
|
assert.equal(factQueries[0].limit, 21);
|
|
assert.equal(factQueries[0].sessionsOrder, "updated_desc");
|
|
assert.deepEqual(factQueries[0].families, ["sessions"]);
|
|
assert.deepEqual(factQueries[1].families, ["messages", "parts", "turns"]);
|
|
assert.deepEqual(factQueries[1].sessionIds, [session.id]);
|
|
assert.deepEqual(factQueries[2].families, ["checkpoints"]);
|
|
assert.deepEqual(factQueries[2].traceIds, [traceId]);
|
|
|
|
const staleProject = await getJson(port, `/v1/workbench/sessions?projectId=prj_stale_filter&includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(staleProject.status, 400);
|
|
assert.equal(staleProject.body.error.code, "workbench_authority_removed");
|
|
assert.equal(factQueries.length, 3);
|
|
|
|
const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}?includeMessages=false`);
|
|
assert.equal(detail.status, 200);
|
|
assert.equal(detail.body.detailMode, "metadata-only");
|
|
assert.equal(detail.body.messagesIncluded, false);
|
|
assert.equal(detail.body.session.sessionId, session.id);
|
|
assert.equal(detail.body.session.messagePageUrl, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages`);
|
|
assert.equal(detail.body.session.messageCount, 2);
|
|
assert.equal(Object.hasOwn(detail.body.session, "messages"), false);
|
|
|
|
const fullDetail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}?includeMessages=true`);
|
|
assert.equal(fullDetail.status, 200);
|
|
assert.equal(fullDetail.body.detailMode, "full");
|
|
assert.equal(fullDetail.body.messagesIncluded, true);
|
|
assert.equal(fullDetail.body.session.messages.length, 2);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=1`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.contractVersion, "workbench-message-page-v1");
|
|
assert.equal(messages.body.messages.length, 1);
|
|
assert.equal(messages.body.messages[0].role, "user");
|
|
assert.equal(messages.body.messages[0].text, "ping");
|
|
assert.equal(messages.body.messages[0].parts[0].text, "ping");
|
|
assert.equal(messages.body.hasMore, true);
|
|
|
|
const allMessages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=2`);
|
|
const agentMessage = allMessages.body.messages.find((message) => message.role !== "user");
|
|
assert.equal(agentMessage.lastEventAt, "2026-06-17T00:00:03.000Z");
|
|
assert.equal(agentMessage.durationMs, 3000);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.contractVersion, "workbench-turn-snapshot-v1");
|
|
assert.equal(turn.body.turn.traceId, traceId);
|
|
assert.equal(turn.body.turn.userMessageId, messages.body.messages[0].messageId);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.startedAt, "2026-06-17T00:00:00.000Z");
|
|
assert.equal(turn.body.turn.finishedAt, "2026-06-17T00:00:03.000Z");
|
|
assert.equal(turn.body.turn.durationMs, 3000);
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=1`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.contractVersion, "workbench-trace-events-v1");
|
|
assert.equal(trace.body.events.length, 1);
|
|
assert.equal(trace.body.hasMore, true);
|
|
assert.equal(trace.body.durationMs, 3000);
|
|
|
|
const emptyTracePage = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?afterProjectedSeq=99`);
|
|
assert.equal(emptyTracePage.status, 200);
|
|
assert.equal(emptyTracePage.body.events.length, 0);
|
|
assert.equal(emptyTracePage.body.range.fromProjectedSeq, null);
|
|
assert.equal(emptyTracePage.body.range.toProjectedSeq, null);
|
|
assert.equal(emptyTracePage.body.nextProjectedSeq, 99);
|
|
|
|
assert.deepEqual(writes, { recordAgentSessionOwner: 0, updateWorkspace: 0 });
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session messages use turn timeline instead of clustered projection writes", async () => {
|
|
const traceIds = Array.from({ length: 5 }, (_, index) => `trc_workbench_timeline_cluster_${index + 1}`);
|
|
const session = {
|
|
id: "ses_workbench_timeline_cluster",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
startedAt: "2026-06-27T00:00:00.000Z",
|
|
endedAt: "2026-06-27T00:10:00.000Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_timeline_cluster",
|
|
threadId: "thread-workbench-timeline-cluster",
|
|
lastTraceId: traceIds.at(-1),
|
|
updatedAt: "2026-06-27T00:10:00.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
messages: traceIds.flatMap((traceId, index) => {
|
|
const turn = index + 1;
|
|
return [
|
|
{ role: "user", text: `sentinel-${turn}`, traceId, turnId: traceId, projectedSeq: turn, sourceSeq: turn, status: "sent", createdAt: `2026-06-27T00:0${index}:00.000Z` },
|
|
{ role: "agent", text: `ok-${turn}`, traceId, turnId: traceId, projectedSeq: 100 + turn, sourceSeq: 100 + turn, status: "completed", createdAt: `2026-06-27T00:0${index}:30.000Z` }
|
|
];
|
|
}),
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({ sessions: [{ session, status: "completed", lastProjectedSeq: 105 }] });
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=20`);
|
|
assert.equal(messages.status, 200);
|
|
assert.deepEqual(messages.body.messages.map((message) => message.role), ["user", "agent", "user", "agent", "user", "agent", "user", "agent", "user", "agent"]);
|
|
assert.deepEqual(messages.body.messages.map((message) => message.text), ["sentinel-1", "ok-1", "sentinel-2", "ok-2", "sentinel-3", "ok-3", "sentinel-4", "ok-4", "sentinel-5", "ok-5"]);
|
|
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].title, "sentinel-5");
|
|
assert.equal(sessions.body.sessions[0].preview, "ok-5");
|
|
assert.equal(sessions.body.sessions[0].titleSource, "message-projection");
|
|
assert.equal(sessions.body.sessions[0].previewSource, "message-projection");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session messages use prompt admission time when turn seq is stale", async () => {
|
|
const sessionId = "ses_workbench_timeline_stale_turn_seq";
|
|
const turnRows = [
|
|
{ traceId: "trc_stale_turn_01", turnSeq: 1, at: "2026-06-28T08:40:42.000Z", prompt: "sentinel-01", reply: "ok-01" },
|
|
{ traceId: "trc_stale_turn_02", turnSeq: 3, at: "2026-06-28T08:43:51.000Z", prompt: "sentinel-02", reply: "ok-02" },
|
|
{ traceId: "trc_stale_turn_03", turnSeq: 2, at: "2026-06-28T08:44:07.000Z", prompt: "sentinel-03", reply: "ok-03" },
|
|
{ traceId: "trc_stale_turn_04", turnSeq: 5, at: "2026-06-28T08:44:27.000Z", prompt: "sentinel-04", reply: "ok-04" },
|
|
{ traceId: "trc_stale_turn_05", turnSeq: 4, at: "2026-06-28T08:44:44.000Z", prompt: "sentinel-05", reply: "ok-05" }
|
|
];
|
|
const facts = emptyFacts();
|
|
facts.sessions.push({
|
|
sessionId,
|
|
ownerUserId: ACTOR.id,
|
|
ownerRole: ACTOR.role,
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
lastTraceId: turnRows.at(-1).traceId,
|
|
projectedSeq: 99,
|
|
sourceSeq: 99,
|
|
terminal: true,
|
|
sealed: true,
|
|
createdAt: turnRows[0].at,
|
|
updatedAt: "2026-06-28T08:45:00.000Z",
|
|
valuesRedacted: true
|
|
});
|
|
turnRows.forEach((row, index) => {
|
|
const userMessageId = `msg_${row.traceId}_user`;
|
|
const agentMessageId = `msg_${row.traceId}_agent`;
|
|
const finishedAt = new Date(Date.parse(row.at) + 5000).toISOString();
|
|
facts.messages.push(
|
|
{ messageId: userMessageId, sessionId, turnId: row.traceId, traceId: row.traceId, role: "user", status: "sent", text: row.prompt, projectedSeq: 100 + index, sourceSeq: 100 + index, createdAt: row.at, updatedAt: row.at, valuesRedacted: true },
|
|
{ messageId: agentMessageId, sessionId, turnId: row.traceId, traceId: row.traceId, role: "agent", status: "completed", text: row.reply, projectedSeq: 200 + index, sourceSeq: 200 + index, terminal: true, sealed: true, createdAt: finishedAt, updatedAt: finishedAt, timing: { startedAt: row.at, lastEventAt: finishedAt, finishedAt, durationMs: 5000, valuesRedacted: true }, valuesRedacted: true }
|
|
);
|
|
facts.parts.push(
|
|
{ partId: `prt_${row.traceId}_user`, messageId: userMessageId, sessionId, turnId: row.traceId, traceId: row.traceId, partIndex: 0, partType: "text", status: "sent", text: row.prompt, projectedSeq: 100 + index, updatedAt: row.at, valuesRedacted: true },
|
|
{ partId: `prt_${row.traceId}_agent`, messageId: agentMessageId, sessionId, turnId: row.traceId, traceId: row.traceId, partIndex: 0, partType: "final_response", status: "completed", text: row.reply, projectedSeq: 200 + index, terminal: true, sealed: true, updatedAt: finishedAt, valuesRedacted: true }
|
|
);
|
|
facts.turns.push({ turnId: row.traceId, sessionId, traceId: row.traceId, messageId: agentMessageId, status: "completed", projectedSeq: row.turnSeq, sourceSeq: row.turnSeq, terminal: true, sealed: true, startedAt: row.at, lastEventAt: finishedAt, finishedAt, durationMs: 5000, updatedAt: finishedAt, valuesRedacted: true });
|
|
facts.checkpoints.push({ traceId: row.traceId, sessionId, turnId: row.traceId, status: "completed", projectionStatus: "caught_up", projectionHealth: "caught-up", projectedSeq: row.turnSeq, sourceSeq: row.turnSeq, terminal: true, sealed: true, startedAt: row.at, lastEventAt: finishedAt, finishedAt, durationMs: 5000, updatedAt: finishedAt, valuesRedacted: true });
|
|
});
|
|
const runtimeStore = createRuntimeStoreFromFacts(facts);
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages?limit=20`);
|
|
assert.equal(messages.status, 200);
|
|
assert.deepEqual(messages.body.messages.map((message) => message.text), ["sentinel-01", "ok-01", "sentinel-02", "ok-02", "sentinel-03", "ok-03", "sentinel-04", "ok-04", "sentinel-05", "ok-05"]);
|
|
assert.equal(messages.body.roleSequencePrefix, "UAUAUAUAUA");
|
|
assert.equal(messages.body.adjacentSameRoleCount, 0);
|
|
assert.match(messages.body.timelineDigest, /^sha256:[0-9a-f]{64}$/u);
|
|
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(sessionId)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].title, "sentinel-05");
|
|
assert.equal(sessions.body.sessions[0].preview, "ok-05");
|
|
assert.equal(sessions.body.sessions[0].firstUserMessagePreview, "sentinel-01");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session list hides shadowed AgentRun alias rows", async () => {
|
|
const sessionId = "ses_fake_echo_canonical";
|
|
const aliasSessionId = "ses_agentrun_fake_echo_canonical";
|
|
const traceId = "trc_fake_echo_alias";
|
|
const facts = emptyFacts();
|
|
facts.sessions.push(
|
|
{ sessionId: aliasSessionId, ownerUserId: ACTOR.id, ownerRole: ACTOR.role, status: "failed", lastTraceId: traceId, projectedSeq: 20, sourceSeq: 20, updatedAt: "2026-06-28T08:52:18.000Z", valuesRedacted: true },
|
|
{ sessionId, ownerUserId: ACTOR.id, ownerRole: ACTOR.role, status: "completed", lastTraceId: traceId, projectedSeq: 10, sourceSeq: 10, updatedAt: "2026-06-28T08:52:10.000Z", valuesRedacted: true }
|
|
);
|
|
facts.messages.push(
|
|
{ messageId: "msg_fake_user", sessionId, turnId: traceId, traceId, role: "user", status: "sent", text: "ECHO sentinel-01", projectedSeq: 1, sourceSeq: 1, createdAt: "2026-06-28T08:52:00.000Z", updatedAt: "2026-06-28T08:52:00.000Z", valuesRedacted: true },
|
|
{ messageId: "msg_fake_agent", sessionId, turnId: traceId, traceId, role: "agent", status: "completed", text: "ECHO sentinel-01 ok", projectedSeq: 2, sourceSeq: 2, terminal: true, sealed: true, createdAt: "2026-06-28T08:52:05.000Z", updatedAt: "2026-06-28T08:52:05.000Z", valuesRedacted: true }
|
|
);
|
|
facts.parts.push(
|
|
{ partId: "prt_fake_user", messageId: "msg_fake_user", sessionId, turnId: traceId, traceId, partIndex: 0, partType: "text", status: "sent", text: "ECHO sentinel-01", projectedSeq: 1, valuesRedacted: true },
|
|
{ partId: "prt_fake_agent", messageId: "msg_fake_agent", sessionId, turnId: traceId, traceId, partIndex: 0, partType: "final_response", status: "completed", text: "ECHO sentinel-01 ok", projectedSeq: 2, terminal: true, sealed: true, valuesRedacted: true }
|
|
);
|
|
const runtimeStore = createRuntimeStoreFromFacts(facts);
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, "/v1/workbench/sessions?limit=10");
|
|
assert.equal(sessions.status, 200);
|
|
assert.deepEqual(sessions.body.sessions.map((session) => session.sessionId), [sessionId]);
|
|
assert.equal(sessions.body.sessions[0].title, "ECHO sentinel-01");
|
|
assert.equal(sessions.body.sessions[0].preview, "ECHO sentinel-01 ok");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace event continuation returns empty catch-up page instead of 404", async () => {
|
|
const traceId = "trc_workbench_trace_events_catchup";
|
|
const session = {
|
|
id: "ses_workbench_trace_events_catchup",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
startedAt: "2026-06-27T01:00:00.000Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_trace_events_catchup",
|
|
threadId: "thread-workbench-trace-events-catchup",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-27T01:00:05.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
messages: [
|
|
{ role: "user", text: "start", traceId, createdAt: "2026-06-27T01:00:00.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
status: "running",
|
|
projectionStatus: "projecting",
|
|
projectionHealth: "projecting",
|
|
lastProjectedSeq: 5,
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-27T01:00:00.000Z" },
|
|
{ projectedSeq: 2, sourceSeq: 2, type: "tool", status: "running", label: "tool:running", createdAt: "2026-06-27T01:00:05.000Z" }
|
|
]
|
|
}]
|
|
});
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?afterProjectedSeq=2&limit=100`);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.body.status, "projecting");
|
|
assert.deepEqual(response.body.events, []);
|
|
assert.equal(response.body.range.fromProjectedSeq, null);
|
|
assert.equal(response.body.range.toProjectedSeq, null);
|
|
assert.equal(response.body.range.total, 5);
|
|
assert.equal(response.body.nextProjectedSeq, 2);
|
|
assert.equal(response.body.traceLastSeq, 5);
|
|
assert.equal(response.body.fullTraceLoaded, false);
|
|
assert.equal(response.body.projectionStatus, "projecting");
|
|
assert.equal(response.body.projectionHealth, "projecting");
|
|
assert.equal(response.body.diagnostic.code, "workbench_trace_events_missing");
|
|
assert.equal(response.body.blocker, null);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench terminal timing ignores late projection updatedAt after sealed result (#2132)", async () => {
|
|
const traceId = "trc_workbench_terminal_late_projection_update";
|
|
const session = {
|
|
id: "ses_workbench_terminal_late_projection_update",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
startedAt: "2026-06-25T23:07:10.153Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_terminal_late_projection_update",
|
|
threadId: "thread-workbench-terminal-late-projection-update",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-25T23:08:27.097Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
lastTraceId: traceId,
|
|
finalResponse: "P5 durable facts smoke OK",
|
|
messages: [
|
|
{ role: "user", text: "请直接回答:P5 durable facts smoke OK", traceId, status: "sent", createdAt: "2026-06-25T23:07:10.153Z" },
|
|
{ role: "agent", text: "P5 durable facts smoke OK", traceId, status: "completed", createdAt: "2026-06-25T23:07:10.976Z", updatedAt: "2026-06-25T23:08:27.097Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const facts = emptyFacts();
|
|
mergeFacts(facts, buildDurableFactsForSession({
|
|
session,
|
|
status: "completed",
|
|
finalText: "P5 durable facts smoke OK",
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "agentrun:backend:admitted", createdAt: "2026-06-25T23:07:10.153Z", updatedAt: "2026-06-25T23:07:10.153Z" },
|
|
{ projectedSeq: 31, sourceSeq: 31, type: "terminal", eventType: "terminal", status: "completed", label: "agentrun:result:completed", terminal: true, createdAt: "2026-06-25T23:07:32.418Z", occurredAt: "2026-06-25T23:07:32.418Z", updatedAt: "2026-06-25T23:08:27.127Z" }
|
|
],
|
|
lastProjectedSeq: 31
|
|
}));
|
|
const checkpointTiming = {
|
|
startedAt: "2026-06-25T23:07:10.153Z",
|
|
lastEventAt: "2026-06-25T23:07:32.418Z",
|
|
finishedAt: "2026-06-25T23:07:32.418Z",
|
|
durationMs: 22265,
|
|
valuesRedacted: true
|
|
};
|
|
const turnTiming = {
|
|
startedAt: "2026-06-25T23:07:10.976Z",
|
|
lastEventAt: "2026-06-25T23:08:27.097Z",
|
|
finishedAt: "2026-06-25T23:07:32.418Z",
|
|
durationMs: 21442,
|
|
valuesRedacted: true
|
|
};
|
|
for (const record of [...facts.sessions, ...facts.messages.filter((message) => message.role !== "user"), ...facts.turns]) {
|
|
Object.assign(record, turnTiming, { timing: turnTiming, updatedAt: "2026-06-25T23:08:27.097Z" });
|
|
}
|
|
Object.assign(facts.checkpoints[0], checkpointTiming, { timing: checkpointTiming, updatedAt: "2026-06-25T23:08:27.127Z" });
|
|
Object.assign(facts.traceEvents.at(-1), checkpointTiming, { timing: checkpointTiming, updatedAt: "2026-06-25T23:08:27.127Z" });
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, workbenchRuntime: runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.startedAt, "2026-06-25T23:07:10.153Z");
|
|
assert.equal(turn.body.turn.lastEventAt, "2026-06-25T23:07:32.418Z");
|
|
assert.equal(turn.body.turn.finishedAt, "2026-06-25T23:07:32.418Z");
|
|
assert.equal(turn.body.turn.durationMs, 22265);
|
|
assert.equal(turn.body.turn.trace.updatedAt, "2026-06-25T23:08:27.127Z");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=80`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.lastEventAt, "2026-06-25T23:07:32.418Z");
|
|
assert.equal(trace.body.finishedAt, "2026-06-25T23:07:32.418Z");
|
|
assert.equal(trace.body.durationMs, 22265);
|
|
assert.equal(trace.body.events.at(-1).updatedAt, "2026-06-25T23:08:27.127Z");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model recovers trace events from durable projection without duplicating final response", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_durable_reload";
|
|
const session = {
|
|
id: "ses_workbench_durable_reload",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
startedAt: "2026-06-17T01:00:00.000Z",
|
|
endedAt: "2026-06-17T01:00:04.000Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_durable_reload",
|
|
threadId: "thread-workbench-durable-reload",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T01:00:04.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
lastTraceId: traceId,
|
|
finalResponse: "durable pong",
|
|
messages: [
|
|
{ role: "user", text: "durable ping", traceId, createdAt: "2026-06-17T01:00:00.000Z" },
|
|
{ role: "agent", text: "durable pong", traceId, createdAt: "2026-06-17T01:00:04.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const durableEvents = [
|
|
{ traceId, seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T01:00:00.000Z", valuesPrinted: false },
|
|
{ traceId, seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T01:00:04.000Z", valuesPrinted: false }
|
|
];
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const factQueries = [];
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{ session, events: durableEvents, status: "completed", finalText: "durable pong", lastProjectedSeq: 2 }],
|
|
queries: factQueries
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.total, 2);
|
|
assert.deepEqual(messages.body.messages.map((message) => message.role), ["user", "agent"]);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.assistantMessageId, messages.body.messages[1].messageId);
|
|
assert.equal(turn.body.turn.trace.eventCount, 2);
|
|
assert.equal(turn.body.turn.trace.status, "completed");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.events.length, 2);
|
|
assert.equal(trace.body.traceStatus, "completed");
|
|
assert.equal(trace.body.hasMore, false);
|
|
assert.ok(factQueries.length >= 3);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model canonicalizes duplicate assistant facts for the same trace", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_duplicate_agent_message";
|
|
const preferredMessageId = "msg_workbench_duplicate_agent_message_agent";
|
|
const duplicateMessageId = "msg_3ae9b11ac11c2dcf816980916f1152b0";
|
|
const finalText = "duplicate history final response";
|
|
const session = {
|
|
id: "ses_workbench_duplicate_agent_message",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
startedAt: "2026-06-17T01:20:00.000Z",
|
|
endedAt: "2026-06-17T01:20:16.000Z",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_duplicate_agent_message",
|
|
threadId: "thread-workbench-duplicate-agent-message",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T01:20:16.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ messageId: "msg_workbench_duplicate_agent_message_user", role: "user", text: "duplicate ping", traceId, createdAt: "2026-06-17T01:20:00.000Z" },
|
|
{ messageId: preferredMessageId, role: "agent", text: "", traceId, status: "completed", createdAt: "2026-06-17T01:20:16.000Z", updatedAt: "2026-06-17T01:20:16.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const facts = buildDurableFactsForSession({
|
|
session,
|
|
events: [
|
|
{ seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T01:20:00.000Z" },
|
|
{ seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T01:20:16.000Z" }
|
|
],
|
|
status: "completed",
|
|
lastProjectedSeq: 2
|
|
});
|
|
facts.turns[0].messageId = duplicateMessageId;
|
|
facts.messages.push({
|
|
messageId: duplicateMessageId,
|
|
sessionId: session.id,
|
|
turnId: traceId,
|
|
traceId,
|
|
role: "agent",
|
|
status: "completed",
|
|
projectedSeq: 9,
|
|
sourceSeq: 9,
|
|
sourceEventId: `${session.id}:message:duplicate-hash`,
|
|
terminal: true,
|
|
sealed: true,
|
|
text: finalText,
|
|
timing: {
|
|
startedAt: "2026-06-17T01:20:00.000Z",
|
|
lastEventAt: "2026-06-17T01:21:48.722Z",
|
|
finishedAt: "2026-06-17T01:21:48.722Z",
|
|
durationMs: 108722,
|
|
status: "completed",
|
|
valuesRedacted: true
|
|
},
|
|
startedAt: "2026-06-17T01:20:00.000Z",
|
|
lastEventAt: "2026-06-17T01:21:48.722Z",
|
|
finishedAt: "2026-06-17T01:21:48.722Z",
|
|
durationMs: 108722,
|
|
createdAt: "2026-06-17T01:20:16.000Z",
|
|
updatedAt: "2026-06-17T01:21:48.722Z",
|
|
valuesRedacted: true
|
|
});
|
|
facts.parts.push({
|
|
partId: "prt_workbench_duplicate_agent_message_hash_final",
|
|
messageId: duplicateMessageId,
|
|
sessionId: session.id,
|
|
turnId: traceId,
|
|
traceId,
|
|
partIndex: 0,
|
|
partType: "final_response",
|
|
status: "completed",
|
|
text: finalText,
|
|
projectedSeq: 9,
|
|
sourceSeq: 9,
|
|
sourceEventId: `${session.id}:part:duplicate-hash`,
|
|
terminal: true,
|
|
sealed: true,
|
|
updatedAt: "2026-06-17T01:21:48.722Z",
|
|
valuesRedacted: true
|
|
});
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore, workbenchRuntime: runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.total, 2);
|
|
assert.deepEqual(messages.body.messages.map((message) => message.role), ["user", "agent"]);
|
|
const assistant = messages.body.messages[1];
|
|
assert.equal(assistant.messageId, preferredMessageId);
|
|
assert.equal(assistant.text, finalText);
|
|
assert.equal(assistant.parts.length, 1);
|
|
assert.equal(assistant.parts[0].messageId, preferredMessageId);
|
|
assert.equal(assistant.parts[0].type, "final_response");
|
|
assert.equal(assistant.parts[0].text, finalText);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model projects terminal result atomically across session, messages, and turn", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_terminal_atomic";
|
|
const finalText = "terminal projection final response";
|
|
const session = {
|
|
id: "ses_workbench_terminal_atomic",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_terminal_atomic",
|
|
threadId: "thread-workbench-terminal-atomic",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T02:10:00.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "terminal atomic ping", traceId, status: "sent", createdAt: "2026-06-17T02:09:58.000Z" },
|
|
{ role: "agent", text: "", traceId, status: "running", createdAt: "2026-06-17T02:09:59.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
traceStore.append(traceId, { seq: 1, type: "request", status: "accepted", label: "request:accepted" });
|
|
traceStore.append(traceId, { seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true });
|
|
results.set(traceId, {
|
|
status: "completed",
|
|
traceId,
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: session.conversationId,
|
|
sessionId: session.id,
|
|
threadId: session.threadId,
|
|
finalResponse: { text: finalText, status: "completed", traceId },
|
|
agentRun: { runId: "run_workbench_terminal_atomic", commandId: "cmd_workbench_terminal_atomic", status: "completed", terminalStatus: "completed" }
|
|
});
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; },
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T02:09:58.000Z" },
|
|
{ seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T02:10:00.000Z" }
|
|
],
|
|
status: "completed",
|
|
finalText,
|
|
runId: "run_workbench_terminal_atomic",
|
|
commandId: "cmd_workbench_terminal_atomic",
|
|
lastProjectedSeq: 2
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.total, 2);
|
|
const assistant = messages.body.messages[1];
|
|
assert.equal(assistant.role, "agent");
|
|
assert.equal(assistant.status, "completed");
|
|
assert.equal(assistant.text, finalText);
|
|
assert.equal(assistant.parts[0].status, "completed");
|
|
assert.equal(assistant.parts[0].type, "final_response");
|
|
assert.equal(assistant.parts[0].messageId, assistant.messageId);
|
|
assert.equal(assistant.parts[0].traceId, traceId);
|
|
assert.equal(assistant.parts[0].text, finalText);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.assistantMessageId, assistant.messageId);
|
|
assert.equal(turn.body.turn.assistantText, finalText);
|
|
assert.equal(turn.body.turn.finalResponse.source, "message-part");
|
|
assert.equal(turn.body.turn.finalResponse.sealed, true);
|
|
assert.equal(turn.body.turn.finalResponse.text, finalText);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model seals turn from terminal message projection when checkpoint is stale", async () => {
|
|
const sessionId = "ses_workbench_terminal_message_stale_checkpoint";
|
|
const traceId = "trc_terminal_message_stale_checkpoint";
|
|
const finalText = "terminal message projection must close the visible turn";
|
|
const startedAt = "2026-06-29T22:09:32.000Z";
|
|
const finishedAt = "2026-06-29T22:09:47.000Z";
|
|
const facts = emptyFacts();
|
|
facts.sessions.push({
|
|
sessionId,
|
|
ownerUserId: ACTOR.id,
|
|
ownerRole: ACTOR.role,
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
lastTraceId: traceId,
|
|
projectedSeq: 33,
|
|
sourceSeq: 36,
|
|
createdAt: startedAt,
|
|
updatedAt: finishedAt,
|
|
valuesRedacted: true
|
|
});
|
|
facts.messages.push(
|
|
{ messageId: "msg_stale_user", sessionId, turnId: traceId, traceId, role: "user", status: "sent", text: "hi", projectedSeq: 31, sourceSeq: 31, createdAt: startedAt, updatedAt: startedAt, valuesRedacted: true },
|
|
{ messageId: "msg_stale_agent", sessionId, turnId: traceId, traceId, role: "agent", status: "completed", text: finalText, projectedSeq: 36, sourceSeq: 36, terminal: true, sealed: true, timing: { startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, valuesRedacted: true }, startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, createdAt: startedAt, updatedAt: finishedAt, valuesRedacted: true }
|
|
);
|
|
facts.parts.push(
|
|
{ partId: "prt_stale_user", messageId: "msg_stale_user", sessionId, turnId: traceId, traceId, partIndex: 0, partType: "text", status: "sent", text: "hi", projectedSeq: 31, sourceSeq: 31, updatedAt: startedAt, valuesRedacted: true },
|
|
{ partId: "prt_stale_agent", messageId: "msg_stale_agent", sessionId, turnId: traceId, traceId, partIndex: 0, partType: "final_response", status: "completed", text: finalText, projectedSeq: 36, sourceSeq: 36, terminal: true, sealed: true, updatedAt: finishedAt, valuesRedacted: true }
|
|
);
|
|
facts.turns.push({ turnId: traceId, sessionId, traceId, messageId: "msg_stale_agent", status: "running", projectedSeq: 33, sourceSeq: 33, terminal: false, sealed: false, startedAt, lastEventAt: finishedAt, updatedAt: finishedAt, valuesRedacted: true });
|
|
facts.checkpoints.push({ traceId, sessionId, turnId: traceId, runId: "run_stale_checkpoint", commandId: "cmd_stale_checkpoint", status: "running", projectionStatus: "projecting", projectionHealth: "degraded", projectedSeq: 33, sourceSeq: 33, terminal: false, sealed: false, startedAt, lastEventAt: finishedAt, updatedAt: finishedAt, valuesRedacted: true });
|
|
const runtimeStore = createRuntimeStoreFromFacts(facts);
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(sessionId)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
const assistant = messages.body.messages.find((message) => message.role === "agent");
|
|
assert.equal(assistant.status, "completed");
|
|
assert.equal(assistant.text, finalText);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
assert.equal(turn.body.turn.running, false);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.finalResponse.text, finalText);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model exposes failed terminal body when final response part is missing", async () => {
|
|
const sessionId = "ses_workbench_failed_terminal_missing_part";
|
|
const traceId = "trc_failed_terminal_missing_part";
|
|
const startedAt = "2026-06-29T22:19:32.000Z";
|
|
const finishedAt = "2026-06-29T22:19:47.000Z";
|
|
const facts = emptyFacts();
|
|
facts.sessions.push({
|
|
sessionId,
|
|
ownerUserId: ACTOR.id,
|
|
ownerRole: ACTOR.role,
|
|
agentId: "hwlab-code-agent",
|
|
status: "failed",
|
|
lastTraceId: traceId,
|
|
projectedSeq: 40,
|
|
sourceSeq: 40,
|
|
terminal: true,
|
|
sealed: true,
|
|
createdAt: startedAt,
|
|
updatedAt: finishedAt,
|
|
valuesRedacted: true
|
|
});
|
|
facts.messages.push(
|
|
{ messageId: "msg_failed_missing_part_user", sessionId, turnId: traceId, traceId, role: "user", status: "sent", text: "hi", projectedSeq: 39, sourceSeq: 39, createdAt: startedAt, updatedAt: startedAt, valuesRedacted: true },
|
|
{ messageId: "msg_failed_missing_part_agent", sessionId, turnId: traceId, traceId, role: "agent", status: "failed", text: "", projectedSeq: 40, sourceSeq: 40, terminal: true, sealed: true, timing: { startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, valuesRedacted: true }, startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, createdAt: startedAt, updatedAt: finishedAt, valuesRedacted: true }
|
|
);
|
|
facts.turns.push({ turnId: traceId, sessionId, traceId, messageId: "msg_failed_missing_part_agent", status: "failed", projectedSeq: 40, sourceSeq: 40, terminal: true, sealed: true, finalResponse: null, diagnostic: { blocker: { code: "provider-stream-disconnected", message: "provider stream disconnected" }, projectionStatus: "caught_up", projectionHealth: "healthy", valuesRedacted: true }, startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, updatedAt: finishedAt, valuesRedacted: true });
|
|
facts.checkpoints.push({ traceId, sessionId, turnId: traceId, runId: "run_failed_missing_part", commandId: "cmd_failed_missing_part", status: "failed", projectionStatus: "caught_up", projectionHealth: "healthy", projectedSeq: 40, sourceSeq: 40, terminal: true, sealed: true, diagnostic: { blocker: { code: "provider-stream-disconnected", message: "provider stream disconnected" }, valuesRedacted: true }, startedAt, lastEventAt: finishedAt, finishedAt, durationMs: 15000, updatedAt: finishedAt, valuesRedacted: true });
|
|
const runtimeStore = createRuntimeStoreFromFacts(facts);
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(sessionId)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "failed");
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
const assistant = messages.body.messages.find((message) => message.role === "agent");
|
|
assert.equal(assistant.status, "failed");
|
|
assert.match(assistant.text, /provider-stream-disconnected/u);
|
|
assert.equal(assistant.parts[0].type, "final_response");
|
|
assert.match(assistant.parts[0].text, /provider-stream-disconnected/u);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "failed");
|
|
assert.equal(turn.body.turn.running, false);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.match(turn.body.turn.finalResponse.text, /provider-stream-disconnected/u);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model does not expose trace-only memory without visible session or result owner", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_trace_only";
|
|
traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" });
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSessionByTraceId() { return null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 404);
|
|
assert.equal(turn.body.error.code, "workbench_turn_not_found");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events`);
|
|
assert.equal(trace.status, 404);
|
|
assert.equal(trace.body.error.code, "workbench_trace_not_found");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace events reports metadata gap when turn projection is visible", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_trace_metadata_gap";
|
|
const session = {
|
|
id: "ses_workbench_trace_metadata_gap",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_trace_metadata_gap",
|
|
threadId: "thread-workbench-trace-metadata-gap",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-20T13:00:00.000Z",
|
|
session: { sessionStatus: "completed", lastTraceId: traceId, messages: [{ role: "user", text: "metadata gap", traceId }, { role: "agent", text: "OK", traceId }] }
|
|
};
|
|
const accessController = {
|
|
store: {},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{ session, events: [], status: "completed", finalText: "OK", runId: "run_workbench_trace_metadata_gap", commandId: "cmd_workbench_trace_metadata_gap", lastProjectedSeq: 2, omitSessions: true }]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 404);
|
|
assert.equal(trace.body.error.code, "workbench_trace_metadata_missing");
|
|
assert.equal(trace.body.error.message, "Workbench trace metadata is missing from the trace events read model.");
|
|
assert.equal(trace.body.error.projectionStatus, "blocked");
|
|
assert.equal(trace.body.error.projectionHealth, "degraded");
|
|
assert.equal(trace.body.error.lastProjectedSeq, 2);
|
|
assert.equal(trace.body.error.sourceRunId, "run_workbench_trace_metadata_gap");
|
|
assert.equal(trace.body.error.sourceCommandId, "cmd_workbench_trace_metadata_gap");
|
|
assert.equal(trace.body.error.blocker.code, "workbench_trace_metadata_missing");
|
|
assert.notEqual(trace.body.error.message, "Workbench trace is not visible to the current actor.");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace events reports catch-up page when session projection is visible", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_trace_events_gap";
|
|
const session = {
|
|
id: "ses_workbench_trace_events_gap",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_trace_events_gap",
|
|
threadId: "thread-workbench-trace-events-gap",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-20T13:05:00.000Z",
|
|
session: { sessionStatus: "completed", lastTraceId: traceId, messages: [{ role: "user", text: "events gap", traceId }, { role: "agent", text: "OK", traceId }] }
|
|
};
|
|
const accessController = {
|
|
store: {},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{ session, events: [], status: "completed", finalText: "OK", runId: "run_workbench_trace_events_gap", commandId: "cmd_workbench_trace_events_gap", lastProjectedSeq: 2 }]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.status, "projecting");
|
|
assert.equal(trace.body.diagnostic.code, "workbench_trace_events_missing");
|
|
assert.equal(trace.body.sessionId, session.id);
|
|
assert.equal(trace.body.projectionStatus, "projecting");
|
|
assert.equal(trace.body.projectionHealth, "projecting");
|
|
assert.equal(trace.body.lastProjectedSeq, 2);
|
|
assert.equal(trace.body.sourceRunId, "run_workbench_trace_events_gap");
|
|
assert.equal(trace.body.sourceCommandId, "cmd_workbench_trace_events_gap");
|
|
assert.equal(trace.body.blocker, null);
|
|
assert.deepEqual(trace.body.events, []);
|
|
assert.equal(trace.body.fullTraceLoaded, false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model projects current turn running state from trace projection", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_active_state";
|
|
const session = {
|
|
id: "ses_workbench_active_state",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "active",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_active_state",
|
|
threadId: "thread-workbench-active-state",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T00:01:00.000Z",
|
|
session: { sessionStatus: "active", lastTraceId: traceId, messages: [{ role: "user", text: "still running", traceId }] }
|
|
};
|
|
traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false });
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [{ seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: session.updatedAt }],
|
|
status: "running",
|
|
lastProjectedSeq: 1
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, "/v1/workbench/sessions");
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "running");
|
|
assert.equal(sessions.body.sessions[0].running, true);
|
|
assert.equal(sessions.body.sessions[0].terminal, false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model projects current turn running state for idle session summary", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_idle_with_running_trace";
|
|
const session = {
|
|
id: "ses_workbench_idle_with_running_trace",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "idle",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_idle_with_running_trace",
|
|
threadId: "thread-workbench-idle-with-running-trace",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T00:02:00.000Z",
|
|
session: { sessionStatus: "idle", lastTraceId: traceId, messages: [{ role: "user", text: "still running despite idle row", traceId, status: "sent" }] }
|
|
};
|
|
traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false });
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [{ seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: session.updatedAt }],
|
|
status: "running",
|
|
lastProjectedSeq: 1
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "running");
|
|
assert.equal(sessions.body.sessions[0].running, true);
|
|
assert.equal(sessions.body.sessions[0].terminal, false);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.status, "running");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model projects newest running turn over stale completed session lastTraceId", async () => {
|
|
const oldTraceId = "trc_workbench_stale_completed_old";
|
|
const newTraceId = "trc_workbench_new_running_current";
|
|
const session = {
|
|
id: "ses_workbench_stale_completed_new_running",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "completed",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_stale_completed_new_running",
|
|
threadId: "thread-workbench-stale-completed-new-running",
|
|
lastTraceId: oldTraceId,
|
|
updatedAt: "2026-07-01T00:00:02.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
lastTraceId: oldTraceId,
|
|
messages: [
|
|
{ messageId: "msg_workbench_old_user", role: "user", text: "first", traceId: oldTraceId, status: "sent", projectedSeq: 10 },
|
|
{ messageId: "msg_workbench_old_agent", role: "agent", text: "first done", traceId: oldTraceId, status: "completed", projectedSeq: 20 }
|
|
]
|
|
}
|
|
};
|
|
const facts = emptyFacts();
|
|
mergeFacts(facts, buildDurableFactsForSession({ session, status: "completed", finalText: "first done", lastProjectedSeq: 20 }));
|
|
facts.messages.push(
|
|
{ messageId: "msg_workbench_new_user", sessionId: session.id, turnId: newTraceId, traceId: newTraceId, role: "user", status: "sent", text: "second", projectedSeq: 30, sourceSeq: 30, sourceEventId: "evt_new_user", terminal: false, sealed: false, updatedAt: "2026-07-01T00:00:03.000Z", valuesRedacted: true },
|
|
{ messageId: "msg_workbench_new_agent", sessionId: session.id, turnId: newTraceId, traceId: newTraceId, role: "agent", status: "running", text: "", projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_agent", terminal: false, sealed: false, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true }
|
|
);
|
|
facts.turns.push({ turnId: newTraceId, sessionId: session.id, traceId: newTraceId, messageId: "msg_workbench_new_agent", status: "running", projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_turn", terminal: false, sealed: false, diagnostic: { projectionStatus: "projecting", projectionHealth: "healthy", valuesRedacted: true }, timing: { startedAt: "2026-07-01T00:00:03.000Z", lastEventAt: "2026-07-01T00:00:04.000Z", finishedAt: null, durationMs: null, valuesRedacted: true }, startedAt: "2026-07-01T00:00:03.000Z", lastEventAt: "2026-07-01T00:00:04.000Z", finishedAt: null, durationMs: null, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true });
|
|
facts.checkpoints.push({ traceId: newTraceId, sessionId: session.id, turnId: newTraceId, projectedSeq: 40, sourceSeq: 40, sourceEventId: "evt_new_checkpoint", projectionStatus: "projecting", projectionHealth: "healthy", terminal: false, sealed: false, diagnostic: { projectionStatus: "projecting", projectionHealth: "healthy", valuesRedacted: true }, updatedAt: "2026-07-01T00:00:04.000Z", valuesRedacted: true });
|
|
const runtimeStore = createRuntimeStoreFromFacts(facts);
|
|
const accessController = {
|
|
store: { async listAgentSessionsForUser() { return [session]; } },
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore: createCodeAgentTraceStore(), runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].lastTraceId, newTraceId);
|
|
assert.equal(sessions.body.sessions[0].status, "running");
|
|
assert.equal(sessions.body.sessions[0].running, true);
|
|
assert.equal(sessions.body.sessions[0].terminal, false);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.traceId, newTraceId);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.status, "running");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model projects completed current turn for idle session summary", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_idle_with_completed_trace";
|
|
const finalText = "OK";
|
|
const session = {
|
|
id: "ses_workbench_idle_with_completed_trace",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "idle",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_idle_with_completed_trace",
|
|
threadId: "thread-workbench-idle-with-completed-trace",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T00:03:00.000Z",
|
|
session: {
|
|
sessionStatus: "idle",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "reply OK", traceId, status: "sent" },
|
|
{ role: "agent", text: "partial assistant text", traceId, status: "running" }
|
|
]
|
|
}
|
|
};
|
|
traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" });
|
|
traceStore.append(traceId, { type: "result", status: "completed", label: "result:completed", terminal: true });
|
|
results.set(traceId, {
|
|
status: "completed",
|
|
traceId,
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: session.conversationId,
|
|
sessionId: session.id,
|
|
threadId: session.threadId,
|
|
finalResponse: finalText,
|
|
agentRun: { runId: "run_workbench_idle_completed", commandId: "cmd_workbench_idle_completed", status: "completed" }
|
|
});
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; },
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const backendPerformanceStore = createBackendPerformanceStore({ env: { POD_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03", HWLAB_RUNTIME_LANE: "v03", HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601" } });
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-17T00:02:58.000Z" },
|
|
{ seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-17T00:03:00.000Z" }
|
|
],
|
|
status: "completed",
|
|
finalText,
|
|
runId: "run_workbench_idle_completed",
|
|
commandId: "cmd_workbench_idle_completed",
|
|
lastProjectedSeq: 2
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results, backendPerformanceStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.status, "completed");
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.messages[1].status, "completed");
|
|
assert.equal(messages.body.messages[1].text, finalText);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
assert.equal(turn.body.turn.running, false);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.assistantText, finalText);
|
|
assert.equal(turn.body.projectionStatus, "caught-up");
|
|
assert.equal(turn.body.sourceRunId, "run_workbench_idle_completed");
|
|
assert.equal(turn.body.sourceCommandId, "cmd_workbench_idle_completed");
|
|
const metricsText = backendPerformanceStore.metricsText();
|
|
const lagCountLine = metricsText.split("\n").find((line) => line.startsWith("hwlab_workbench_projection_lag_seconds_count") && line.includes('projection_status="caught_up"'));
|
|
const turnGetCountLine = metricsText.split("\n").find((line) => line.startsWith("hwlab_workbench_turn_get_duration_seconds_count") && line.includes('route="/v1/workbench/turns/:traceId"'));
|
|
assert.ok(lagCountLine?.endsWith(" 1"));
|
|
assert.ok(turnGetCountLine?.endsWith(" 1"));
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.projectionStatus, "caught-up");
|
|
assert.equal(trace.body.sourceRunId, "run_workbench_idle_completed");
|
|
assert.equal(trace.body.sourceCommandId, "cmd_workbench_idle_completed");
|
|
assert.equal(trace.body.lastProjectedSeq, 2);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model lets terminal result override stale running session state", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_stale_running_result_completed";
|
|
const finalText = "OK";
|
|
const session = {
|
|
id: "ses_workbench_stale_running_result_completed",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_stale_running_result_completed",
|
|
threadId: "thread-workbench-stale-running-result-completed",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-18T16:31:04.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "reply OK", traceId, status: "sent" },
|
|
{ role: "agent", text: "", traceId, status: "running" }
|
|
]
|
|
}
|
|
};
|
|
traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false });
|
|
results.set(traceId, {
|
|
status: "completed",
|
|
traceId,
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: session.conversationId,
|
|
sessionId: session.id,
|
|
threadId: session.threadId,
|
|
finalResponse: finalText,
|
|
agentRun: { runId: "run_workbench_stale_running_completed", commandId: "cmd_workbench_stale_running_completed", status: "completed" }
|
|
});
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; },
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: "2026-06-18T16:30:00.000Z" },
|
|
{ seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-18T16:31:04.000Z" }
|
|
],
|
|
status: "completed",
|
|
finalText,
|
|
runId: "run_workbench_stale_running_completed",
|
|
commandId: "cmd_workbench_stale_running_completed",
|
|
lastProjectedSeq: 2
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.status, "completed");
|
|
|
|
const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`);
|
|
assert.equal(detail.status, 200);
|
|
assert.equal(detail.body.session.status, "completed");
|
|
assert.equal(detail.body.session.running, false);
|
|
assert.equal(detail.body.session.terminal, true);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.messages[1].status, "completed");
|
|
assert.equal(messages.body.messages[1].text, finalText);
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.status, "completed");
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
assert.equal(turn.body.turn.running, false);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.assistantText, finalText);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session list uses durable projection checkpoint without trace/result hydration", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_compact_list_summary";
|
|
const finalText = "compact list summary OK";
|
|
const session = {
|
|
id: "ses_workbench_compact_list_summary",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_compact_list_summary",
|
|
threadId: "thread-workbench-compact-list-summary",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-19T15:40:00.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "compact summary", traceId, status: "sent" },
|
|
{ role: "agent", text: "", traceId, status: "running" }
|
|
],
|
|
traceResults: {
|
|
[traceId]: {
|
|
traceId,
|
|
status: "completed",
|
|
finalResponse: { text: finalText, status: "completed", traceId, valuesPrinted: false },
|
|
traceSummary: {
|
|
traceId,
|
|
source: "agent-session-compact-summary",
|
|
sourceEventCount: 42,
|
|
terminalStatus: "completed",
|
|
agentRun: { runId: "run_workbench_compact_list_summary", commandId: "cmd_workbench_compact_list_summary", lastSeq: 42, valuesPrinted: false },
|
|
valuesPrinted: false
|
|
},
|
|
agentRun: { runId: "run_workbench_compact_list_summary", commandId: "cmd_workbench_compact_list_summary", status: "completed", terminalStatus: "completed", lastSeq: 42, valuesPrinted: false },
|
|
updatedAt: "2026-06-19T15:40:00.000Z",
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
},
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const factQueries = [];
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ seq: 1, sourceSeq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-19T15:39:10.000Z" },
|
|
{ seq: 42, sourceSeq: 42, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-19T15:40:00.000Z" }
|
|
],
|
|
status: "completed",
|
|
finalText,
|
|
runId: "run_workbench_compact_list_summary",
|
|
commandId: "cmd_workbench_compact_list_summary",
|
|
lastProjectedSeq: 42
|
|
}],
|
|
queries: factQueries
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
assert.equal(sessions.body.sessions[0].turnSummary.status, "completed");
|
|
assert.equal(sessions.body.sessions[0].turnSummary.eventCount, 42);
|
|
assert.equal(sessions.body.sessions[0].projectionStatus, "caught-up");
|
|
assert.ok(factQueries.length >= 1);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session list overlaps include lookup with the page query", async () => {
|
|
const makeSession = (id, traceId, updatedAt) => ({
|
|
id,
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: id.replace(/^ses_/u, "cnv_"),
|
|
threadId: `thread-${id}`,
|
|
lastTraceId: traceId,
|
|
updatedAt,
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [{ role: "user", text: id, traceId, status: "sent" }]
|
|
}
|
|
});
|
|
const included = makeSession("ses_parallel_include", "trc_parallel_include", "2026-06-19T14:00:00.000Z");
|
|
const firstPage = makeSession("ses_parallel_page_a", "trc_parallel_page_a", "2026-06-19T16:00:00.000Z");
|
|
const secondPage = makeSession("ses_parallel_page_b", "trc_parallel_page_b", "2026-06-19T15:00:00.000Z");
|
|
const facts = emptyFacts();
|
|
for (const session of [included, firstPage, secondPage]) mergeFacts(facts, buildDurableFactsForSession({ session, status: "running" }));
|
|
const accessController = {
|
|
store: {},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
let pageReleased = false;
|
|
let releasePage = () => {};
|
|
const pageGate = new Promise((resolve) => { releasePage = resolve; });
|
|
const queries = [];
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
queries.push({ ...params, pageReleased });
|
|
if (params.sessionsOrder === "updated_desc") await pageGate;
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const pending = getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(included.id)}&limit=1`);
|
|
await waitForCondition(() => queries.some((query) => query.sessionId === included.id) && queries.some((query) => query.sessionsOrder === "updated_desc"));
|
|
const includeIndex = queries.findIndex((query) => query.sessionId === included.id);
|
|
assert.ok(includeIndex >= 0);
|
|
assert.equal(queries[includeIndex].pageReleased, false);
|
|
pageReleased = true;
|
|
releasePage();
|
|
const sessions = await pending;
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].sessionId, included.id);
|
|
assert.equal(sessions.body.hasMore, true);
|
|
assert.equal(queries.find((query) => query.sessionsOrder === "updated_desc")?.sessionProjection, "summary");
|
|
assert.equal(queries.find((query) => query.sessionId === included.id)?.sessionProjection, undefined);
|
|
} finally {
|
|
pageReleased = true;
|
|
releasePage();
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model reads terminal session status from durable facts", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_durable_terminal_after_memory_running";
|
|
const session = {
|
|
id: "ses_workbench_durable_terminal_after_memory_running",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_durable_terminal_after_memory_running",
|
|
threadId: "thread-workbench-durable-terminal-after-memory-running",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-18T17:22:00.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "reply OK", traceId, status: "sent" },
|
|
{ role: "agent", text: "", traceId, status: "running" }
|
|
]
|
|
}
|
|
};
|
|
traceStore.append(traceId, { type: "backend", status: "running", label: "runner:created", terminal: false, seq: 1 });
|
|
const durableEvents = [
|
|
{ traceId, seq: 1, type: "request", status: "accepted", label: "request:accepted", createdAt: "2026-06-18T17:21:40.000Z", valuesPrinted: false },
|
|
{ traceId, seq: 2, type: "result", status: "completed", label: "result:completed", terminal: true, createdAt: "2026-06-18T17:22:00.000Z", valuesPrinted: false }
|
|
];
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; },
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{ session, events: durableEvents, status: "completed", finalText: "OK", lastProjectedSeq: 2 }]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const sessions = await getJson(port, `/v1/workbench/sessions?includeSessionId=${encodeURIComponent(session.id)}`);
|
|
assert.equal(sessions.status, 200);
|
|
assert.equal(sessions.body.sessions[0].status, "completed");
|
|
assert.equal(sessions.body.sessions[0].running, false);
|
|
assert.equal(sessions.body.sessions[0].terminal, true);
|
|
|
|
const detail = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}`);
|
|
assert.equal(detail.status, 200);
|
|
assert.equal(detail.body.session.status, "completed");
|
|
assert.equal(detail.body.session.running, false);
|
|
assert.equal(detail.body.session.terminal, true);
|
|
|
|
const messages = await getJson(port, `/v1/workbench/sessions/${encodeURIComponent(session.id)}/messages?limit=10`);
|
|
assert.equal(messages.status, 200);
|
|
assert.equal(messages.body.messages[1].status, "completed");
|
|
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.status, "completed");
|
|
assert.equal(turn.body.turn.status, "completed");
|
|
assert.equal(turn.body.turn.running, false);
|
|
assert.equal(turn.body.turn.terminal, true);
|
|
assert.equal(turn.body.turn.trace.eventCount, 2);
|
|
assert.equal(turn.body.projectionStatus, "caught-up");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model keeps non-terminal durable tool completed events out of turn status", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_durable_tool_completed_nonterminal";
|
|
const session = {
|
|
id: "ses_workbench_durable_tool_completed_nonterminal",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_durable_tool_completed_nonterminal",
|
|
threadId: "thread-workbench-durable-tool-completed-nonterminal",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-18T19:35:51.787Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "write benchmark", traceId, status: "sent" },
|
|
{ role: "agent", text: "", traceId, status: "running" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
traceStore.append(traceId, { seq: 1, type: "backend_status", status: "running", label: "runner:created", terminal: false });
|
|
const durableEvents = [
|
|
{ traceId, seq: 1, type: "assistant_message", status: "running", label: "agentrun:assistant:message", terminal: false, message: "正在安装 Python。", createdAt: "2026-06-18T19:35:40.000Z", valuesPrinted: false },
|
|
{ traceId, seq: 2, type: "commandExecution", status: "completed", label: "item/commandExecution:completed", terminal: false, command: "ls -ld .", stdout: "OK\n", createdAt: "2026-06-18T19:35:51.787Z", valuesPrinted: false }
|
|
];
|
|
results.set(traceId, {
|
|
status: "running",
|
|
traceId,
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: session.conversationId,
|
|
sessionId: session.id,
|
|
threadId: session.threadId,
|
|
finalResponse: null,
|
|
assistantText: null,
|
|
agentRun: { runId: "run_workbench_nonterminal_tool_completed", commandId: "cmd_workbench_nonterminal_tool_completed", status: "runner-job-created", runStatus: "pending", commandState: "pending" }
|
|
});
|
|
const accessController = {
|
|
store: {
|
|
async listAgentSessionsForUser() { return [session]; },
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: durableEvents,
|
|
status: "running",
|
|
assistantText: "partial assistant text",
|
|
runId: "run_workbench_nonterminal_tool_completed",
|
|
commandId: "cmd_workbench_nonterminal_tool_completed",
|
|
projectionStatus: "projecting",
|
|
lastProjectedSeq: 2
|
|
}]
|
|
});
|
|
const workbenchRuntime = {
|
|
async queryWorkbenchFacts(query) { return runtimeStore.queryWorkbenchFacts(query); }
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, workbenchRuntime, codeAgentChatResults: results });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 200);
|
|
assert.equal(turn.body.status, "running");
|
|
assert.equal(turn.body.turn.status, "running");
|
|
assert.equal(turn.body.turn.running, true);
|
|
assert.equal(turn.body.turn.terminal, false);
|
|
assert.equal(turn.body.turn.assistantText, null);
|
|
assert.equal(turn.body.turn.finalResponse, null);
|
|
assert.equal(turn.body.projectionStatus, "projecting");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.events.at(-1).status, "completed");
|
|
assert.equal(trace.body.events.at(-1).terminal, false);
|
|
assert.equal(trace.body.projectionStatus, "projecting");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace events API blocks historical projectedSeq collisions instead of handing them to renderer", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const traceId = "trc_workbench_projected_seq_collision";
|
|
const session = {
|
|
id: "ses_workbench_projected_seq_collision",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_projected_seq_collision",
|
|
threadId: "thread-workbench-projected-seq-collision",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-20T12:20:00.000Z",
|
|
session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: "collision", traceId, status: "sent" }] }
|
|
};
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = createDurableFactsRuntimeStore({
|
|
sessions: [{
|
|
session,
|
|
events: [
|
|
{ id: "wte_collision_a", projectedSeq: 1, sourceSeq: 1, sourceEventId: "source-a", type: "backend", status: "running", label: "projected:1:a", createdAt: "2026-06-20T12:19:58.000Z" },
|
|
{ id: "wte_collision_b", projectedSeq: 1, sourceSeq: 2, sourceEventId: "source-b", type: "assistant", status: "running", label: "projected:1:b", message: "later", createdAt: "2026-06-20T12:19:59.000Z" }
|
|
],
|
|
status: "running",
|
|
projectionStatus: "projecting",
|
|
lastProjectedSeq: 1
|
|
}]
|
|
});
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: createCodeAgentChatResultStore() });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 200);
|
|
assert.equal(trace.body.status, "blocked");
|
|
assert.equal(trace.body.projectionStatus, "blocked");
|
|
assert.equal(trace.body.projectionHealth, "degraded");
|
|
assert.equal(trace.body.blocker.code, "projected_seq_collision");
|
|
assert.deepEqual(trace.body.events, []);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench realtime stream surfaces facts blocker instead of legacy trace fallback", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_realtime";
|
|
const session = {
|
|
id: "ses_workbench_realtime",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_realtime",
|
|
threadId: "thread-workbench-realtime",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-17T02:00:00.000Z",
|
|
session: { sessionStatus: "running", lastTraceId: traceId, messages: [{ role: "user", text: "stream", traceId }] }
|
|
};
|
|
traceStore.append(traceId, { type: "assistant", status: "completed", label: "assistant:completed", terminal: true, message: "legacy trace answer" });
|
|
results.set(traceId, { status: "completed", traceId, ownerUserId: ACTOR.id, sessionId: session.id, threadId: session.threadId, finalResponse: { text: "legacy result answer" } });
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore, codeAgentChatResults: results, env: { HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000" } });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const eventsPromise = getSseEvents(port, `/v1/workbench/events?sessionId=${encodeURIComponent(session.id)}&traceId=${encodeURIComponent(traceId)}`, 4);
|
|
const events = await eventsPromise;
|
|
assert.deepEqual(events.map((event) => event.event), [
|
|
"workbench.connected",
|
|
"workbench.error",
|
|
"workbench.trace.snapshot",
|
|
"workbench.turn.snapshot"
|
|
]);
|
|
assert.equal(events[0].data.filters.sessionId, session.id);
|
|
assert.equal(events[1].data.error.code, "workbench_facts_trace_missing");
|
|
assert.equal(events[2].data.traceId, traceId);
|
|
assert.equal(events[2].data.snapshot.status, "unknown");
|
|
assert.equal(events[2].data.snapshot.eventCount, 0);
|
|
assert.equal(events[3].data.turn.traceId, traceId);
|
|
assert.equal(events[3].data.turn.status, "unknown");
|
|
assert.equal(events[3].data.turn.terminal, false);
|
|
assert.equal(events[3].data.turn.finalResponse, null);
|
|
assert.equal(JSON.stringify(events).includes("legacy result answer"), false);
|
|
assert.equal(JSON.stringify(events).includes("legacy trace answer"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench realtime stream forwards HWLAB Kafka events after initial connection", async () => {
|
|
const fakeKafka = createFakeKafkaFactory();
|
|
const traceId = "trc_workbench_realtime_after_seq";
|
|
const session = {
|
|
id: "ses_workbench_realtime_after_seq",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_realtime_after_seq",
|
|
threadId: "thread-workbench-realtime-after-seq",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-24T14:00:00.000Z",
|
|
session: { sessionStatus: "running", lastTraceId: traceId }
|
|
};
|
|
const outboxQueries = [];
|
|
const workbenchRuntime = {
|
|
async readWorkbenchProjectionOutbox(params = {}) {
|
|
outboxQueries.push({ ...params });
|
|
return [
|
|
{ outboxSeq: 11, projectedSeq: 7, traceId, sessionId: session.id, turnId: traceId, commitType: "event", terminal: false, sealed: false, createdAt: "2026-06-24T14:00:01.000Z" }
|
|
];
|
|
}
|
|
};
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const serverWithKafka = createCloudApiServer({
|
|
accessController,
|
|
workbenchRuntime,
|
|
kafkaFactory: fakeKafka.factory,
|
|
env: {
|
|
HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000",
|
|
HWLAB_KAFKA_BOOTSTRAP_SERVERS: "127.0.0.1:9092",
|
|
HWLAB_KAFKA_CLIENT_ID: "test-hwlab-cloud-api",
|
|
HWLAB_KAFKA_EVENT_TOPIC: "hwlab.event.v1"
|
|
}
|
|
});
|
|
await new Promise((resolve) => serverWithKafka.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = serverWithKafka.address();
|
|
setTimeout(() => { void fakeKafka.emit({
|
|
eventType: "hwlab.trace.event.projected",
|
|
sessionId: "ses_agentrun_workbench_realtime_after_seq",
|
|
traceId,
|
|
context: { sourceSeq: 7, runId: "run_workbench_realtime_after_seq", commandId: "cmd_workbench_realtime_after_seq" },
|
|
event: { type: "backend", eventType: "backend", status: "running", label: "agentrun:event:test", message: "Kafka realtime event", sourceSeq: 7 }
|
|
}); }, 25);
|
|
const events = await getSseEvents(port, `/v1/workbench/events?sessionId=${encodeURIComponent(session.id)}&traceId=${encodeURIComponent(traceId)}&afterSeq=10`, 2);
|
|
assert.deepEqual(events.map((event) => event.event), ["workbench.connected", "workbench.trace.event"]);
|
|
assert.equal(events[0].id, "10");
|
|
assert.equal(events[1].id, "hwlab.event.v1:0:0");
|
|
assert.equal(events[1].data.realtimeSource, "kafka");
|
|
assert.equal(events[1].data.realtimeAuthority, "workbench-realtime-authority-v2");
|
|
assert.equal(events[1].data.entity.family, "traceEvents");
|
|
assert.equal(events[1].data.entity.version, 7);
|
|
assert.equal(events[1].data.entity.projectionRevision, "kafka:hwlab.event.v1:0:0");
|
|
assert.equal(events[1].data.event.message, "Kafka realtime event");
|
|
assert.equal(events[1].data.kafka.topic, "hwlab.event.v1");
|
|
assert.equal(events[1].data.cursor.traceSeq, 7);
|
|
assert.deepEqual(outboxQueries, []);
|
|
} finally {
|
|
await new Promise((resolve, reject) => serverWithKafka.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench session realtime Kafka stream does not pin subscription to stale lastTraceId", async () => {
|
|
const fakeKafka = createFakeKafkaFactory();
|
|
const staleTraceId = "trc_workbench_realtime_stale_trace";
|
|
const liveTraceId = "trc_workbench_realtime_live_trace";
|
|
const session = {
|
|
id: "ses_workbench_realtime_session_wide",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_realtime_session_wide",
|
|
threadId: "thread-workbench-realtime-session-wide",
|
|
lastTraceId: staleTraceId,
|
|
updatedAt: "2026-06-24T14:00:00.000Z",
|
|
session: { sessionStatus: "running", lastTraceId: staleTraceId }
|
|
};
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId() { return null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const serverWithKafka = createCloudApiServer({
|
|
accessController,
|
|
workbenchRuntime: {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
return {
|
|
facts: {
|
|
sessions: [{ sessionId: session.id, ownerUserId: ACTOR.id, threadId: session.threadId, lastTraceId: staleTraceId, status: "running", valuesRedacted: true }],
|
|
messages: [],
|
|
parts: [],
|
|
turns: [],
|
|
checkpoints: []
|
|
},
|
|
count: 1,
|
|
persistence: { adapter: "test-session-wide-kafka", durable: true },
|
|
params
|
|
};
|
|
}
|
|
},
|
|
kafkaFactory: fakeKafka.factory,
|
|
env: {
|
|
HWLAB_WORKBENCH_SSE_HEARTBEAT_MS: "60000",
|
|
HWLAB_KAFKA_BOOTSTRAP_SERVERS: "127.0.0.1:9092",
|
|
HWLAB_KAFKA_CLIENT_ID: "test-hwlab-cloud-api",
|
|
HWLAB_KAFKA_EVENT_TOPIC: "hwlab.event.v1"
|
|
}
|
|
});
|
|
await new Promise((resolve) => serverWithKafka.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = serverWithKafka.address();
|
|
setTimeout(() => { void fakeKafka.emit({
|
|
eventType: "hwlab.trace.event.projected",
|
|
sessionId: "ses_agentrun_workbench_realtime_session_wide",
|
|
traceId: liveTraceId,
|
|
context: { runId: "run_workbench_realtime_session_wide", commandId: "cmd_workbench_realtime_session_wide" },
|
|
event: { type: "backend", eventType: "backend", status: "running", label: "agentrun:event:live", message: "Kafka live trace event" }
|
|
}); }, 100);
|
|
const events = await getSseEvents(port, `/v1/workbench/events?sessionId=${encodeURIComponent(session.id)}`, 4);
|
|
assert.equal(events[0].event, "workbench.connected");
|
|
const liveEvent = events.find((event) => event.event === "workbench.trace.event" && event.data?.traceId === liveTraceId);
|
|
assert.ok(liveEvent, JSON.stringify(events.map((event) => ({ event: event.event, traceId: event.data?.traceId, message: event.data?.event?.message }))));
|
|
assert.equal(liveEvent.data.event.message, "Kafka live trace event");
|
|
assert.equal(liveEvent.data.realtimeAuthority, "workbench-realtime-authority-v2");
|
|
} finally {
|
|
await new Promise((resolve, reject) => serverWithKafka.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench read model exposes runtime trace projection query failures as projection blockers", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const results = createCodeAgentChatResultStore();
|
|
const traceId = "trc_workbench_projection_store_unavailable";
|
|
const session = {
|
|
id: "ses_workbench_projection_store_unavailable",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_projection_store_unavailable",
|
|
threadId: "thread-workbench-projection-store-unavailable",
|
|
lastTraceId: traceId,
|
|
updatedAt: "2026-06-18T01:10:00.000Z",
|
|
session: {
|
|
sessionStatus: "running",
|
|
lastTraceId: traceId,
|
|
messages: [
|
|
{ role: "user", text: "projection store unavailable", traceId, status: "sent", createdAt: "2026-06-18T01:09:58.000Z" },
|
|
{ role: "agent", text: "", traceId, status: "running", createdAt: "2026-06-18T01:09:59.000Z" }
|
|
],
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
}
|
|
};
|
|
traceStore.append(traceId, { seq: 1, type: "backend", status: "running", label: "runner:created", terminal: false, createdAt: "2026-06-18T01:10:00.000Z" });
|
|
results.set(traceId, { status: "running", traceId, ownerUserId: ACTOR.id, sessionId: session.id, threadId: session.threadId });
|
|
const accessController = {
|
|
store: {
|
|
async getAgentSession(sessionId) { return sessionId === session.id ? session : null; },
|
|
async getAgentSessionByTraceId(requestTraceId) { return requestTraceId === traceId ? session : null; }
|
|
},
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts() {
|
|
const error = new Error("runtime query failed");
|
|
error.code = "TEST_RUNTIME_QUERY_FAILED";
|
|
throw error;
|
|
}
|
|
};
|
|
const server = createCloudApiServer({ accessController, traceStore, runtimeStore, codeAgentChatResults: results });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
|
assert.equal(turn.status, 503);
|
|
assert.equal(turn.body.error.code, "projection_store_unavailable");
|
|
|
|
const trace = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?limit=10`);
|
|
assert.equal(trace.status, 503);
|
|
assert.equal(trace.body.error.code, "projection_store_unavailable");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace events remain visible after session lastTraceId moves", async () => {
|
|
const oldTraceId = "trc_workbench_history_old_trace";
|
|
const newTraceId = "trc_workbench_history_new_trace";
|
|
const session = {
|
|
id: "ses_workbench_history_trace",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "running",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_history_trace",
|
|
threadId: "thread-workbench-history-trace",
|
|
lastTraceId: newTraceId,
|
|
updatedAt: "2026-06-20T10:10:00.000Z",
|
|
session: {
|
|
messages: [
|
|
{ role: "user", text: "old turn", traceId: oldTraceId, status: "sent", createdAt: "2026-06-20T10:00:00.000Z" },
|
|
{ role: "agent", text: "old final", traceId: oldTraceId, status: "completed", createdAt: "2026-06-20T10:00:01.000Z" }
|
|
],
|
|
valuesRedacted: true
|
|
}
|
|
};
|
|
const oldFacts = buildDurableFactsForSession({
|
|
session: { ...session, status: "completed", lastTraceId: oldTraceId, updatedAt: "2026-06-20T10:01:00.000Z" },
|
|
status: "completed",
|
|
finalText: "old final",
|
|
runId: "run_workbench_history_old_trace",
|
|
commandId: "cmd_workbench_history_old_trace",
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:00:00.000Z" },
|
|
{ projectedSeq: 2, sourceSeq: 2, type: "result", status: "completed", label: "runner:completed", terminal: true, createdAt: "2026-06-20T10:01:00.000Z" }
|
|
],
|
|
lastProjectedSeq: 2
|
|
});
|
|
const newFacts = buildDurableFactsForSession({
|
|
session: { ...session, session: { messages: [{ role: "user", text: "new turn", traceId: newTraceId, status: "sent" }] } },
|
|
status: "running",
|
|
runId: "run_workbench_history_new_trace",
|
|
commandId: "cmd_workbench_history_new_trace",
|
|
events: [{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:10:00.000Z" }],
|
|
lastProjectedSeq: 1
|
|
});
|
|
const facts = emptyFacts();
|
|
mergeFacts(facts, oldFacts);
|
|
mergeFacts(facts, newFacts);
|
|
facts.sessions = newFacts.sessions;
|
|
const queries = [];
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
queries.push(params);
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(oldTraceId)}/events?limit=10`);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.body.status, "succeeded");
|
|
assert.equal(response.body.sessionId, session.id);
|
|
assert.equal(response.body.traceId, oldTraceId);
|
|
assert.equal(response.body.events.length, 2);
|
|
assert.equal(response.body.events.at(-1).status, "completed");
|
|
assert.equal(response.body.error, undefined);
|
|
assert.equal(queries.some((query) => query.sessionId === session.id && Array.isArray(query.families) && query.families.includes("sessions")), true);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
test("workbench trace event page keeps per-trace terminal status after later session cancel", async () => {
|
|
const completedTraceId = "trc_workbench_completed_before_cancel";
|
|
const canceledTraceId = "trc_workbench_later_cancel";
|
|
const session = {
|
|
id: "ses_workbench_completed_before_cancel",
|
|
projectId: "prj_hwpod_workbench",
|
|
agentId: "hwlab-code-agent",
|
|
status: "canceled",
|
|
ownerUserId: ACTOR.id,
|
|
conversationId: "cnv_workbench_completed_before_cancel",
|
|
threadId: "thread-workbench-completed-before-cancel",
|
|
lastTraceId: canceledTraceId,
|
|
updatedAt: "2026-06-20T10:20:30.000Z",
|
|
session: {
|
|
messages: [
|
|
{ role: "user", text: "completed first", traceId: completedTraceId, status: "sent", createdAt: "2026-06-20T10:20:00.000Z" },
|
|
{ role: "agent", text: "completed final", traceId: completedTraceId, status: "completed", createdAt: "2026-06-20T10:20:10.000Z" },
|
|
{ role: "user", text: "cancel later", traceId: canceledTraceId, status: "sent", createdAt: "2026-06-20T10:20:20.000Z" },
|
|
{ role: "agent", text: "hwlab-user-cancel", traceId: canceledTraceId, status: "canceled", createdAt: "2026-06-20T10:20:30.000Z" }
|
|
],
|
|
valuesRedacted: true
|
|
}
|
|
};
|
|
const facts = emptyFacts();
|
|
mergeFacts(facts, buildDurableFactsForSession({
|
|
session: { ...session, status: "completed", lastTraceId: completedTraceId, updatedAt: "2026-06-20T10:20:10.000Z" },
|
|
status: "completed",
|
|
finalText: "completed final",
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:20:00.000Z" },
|
|
{ projectedSeq: 2, sourceSeq: 2, type: "result", status: "completed", label: "runner:completed", terminal: true, createdAt: "2026-06-20T10:20:10.000Z" }
|
|
],
|
|
lastProjectedSeq: 2
|
|
}));
|
|
mergeFacts(facts, buildDurableFactsForSession({
|
|
session: { ...session, status: "canceled", lastTraceId: canceledTraceId, session: { messages: session.session.messages.slice(2), valuesRedacted: true } },
|
|
status: "canceled",
|
|
finalText: "hwlab-user-cancel",
|
|
events: [
|
|
{ projectedSeq: 1, sourceSeq: 1, type: "backend", status: "running", label: "runner:started", createdAt: "2026-06-20T10:20:20.000Z" },
|
|
{ projectedSeq: 2, sourceSeq: 2, type: "cancel", status: "canceled", label: "runner:canceled", terminal: true, createdAt: "2026-06-20T10:20:30.000Z" }
|
|
],
|
|
lastProjectedSeq: 2
|
|
}));
|
|
facts.sessions = facts.sessions.filter((item) => item.lastTraceId === canceledTraceId);
|
|
const runtimeStore = {
|
|
async queryWorkbenchFacts(params = {}) {
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
const accessController = {
|
|
async ensureBootstrap() {},
|
|
async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; }
|
|
};
|
|
const server = createCloudApiServer({ accessController, workbenchRuntime: runtimeStore });
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await getJson(port, `/v1/workbench/traces/${encodeURIComponent(completedTraceId)}/events?limit=10`);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.body.traceStatus, "completed");
|
|
assert.equal(response.body.events.at(-1).status, "completed");
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|
|
|
|
async function getJson(port, path) {
|
|
const response = await fetch(`http://127.0.0.1:${port}${path}`);
|
|
return {
|
|
status: response.status,
|
|
body: await response.json()
|
|
};
|
|
}
|
|
|
|
async function waitForCondition(predicate, timeoutMs = 500) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (predicate()) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
}
|
|
assert.fail("condition was not met before timeout");
|
|
}
|
|
|
|
function createDurableFactsRuntimeStore({ sessions = [], queryError = null, queries = [] } = {}) {
|
|
const facts = emptyFacts();
|
|
for (const item of sessions) mergeFacts(facts, buildDurableFactsForSession(item));
|
|
return createRuntimeStoreFromFacts(facts, { queryError, queries });
|
|
}
|
|
|
|
function createRuntimeStoreFromFacts(facts, { queryError = null, queries = [] } = {}) {
|
|
return {
|
|
queries,
|
|
async queryWorkbenchFacts(params = {}) {
|
|
queries.push(params);
|
|
if (queryError) throw queryError;
|
|
const filtered = filterFacts(facts, params);
|
|
return {
|
|
facts: filtered,
|
|
count: Object.values(filtered).reduce((sum, rows) => sum + rows.length, 0),
|
|
persistence: { adapter: "test-durable-workbench-facts", durable: true }
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
function buildDurableFactsForSession({ session, events = [], status = null, finalText = null, assistantText = null, runId = null, commandId = null, projectionStatus = null, projectionHealth = "healthy", lastProjectedSeq = null, omitSessions = false } = {}) {
|
|
const traceId = session.lastTraceId ?? session.session?.lastTraceId;
|
|
const projectedStatus = normalizeTestStatus(status ?? session.status);
|
|
const terminal = testTerminalStatuses.has(projectedStatus);
|
|
const normalizedEvents = normalizeTestEvents(events, session, { traceId });
|
|
const timing = testTimingProjection({ session, events: normalizedEvents, status: projectedStatus, terminal });
|
|
const normalizedMessages = normalizeTestMessages(session, { traceId, status: projectedStatus, finalText, terminal, timing });
|
|
const explicitLastProjectedSeq = lastProjectedSeq !== null && lastProjectedSeq !== undefined ? Number(lastProjectedSeq) : NaN;
|
|
const projectedSeq = Number.isFinite(explicitLastProjectedSeq) ? explicitLastProjectedSeq : Math.max(normalizedEvents.length, normalizedMessages.length, 0);
|
|
const checkpointStatus = projectionStatus ?? (terminal ? "caught_up" : "projecting");
|
|
return {
|
|
sessions: omitSessions ? [] : [{
|
|
sessionId: session.id,
|
|
ownerUserId: session.ownerUserId,
|
|
ownerRole: session.ownerRole ?? null,
|
|
projectId: session.projectId ?? null,
|
|
conversationId: session.conversationId ?? null,
|
|
threadId: session.threadId ?? null,
|
|
agentId: session.agentId ?? "hwlab-code-agent",
|
|
status: projectedStatus,
|
|
lastTraceId: traceId,
|
|
providerProfile: session.session?.providerProfile ?? null,
|
|
projectedSeq,
|
|
sourceSeq: projectedSeq,
|
|
sourceEventId: traceId,
|
|
terminal,
|
|
sealed: terminal,
|
|
timing,
|
|
startedAt: timing.startedAt,
|
|
lastEventAt: timing.lastEventAt,
|
|
finishedAt: timing.finishedAt,
|
|
durationMs: timing.durationMs,
|
|
createdAt: session.startedAt ?? session.createdAt ?? session.updatedAt ?? null,
|
|
updatedAt: session.updatedAt ?? session.endedAt ?? session.startedAt ?? null,
|
|
valuesRedacted: true
|
|
}],
|
|
messages: normalizedMessages,
|
|
parts: normalizedMessages.filter((message) => message.text).map((message, index) => {
|
|
const partType = message.role !== "user" && message.terminal ? "final_response" : "text";
|
|
return {
|
|
partId: `prt_${session.id}_${index}`,
|
|
messageId: message.messageId,
|
|
sessionId: session.id,
|
|
turnId: message.turnId,
|
|
traceId: message.traceId,
|
|
partIndex: 0,
|
|
partType,
|
|
status: message.status,
|
|
text: message.text,
|
|
projectedSeq: message.projectedSeq,
|
|
sourceSeq: message.sourceSeq,
|
|
sourceEventId: message.sourceEventId,
|
|
terminal: message.terminal,
|
|
sealed: partType === "final_response" ? true : message.sealed,
|
|
updatedAt: message.updatedAt,
|
|
valuesRedacted: true
|
|
};
|
|
}),
|
|
turns: traceId ? [{
|
|
turnId: traceId,
|
|
sessionId: session.id,
|
|
traceId,
|
|
messageId: normalizedMessages.find((message) => message.role !== "user")?.messageId ?? null,
|
|
status: projectedStatus,
|
|
projectedSeq,
|
|
sourceSeq: projectedSeq,
|
|
sourceEventId: traceId,
|
|
terminal,
|
|
sealed: terminal,
|
|
assistantText: assistantText ?? null,
|
|
finalResponse: finalText ? { text: finalText, status: projectedStatus, traceId, valuesPrinted: false } : null,
|
|
diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true },
|
|
timing,
|
|
startedAt: timing.startedAt,
|
|
lastEventAt: timing.lastEventAt,
|
|
finishedAt: timing.finishedAt,
|
|
durationMs: timing.durationMs,
|
|
createdAt: session.startedAt ?? session.updatedAt ?? null,
|
|
updatedAt: session.updatedAt ?? null,
|
|
valuesRedacted: true
|
|
}] : [],
|
|
traceEvents: normalizedEvents,
|
|
checkpoints: traceId ? [{
|
|
traceId,
|
|
sessionId: session.id,
|
|
turnId: traceId,
|
|
runId,
|
|
commandId,
|
|
projectedSeq,
|
|
sourceSeq: projectedSeq,
|
|
sourceEventId: traceId,
|
|
projectionStatus: checkpointStatus,
|
|
projectionHealth,
|
|
terminal,
|
|
sealed: terminal,
|
|
diagnostic: { projectionStatus: checkpointStatus, projectionHealth, valuesRedacted: true },
|
|
timing,
|
|
startedAt: timing.startedAt,
|
|
lastEventAt: timing.lastEventAt,
|
|
finishedAt: timing.finishedAt,
|
|
durationMs: timing.durationMs,
|
|
createdAt: session.startedAt ?? session.updatedAt ?? null,
|
|
updatedAt: session.updatedAt ?? null,
|
|
valuesRedacted: true
|
|
}] : []
|
|
};
|
|
}
|
|
|
|
function normalizeTestMessages(session, { traceId, status, finalText, terminal, timing }) {
|
|
const raw = Array.isArray(session.session?.messages) ? session.session.messages : [];
|
|
return raw.map((message, index) => {
|
|
const role = message.role ?? (index % 2 === 0 ? "user" : "agent");
|
|
const isAssistant = role === "agent" || role === "assistant";
|
|
const text = isAssistant && finalText ? finalText : String(message.text ?? message.content ?? "");
|
|
const messageStatus = isAssistant && finalText ? status : normalizeTestStatus(message.status ?? (role === "user" ? "sent" : status));
|
|
const messageTraceId = message.traceId ?? traceId;
|
|
const projectedSeq = Number.isFinite(Number(message.projectedSeq)) ? Number(message.projectedSeq) : index + 1;
|
|
const sourceSeq = Number.isFinite(Number(message.sourceSeq)) ? Number(message.sourceSeq) : projectedSeq;
|
|
return {
|
|
messageId: message.messageId ?? `msg_${session.id}_${index}`,
|
|
sessionId: session.id,
|
|
turnId: message.turnId ?? messageTraceId,
|
|
traceId: messageTraceId,
|
|
role,
|
|
status: messageStatus,
|
|
projectedSeq,
|
|
sourceSeq,
|
|
sourceEventId: `${session.id}:message:${index}`,
|
|
terminal: isAssistant && terminal,
|
|
sealed: isAssistant && terminal,
|
|
text,
|
|
...(isAssistant ? { timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs } : {}),
|
|
createdAt: message.createdAt ?? session.startedAt ?? session.updatedAt ?? null,
|
|
updatedAt: message.updatedAt ?? session.updatedAt ?? null,
|
|
valuesRedacted: true
|
|
};
|
|
});
|
|
}
|
|
|
|
function testTimingProjection({ session, events = [], status, terminal }) {
|
|
const startedAt = timestampIso(session.startedAt ?? session.createdAt ?? events[0]?.createdAt ?? events[0]?.occurredAt);
|
|
const lastEvent = events.at(-1) ?? null;
|
|
const lastEventAt = timestampIso(lastEvent?.updatedAt ?? lastEvent?.createdAt ?? lastEvent?.occurredAt ?? session.updatedAt);
|
|
const finishedAt = terminal ? timestampIso(session.endedAt ?? lastEventAt ?? session.updatedAt) : null;
|
|
const durationMs = terminal ? elapsedMs(startedAt, finishedAt ?? lastEventAt) : null;
|
|
return { startedAt, lastEventAt, finishedAt, durationMs, status, valuesRedacted: true };
|
|
}
|
|
|
|
function timestampIso(value) {
|
|
const ms = Date.parse(String(value ?? ""));
|
|
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
}
|
|
|
|
function elapsedMs(startedAt, endedAt) {
|
|
const start = Date.parse(String(startedAt ?? ""));
|
|
const end = Date.parse(String(endedAt ?? ""));
|
|
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
|
|
return end - start;
|
|
}
|
|
|
|
function normalizeTestEvents(events, session, { traceId }) {
|
|
return events.map((event, index) => {
|
|
const projectedSeq = Number.isFinite(Number(event.projectedSeq)) ? Number(event.projectedSeq) : Number.isFinite(Number(event.seq)) ? Number(event.seq) : index + 1;
|
|
const sourceSeq = Number.isFinite(Number(event.sourceSeq)) ? Number(event.sourceSeq) : null;
|
|
return {
|
|
...event,
|
|
id: event.id ?? `wte_${session.id}_${index}`,
|
|
traceId: event.traceId ?? traceId,
|
|
sessionId: event.sessionId ?? session.id,
|
|
turnId: event.turnId ?? traceId,
|
|
messageId: event.messageId ?? null,
|
|
sourceSeq,
|
|
sourceEventId: event.sourceEventId ?? `${traceId}:${sourceSeq ?? projectedSeq}:${index}`,
|
|
projectedSeq,
|
|
eventType: event.eventType ?? event.type ?? event.label ?? "event",
|
|
terminal: event.terminal === true,
|
|
sealed: event.terminal === true,
|
|
occurredAt: event.occurredAt ?? event.createdAt ?? session.updatedAt ?? null,
|
|
updatedAt: event.updatedAt ?? event.createdAt ?? session.updatedAt ?? null,
|
|
valuesRedacted: true
|
|
};
|
|
});
|
|
}
|
|
|
|
function filterFacts(facts, params = {}) {
|
|
const families = testFactFamilySet(params);
|
|
const afterProjectedSeq = Number.parseInt(params.afterProjectedSeq ?? "", 10);
|
|
const filtered = {
|
|
sessions: families.has("sessions") ? facts.sessions.filter((record) => matchesFact(record, params, ["sessionId", "ownerUserId", "projectId", "conversationId", "threadId", "status"]) && matchesTraceFact(record.lastTraceId, params)) : [],
|
|
messages: families.has("messages") ? facts.messages.filter((record) => matchesFact(record, params, ["messageId", "sessionId", "turnId", "traceId", "role", "status"])) : [],
|
|
parts: families.has("parts") ? facts.parts.filter((record) => matchesFact(record, params, ["partId", "messageId", "sessionId", "turnId", "traceId", "partType", "status"])) : [],
|
|
turns: families.has("turns") ? facts.turns.filter((record) => matchesFact(record, params, ["turnId", "sessionId", "traceId", "messageId", "status"])) : [],
|
|
traceEvents: families.has("traceEvents") ? facts.traceEvents
|
|
.filter((record) => matchesFact(record, params, ["id", "traceId", "sessionId", "turnId", "messageId", "eventType"]))
|
|
.filter((record) => !Number.isInteger(afterProjectedSeq) || afterProjectedSeq <= 0 || Number(record.projectedSeq) > afterProjectedSeq)
|
|
.sort((left, right) => Number(left.projectedSeq) - Number(right.projectedSeq)) : [],
|
|
checkpoints: families.has("checkpoints") ? facts.checkpoints.filter((record) => matchesFact(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"])) : []
|
|
};
|
|
if (workbenchTestFactOrder(params, "sessions") === "updated_desc") {
|
|
filtered.sessions.sort((left, right) => String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")) || String(right.sessionId ?? "").localeCompare(String(left.sessionId ?? "")));
|
|
}
|
|
const limit = Number.parseInt(params.limit ?? "", 10);
|
|
if (!Number.isInteger(limit) || limit <= 0) return filtered;
|
|
return Object.fromEntries(Object.entries(filtered).map(([key, value]) => [key, value.slice(0, limit)]));
|
|
}
|
|
|
|
function workbenchTestFactOrder(params = {}, family = "") {
|
|
const value = params[`${family}Order`] ?? params.order;
|
|
return value === "updated_desc" ? "updated_desc" : "updated_asc";
|
|
}
|
|
|
|
function matchesFact(record, params, fields) {
|
|
return fields.every((field) => {
|
|
if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) return false;
|
|
const values = Array.isArray(params[`${field}s`]) ? params[`${field}s`].map((item) => String(item ?? "")).filter(Boolean) : [];
|
|
return values.length === 0 || values.includes(String(record[field] ?? ""));
|
|
});
|
|
}
|
|
|
|
function matchesTraceFact(value, params) {
|
|
if (params.traceId && value !== params.traceId) return false;
|
|
const traceIds = Array.isArray(params.traceIds) ? params.traceIds.map((item) => String(item ?? "")).filter(Boolean) : [];
|
|
return traceIds.length === 0 || traceIds.includes(String(value ?? ""));
|
|
}
|
|
|
|
function testFactFamilySet(params = {}) {
|
|
const all = ["sessions", "messages", "parts", "turns", "traceEvents", "checkpoints"];
|
|
const raw = params.families ?? params.factFamilies;
|
|
if (!raw) return new Set(all);
|
|
const values = Array.isArray(raw) ? raw : String(raw).split(",");
|
|
const selected = values.map((item) => String(item ?? "").trim()).filter((item) => all.includes(item));
|
|
return new Set(selected.length ? selected : all);
|
|
}
|
|
|
|
function mergeFacts(target, source) {
|
|
for (const key of Object.keys(target)) target[key].push(...source[key]);
|
|
return target;
|
|
}
|
|
|
|
function emptyFacts() {
|
|
return { sessions: [], messages: [], parts: [], turns: [], traceEvents: [], checkpoints: [] };
|
|
}
|
|
|
|
function normalizeTestStatus(value) {
|
|
const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
|
if (status === "active" || status === "busy" || status === "processing") return "running";
|
|
if (status === "cancelled") return "canceled";
|
|
return status || "unknown";
|
|
}
|
|
|
|
const testTerminalStatuses = new Set(["completed", "failed", "blocked", "timeout", "canceled", "idle"]);
|
|
|
|
async function getSseEvents(port, path, count) {
|
|
const controller = new AbortController();
|
|
const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal });
|
|
assert.equal(response.status, 200);
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
const events = [];
|
|
try {
|
|
while (events.length < count) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
for (;;) {
|
|
const index = buffer.indexOf("\n\n");
|
|
if (index < 0) break;
|
|
const block = buffer.slice(0, index);
|
|
buffer = buffer.slice(index + 2);
|
|
events.push(parseSseBlock(block));
|
|
if (events.length >= count) break;
|
|
}
|
|
}
|
|
} finally {
|
|
controller.abort();
|
|
}
|
|
return events;
|
|
}
|
|
|
|
function parseSseBlock(block) {
|
|
const lines = block.split("\n");
|
|
const event = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() ?? "message";
|
|
const id = lines.find((line) => line.startsWith("id:"))?.slice(3).trim() ?? null;
|
|
const data = lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
return { event, id, data: JSON.parse(data) };
|
|
}
|
|
|
|
function createFakeKafkaFactory() {
|
|
let eachMessage = null;
|
|
let offset = 0;
|
|
let subscribedTopic = "hwlab.event.v1";
|
|
const consumer = {
|
|
connect: async () => undefined,
|
|
subscribe: async (input = {}) => { subscribedTopic = String(input.topic ?? subscribedTopic); },
|
|
run: async (input = {}) => { eachMessage = input.eachMessage; },
|
|
stop: async () => undefined,
|
|
disconnect: async () => undefined
|
|
};
|
|
return {
|
|
factory: () => ({ consumer: () => consumer }),
|
|
emit: async (value = {}) => {
|
|
await waitFor(() => typeof eachMessage === "function");
|
|
await eachMessage({
|
|
topic: subscribedTopic,
|
|
partition: 0,
|
|
message: {
|
|
offset: String(offset++),
|
|
key: Buffer.from(value.traceId ?? value.sessionId ?? "fake"),
|
|
timestamp: String(Date.now()),
|
|
value: Buffer.from(JSON.stringify(value))
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
async function waitFor(predicate, timeoutMs = 1000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (predicate()) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
throw new Error("waitFor timed out");
|
|
}
|