f416950556
Move fresh and retry runner dispatch into AgentRun command admission, remove process-local job creation and secret persistence, and split oversized modules by responsibility. Co-Authored-By: Codex <noreply@openai.com>
801 lines
38 KiB
TypeScript
801 lines
38 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 durable state and timeline 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";
|
|
|
|
import {
|
|
ACTOR,
|
|
getJson,
|
|
waitForCondition,
|
|
createDurableFactsRuntimeStore,
|
|
createRuntimeStoreFromFacts,
|
|
buildDurableFactsForSession,
|
|
normalizeTestMessages,
|
|
testTimingProjection,
|
|
timestampIso,
|
|
elapsedMs,
|
|
normalizeTestEvents,
|
|
filterFacts,
|
|
workbenchTestFactOrder,
|
|
matchesFact,
|
|
matchesTraceFact,
|
|
testFactFamilySet,
|
|
mergeFacts,
|
|
emptyFacts,
|
|
normalizeTestStatus,
|
|
getSseEvents,
|
|
parseSseBlock,
|
|
createFakeKafkaFactory,
|
|
waitFor
|
|
} from "./server-workbench-http-test-helpers.ts";
|
|
|
|
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()));
|
|
}
|
|
});
|