fix: keep terminal workbench timing sealed
This commit is contained in:
@@ -574,6 +574,99 @@ test("workbench read model exposes session, messages, turn, and trace without wr
|
||||
}
|
||||
});
|
||||
|
||||
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";
|
||||
|
||||
@@ -1276,9 +1276,12 @@ function factTraceTimingProjection(events = [], checkpoint = null, status = null
|
||||
const observedAt = new Date().toISOString();
|
||||
const eventStartTimes = factArray(events).flatMap((event) => [event?.createdAt, event?.occurredAt]);
|
||||
const eventActivityTimes = factArray(events).flatMap((event) => [event?.createdAt, event?.occurredAt, event?.updatedAt]);
|
||||
const terminalEventTimes = factArray(events)
|
||||
.filter(factTraceEventIsTerminalAuthority)
|
||||
.flatMap(factTraceEventTerminalTimes);
|
||||
const startedAt = firstTimestampIso(checkpointTiming.startedAt, ...eventStartTimes);
|
||||
const lastEventAt = latestTimestampIso(checkpointTiming.lastEventAt, ...eventActivityTimes);
|
||||
const finishedAt = terminal ? latestTimestampIso(checkpointTiming.finishedAt, lastEventAt) : null;
|
||||
const finishedAt = terminal ? latestTimestampIso(checkpointTiming.finishedAt, ...terminalEventTimes) : null;
|
||||
const lastEventAt = terminal && finishedAt ? finishedAt : latestTimestampIso(checkpointTiming.lastEventAt, ...eventActivityTimes);
|
||||
const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt);
|
||||
const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt);
|
||||
return {
|
||||
@@ -1293,6 +1296,16 @@ function factTraceTimingProjection(events = [], checkpoint = null, status = null
|
||||
};
|
||||
}
|
||||
|
||||
function factTraceEventIsTerminalAuthority(event = null) {
|
||||
const eventType = textValue(event?.eventType ?? event?.type);
|
||||
return event?.terminal === true || event?.sealed === true || eventType === "terminal";
|
||||
}
|
||||
|
||||
function factTraceEventTerminalTimes(event = null) {
|
||||
const source = objectValue(event?.timing);
|
||||
return [source?.finishedAt, event?.finishedAt, source?.lastEventAt, event?.lastEventAt, event?.occurredAt, event?.createdAt];
|
||||
}
|
||||
|
||||
function factTraceEventDto(event, index) {
|
||||
const seq = factProjectedSeq(event);
|
||||
if (!seq) return null;
|
||||
@@ -1368,8 +1381,8 @@ function factCombinedTimingProjection(status = null, ...records) {
|
||||
const observedAt = new Date().toISOString();
|
||||
const timings = records.map((record) => factTimingSource(record)).filter(Boolean);
|
||||
const startedAt = firstTimestampIso(...timings.map((timing) => timing.startedAt));
|
||||
const lastEventAt = latestTimestampIso(...timings.map((timing) => timing.lastEventAt));
|
||||
const finishedAt = terminal ? latestTimestampIso(...timings.map((timing) => timing.finishedAt)) : null;
|
||||
const lastEventAt = terminal && finishedAt ? finishedAt : latestTimestampIso(...timings.map((timing) => timing.lastEventAt));
|
||||
const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt);
|
||||
const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt);
|
||||
return { startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: true };
|
||||
|
||||
Reference in New Issue
Block a user