test: cover workbench projector resume
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010403 API契约 draft-2026-06-18-r1.
|
||||
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-19-p0-projector-resume; PJ2026-010403 API契约 draft-2026-06-18-r1.
|
||||
// Responsibility: Same-origin fake Workbench API and static server for Playwright browser regression tests.
|
||||
|
||||
import { createReadStream, existsSync, statSync } from "node:fs";
|
||||
@@ -78,6 +78,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
||||
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? `127.0.0.1:${port}`}`);
|
||||
const path = url.pathname;
|
||||
recordLedger(method, url);
|
||||
refreshBackgroundProjection();
|
||||
|
||||
if (path === "/__e2e/health") return json(response, 200, { ok: true, scenarioId: state.scenarioId });
|
||||
if (path === "/__e2e/state") return json(response, 200, stateSummary());
|
||||
@@ -301,6 +302,11 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
sessions.unshift(session);
|
||||
traces.trc_scroll_follow = scrollFollowTrace(90);
|
||||
}
|
||||
if (id === "projector-resume-from-checkpoint") {
|
||||
const session = projectorResumeSession();
|
||||
sessions.unshift(session);
|
||||
traces.trc_f2456023233a485b = projectorResumeEarlyTrace();
|
||||
}
|
||||
if (id === "cross-session-late-events") {
|
||||
sessions.unshift(crossSessionLateB(), crossSessionLateA());
|
||||
traces.trc_late_A = crossSessionLateTraceA();
|
||||
@@ -320,6 +326,8 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
? "ses_terminal_empty"
|
||||
: id === "progress-only-final-response" || id === "terminal-completed-no-final-response" || id === "tool-completed-projection-running"
|
||||
? "ses_running"
|
||||
: id === "projector-resume-from-checkpoint"
|
||||
? "ses_317f78e1-ed91-4b09-a72a-3668d5469f59"
|
||||
: id === "projection-degraded-diagnostics"
|
||||
? "ses_projection_degraded"
|
||||
: id === "projection-sse-error"
|
||||
@@ -346,11 +354,97 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
terminalScript: id === "event-replay" || id === "running-to-terminal" || id === "stale-submit-restore" || id === "progress-only-final-response" || id === "terminal-completed-no-final-response",
|
||||
terminalFailureScript: id === "stale-submit-restore",
|
||||
staleTraceId,
|
||||
liveBackfillTraceId: null,
|
||||
liveBackfillReadyAtMs: null
|
||||
liveBackfillTraceId: id === "projector-resume-from-checkpoint" ? "trc_f2456023233a485b" : null,
|
||||
liveBackfillReadyAtMs: id === "projector-resume-from-checkpoint" ? Date.now() + 5_000 : null
|
||||
};
|
||||
}
|
||||
|
||||
function refreshBackgroundProjection(): void {
|
||||
if (state.scenarioId !== "projector-resume-from-checkpoint") return;
|
||||
if (!state.liveBackfillReadyAtMs || Date.now() < state.liveBackfillReadyAtMs) return;
|
||||
const session = sessionById("ses_317f78e1-ed91-4b09-a72a-3668d5469f59");
|
||||
if (session?.status === "completed") return;
|
||||
commitProjectorResumeProjection();
|
||||
}
|
||||
|
||||
function projectorResumeSession(): SessionRecord {
|
||||
const now = new Date().toISOString();
|
||||
const trace = projectorResumeEarlyTrace();
|
||||
return {
|
||||
sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59",
|
||||
threadId: "thr_projector_resume_1596",
|
||||
status: "running",
|
||||
lastTraceId: "trc_f2456023233a485b",
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
messageCount: 2,
|
||||
firstUserMessagePreview: "issue 1596 durable projector resume sample",
|
||||
turnSummary: { traceId: "trc_f2456023233a485b", status: "running", running: true, terminal: false, ...projectorResumeProjection("projecting", 4) },
|
||||
messages: [
|
||||
{ id: "msg_projector_resume_user", messageId: "msg_projector_resume_user", role: "user", title: "用户", text: "issue 1596 durable projector resume sample", status: "sent", createdAt: now, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", traceId: "trc_f2456023233a485b", turnId: "turn_projector_resume_1596" },
|
||||
{ id: "msg_projector_resume_agent", messageId: "msg_projector_resume_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", traceId: "trc_f2456023233a485b", turnId: "turn_projector_resume_1596", runnerTrace: trace, ...projectorResumeProjection("projecting", 4) }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function projectorResumeEarlyTrace(): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
const projection = projectorResumeProjection("projecting", 4);
|
||||
const events = [
|
||||
{ seq: 1, sourceSeq: 1, createdAt, label: "agentrun:request:accepted", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704" } },
|
||||
{ seq: 2, sourceSeq: 2, createdAt, label: "agentrun:run:created", type: "backend_status", status: "running", payload: { runId: "run_58ef1811071548539f881515912560d8", runStatus: "claimed" } },
|
||||
{ seq: 3, sourceSeq: 3, createdAt, label: "agentrun:command:created", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", commandState: "running" } },
|
||||
{ seq: 4, sourceSeq: 4, createdAt, label: "agentrun:runner-job:created", type: "backend_status", status: "running", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", namespace: "agentrun-v01" } }
|
||||
];
|
||||
return { traceId: "trc_f2456023233a485b", status: "running", running: true, terminal: false, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", turnId: "turn_projector_resume_1596", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, ...projection };
|
||||
}
|
||||
|
||||
function projectorResumeCompletedTrace(): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
const finalText = "fake projector resume completed from durable checkpoint.";
|
||||
const projection = projectorResumeProjection("caught-up", 7);
|
||||
const events = [
|
||||
...(projectorResumeEarlyTrace().events as JsonRecord[]),
|
||||
{ seq: 5, sourceSeq: 2748, createdAt, label: "agentrun:assistant:message", type: "assistant_message", status: "completed", message: finalText, payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704" } },
|
||||
{ seq: 6, sourceSeq: 2749, createdAt, label: "item/commandExecution:completed", type: "commandExecution", status: "completed", stdout: "OK\n", payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", exitCode: 0 } },
|
||||
{ seq: 7, sourceSeq: 2750, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true, payload: { commandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704", terminalStatus: "completed" } }
|
||||
];
|
||||
return { traceId: "trc_f2456023233a485b", status: "completed", running: false, terminal: true, sessionId: "ses_317f78e1-ed91-4b09-a72a-3668d5469f59", threadId: "thr_projector_resume_1596", turnId: "turn_projector_resume_1596", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false, finalResponse: { text: finalText, status: "completed" }, traceSummary: { source: "durable-projector-resume", terminalStatus: "completed", valuesPrinted: false }, ...projection };
|
||||
}
|
||||
|
||||
function projectorResumeProjection(status: "projecting" | "caught-up", lastProjectedSeq: number): JsonRecord {
|
||||
const caughtUp = status === "caught-up";
|
||||
const projection = {
|
||||
projectionStatus: status,
|
||||
projectionHealth: status,
|
||||
lastProjectedSeq,
|
||||
sourceLatestSeq: 2750,
|
||||
sourceRunId: "run_58ef1811071548539f881515912560d8",
|
||||
sourceCommandId: "cmd_d4d20aa2bb2c4d19a91143ce87d73704",
|
||||
staleMs: caughtUp ? 0 : 90_000,
|
||||
blocker: caughtUp ? null : { code: "projector_resume_pending", layer: "workbench-projector", retryable: true, message: "后台投影恢复中,已从 checkpoint 记录 lastProjectedSeq=4。", valuesPrinted: false },
|
||||
valuesRedacted: true
|
||||
};
|
||||
return { projection, ...projection };
|
||||
}
|
||||
|
||||
function commitProjectorResumeProjection(): void {
|
||||
const completed = projectorResumeCompletedTrace();
|
||||
state.traces.trc_f2456023233a485b = completed;
|
||||
const session = sessionById("ses_317f78e1-ed91-4b09-a72a-3668d5469f59");
|
||||
if (!session) return;
|
||||
session.status = "completed";
|
||||
session.updatedAt = new Date().toISOString();
|
||||
session.turnSummary = { traceId: "trc_f2456023233a485b", status: "completed", running: false, terminal: true, ...projectorResumeProjection("caught-up", 7) };
|
||||
session.messages = (session.messages ?? []).map((message) => {
|
||||
if (message.role !== "agent" || message.traceId !== "trc_f2456023233a485b") return message;
|
||||
return { ...message, text: "fake projector resume completed from durable checkpoint.", status: "completed", runnerTrace: completed, ...projectorResumeProjection("caught-up", 7) };
|
||||
});
|
||||
const terminalEvent = (completed.events as JsonRecord[]).at(-1) ?? null;
|
||||
if (terminalEvent) broadcastSse("workbench.trace.event", { type: "trace.event", sessionId: session.sessionId, threadId: session.threadId ?? null, traceId: "trc_f2456023233a485b", event: terminalEvent, snapshot: completed });
|
||||
broadcastSse("workbench.turn.snapshot", { type: "turn.snapshot", sessionId: session.sessionId, threadId: session.threadId ?? null, traceId: "trc_f2456023233a485b", turn: turnPayload("trc_f2456023233a485b") });
|
||||
}
|
||||
|
||||
function markSessionText(sessions: SessionRecord[], sessionId: string, userText: string, agentText: string): void {
|
||||
const session = sessions.find((item) => item.sessionId === sessionId);
|
||||
if (!session) return;
|
||||
@@ -1037,22 +1131,29 @@ function workbenchTurnPayload(traceId: string): JsonRecord {
|
||||
function tracePayload(traceId: string, url: URL): JsonRecord {
|
||||
const turn = turnPayload(traceId);
|
||||
const events = Array.isArray(turn.events) ? turn.events as JsonRecord[] : [];
|
||||
const sinceSeq = Number(url.searchParams.get("sinceSeq") ?? 0);
|
||||
const afterSeq = traceAfterSeq(url);
|
||||
const requestedLimit = Number(url.searchParams.get("limit") ?? events.length);
|
||||
const limit = requestedLimit || events.length || 100;
|
||||
const filtered = Number.isFinite(sinceSeq) && sinceSeq > 0 ? events.filter((event) => Number(event.seq ?? 0) > sinceSeq) : events;
|
||||
const filtered = afterSeq > 0 ? events.filter((event) => Number(event.seq ?? 0) > afterSeq) : events;
|
||||
const page = filtered.slice(0, Math.max(1, Number.isFinite(limit) ? limit : 100));
|
||||
const lastSeq = Number(page.at(-1)?.seq ?? 0);
|
||||
const firstSeq = page.length > 0 ? Number(page[0]?.seq ?? 0) : null;
|
||||
const lastSeq = page.length > 0 ? Number(page.at(-1)?.seq ?? 0) : null;
|
||||
const hasMore = filtered.length > page.length;
|
||||
return { ...turn, events: page, eventCount: events.length, hasMore, fullTraceLoaded: !hasMore, nextSinceSeq: hasMore ? lastSeq : null, range: { sinceSeq, returned: page.length, total: events.length } };
|
||||
return { ...turn, events: page, eventCount: events.length, hasMore, fullTraceLoaded: !hasMore, nextSinceSeq: lastSeq, nextSeq: page.length > 0 ? lastSeq : afterSeq, range: { afterSeq, fromSeq: firstSeq, toSeq: lastSeq, limit: Math.max(1, Number.isFinite(limit) ? limit : 100), returned: page.length, total: events.length } };
|
||||
}
|
||||
|
||||
function traceAfterSeq(url: URL): number {
|
||||
const cursor = url.searchParams.get("cursor") ?? "";
|
||||
if (cursor.startsWith("seq:")) return Math.max(0, Number.parseInt(cursor.slice(4), 10) || 0);
|
||||
return Math.max(0, Number.parseInt(url.searchParams.get("sinceSeq") ?? "0", 10) || 0);
|
||||
}
|
||||
|
||||
function workbenchTracePayload(traceId: string, url: URL): JsonRecord {
|
||||
const payload = tracePayload(traceId, url);
|
||||
if (state.scenarioId === "tool-completed-projection-running" && traceId === "trc_running") {
|
||||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: "completed", events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSinceSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, projectionStatus: "projecting", terminalEvidence: null, finalResponse: null, traceSummary: payload.traceSummary, retention: payload.retention };
|
||||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: "completed", events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, projectionStatus: "projecting", terminalEvidence: null, finalResponse: null, traceSummary: payload.traceSummary, retention: payload.retention };
|
||||
}
|
||||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: payload.status, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSinceSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, terminalEvidence: payload.terminalEvidence, finalResponse: payload.finalResponse, traceSummary: payload.traceSummary, retention: payload.retention, ...projectionEnvelope(payload) };
|
||||
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: payload.status, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, terminalEvidence: payload.terminalEvidence, finalResponse: payload.finalResponse, traceSummary: payload.traceSummary, retention: payload.retention, ...projectionEnvelope(payload) };
|
||||
}
|
||||
|
||||
function projectionEnvelope(payload: JsonRecord): JsonRecord {
|
||||
|
||||
Reference in New Issue
Block a user