488 lines
20 KiB
TypeScript
488 lines
20 KiB
TypeScript
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor.
|
|
// Responsibility: Workbench projection writer/finalizer durable facts regression tests.
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudRuntimeStore } from "../db/runtime-store.ts";
|
|
import { writeWorkbenchProjectionEvent, writeWorkbenchProjectionSession } from "./workbench-projection-writer.ts";
|
|
|
|
test("workbench projection writer commits terminal owner evidence as sealed durable facts", async () => {
|
|
const factWrites = [];
|
|
const accessWrites = [];
|
|
const runtimeStore = {
|
|
async writeWorkbenchFacts(params, requestMeta) {
|
|
factWrites.push({ params, requestMeta });
|
|
return { written: true, facts: params.facts };
|
|
}
|
|
};
|
|
const accessController = {
|
|
async recordAgentSessionOwner(input) {
|
|
accessWrites.push(input);
|
|
return {
|
|
id: input.sessionId,
|
|
projectId: input.projectId,
|
|
ownerUserId: input.ownerUserId,
|
|
conversationId: input.conversationId,
|
|
threadId: input.threadId,
|
|
lastTraceId: input.traceId,
|
|
status: input.status,
|
|
session: input.session,
|
|
updatedAt: "2026-06-20T11:00:00.000Z"
|
|
};
|
|
}
|
|
};
|
|
|
|
await writeWorkbenchProjectionSession({
|
|
accessController,
|
|
runtimeStore,
|
|
traceId: "trc_writer_terminal",
|
|
ownerUserId: "usr_writer",
|
|
ownerRole: "user",
|
|
sessionId: "ses_writer_terminal",
|
|
projectId: "prj_writer",
|
|
conversationId: "cnv_writer_terminal",
|
|
threadId: "thread-writer-terminal",
|
|
status: "completed",
|
|
payload: {
|
|
traceId: "trc_writer_terminal",
|
|
status: "completed",
|
|
assistantText: "final answer",
|
|
runnerTrace: {
|
|
traceId: "trc_writer_terminal",
|
|
status: "completed",
|
|
startedAt: "2026-06-20T10:59:30.000Z",
|
|
lastEventAt: "2026-06-20T11:00:00.000Z",
|
|
finishedAt: "2026-06-20T11:00:00.000Z",
|
|
elapsedMs: 30000
|
|
},
|
|
agentRun: { runId: "run_writer_terminal", commandId: "cmd_writer_terminal", status: "completed", terminalStatus: "completed", lastSeq: 42 },
|
|
updatedAt: "2026-06-20T11:00:00.000Z"
|
|
},
|
|
session: {
|
|
sessionStatus: "completed",
|
|
messages: [
|
|
{ messageId: "msg_writer_user", role: "user", text: "question", status: "sent", turnId: "trc_writer_terminal", traceId: "trc_writer_terminal" },
|
|
{ messageId: "msg_writer_agent", role: "agent", text: "final answer", status: "completed", turnId: "trc_writer_terminal", traceId: "trc_writer_terminal" }
|
|
],
|
|
finalResponse: { text: "final answer", status: "completed", traceId: "trc_writer_terminal" }
|
|
}
|
|
});
|
|
|
|
assert.equal(accessWrites.length, 1);
|
|
assert.equal(factWrites.length, 1);
|
|
const facts = factWrites[0].params.facts;
|
|
assert.equal(facts.sessions[0].status, "completed");
|
|
assert.equal(facts.sessions[0].sealed, true);
|
|
assert.equal(facts.messages.length, 2);
|
|
assert.equal(facts.messages.find((message) => message.messageId === "msg_writer_agent").sealed, true);
|
|
assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_agent" && part.text === "final answer" && part.sealed === true), true);
|
|
assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_agent" && part.partType === "final_response" && part.text === "final answer" && part.sealed === true), true);
|
|
assert.equal(facts.turns[0].terminal, true);
|
|
assert.equal(facts.turns[0].sealed, true);
|
|
assert.equal(facts.turns[0].finalResponse.text, "final answer");
|
|
assert.equal(facts.messages.find((message) => message.messageId === "msg_writer_agent").lastEventAt, "2026-06-20T11:00:00.000Z");
|
|
assert.equal(facts.messages.find((message) => message.messageId === "msg_writer_agent").durationMs, 30000);
|
|
assert.equal(facts.turns[0].startedAt, "2026-06-20T10:59:30.000Z");
|
|
assert.equal(facts.turns[0].durationMs, 30000);
|
|
assert.equal(facts.checkpoints[0].projectionStatus, "caught_up");
|
|
assert.equal(facts.checkpoints[0].sealed, true);
|
|
assert.equal(facts.checkpoints[0].timing.finishedAt, "2026-06-20T11:00:00.000Z");
|
|
});
|
|
|
|
test("workbench projection writer backfills terminal final and completion trace events from sealed facts", async () => {
|
|
const runtimeStore = createCloudRuntimeStore({ now: () => "2026-06-20T11:20:00.000Z" });
|
|
const accessController = {
|
|
async recordAgentSessionOwner(input) {
|
|
return {
|
|
id: input.sessionId,
|
|
projectId: input.projectId,
|
|
ownerUserId: input.ownerUserId,
|
|
conversationId: input.conversationId,
|
|
threadId: input.threadId,
|
|
lastTraceId: input.traceId,
|
|
status: input.status,
|
|
session: input.session,
|
|
updatedAt: "2026-06-20T11:20:00.000Z"
|
|
};
|
|
}
|
|
};
|
|
|
|
for (const [sourceSeq, label] of [
|
|
[1, "agentrun:backend:admitted"],
|
|
[2, "agentrun:run:created"],
|
|
[3, "agentrun:runner-job:queued"]
|
|
]) {
|
|
await writeWorkbenchProjectionEvent({
|
|
runtimeStore,
|
|
event: {
|
|
traceId: "trc_writer_terminal_backfill",
|
|
sessionId: "ses_writer_terminal_backfill",
|
|
source: "agentrun",
|
|
sourceSeq,
|
|
type: "backend",
|
|
eventType: "backend",
|
|
status: "running",
|
|
label,
|
|
runId: "run_writer_terminal_backfill",
|
|
commandId: "cmd_writer_terminal_backfill",
|
|
createdAt: `2026-06-20T11:19:0${sourceSeq}.000Z`
|
|
}
|
|
});
|
|
}
|
|
|
|
await writeWorkbenchProjectionSession({
|
|
accessController,
|
|
runtimeStore,
|
|
traceId: "trc_writer_terminal_backfill",
|
|
ownerUserId: "usr_writer",
|
|
ownerRole: "user",
|
|
sessionId: "ses_writer_terminal_backfill",
|
|
projectId: "prj_writer",
|
|
conversationId: "cnv_writer_terminal_backfill",
|
|
threadId: "thread-writer-terminal-backfill",
|
|
status: "completed",
|
|
payload: {
|
|
traceId: "trc_writer_terminal_backfill",
|
|
status: "completed",
|
|
finalResponse: { text: "BACKFILL_DONE", status: "completed", traceId: "trc_writer_terminal_backfill" },
|
|
runnerTrace: {
|
|
traceId: "trc_writer_terminal_backfill",
|
|
status: "completed",
|
|
startedAt: "2026-06-20T11:19:01.000Z",
|
|
lastEventAt: "2026-06-20T11:19:13.000Z",
|
|
finishedAt: "2026-06-20T11:19:13.000Z",
|
|
eventCount: 3,
|
|
events: []
|
|
},
|
|
agentRun: { runId: "run_writer_terminal_backfill", commandId: "cmd_writer_terminal_backfill", status: "completed", terminalStatus: "completed", lastSeq: 3 },
|
|
updatedAt: "2026-06-20T11:19:13.000Z"
|
|
},
|
|
session: {
|
|
sessionStatus: "completed",
|
|
messages: [
|
|
{ messageId: "msg_writer_backfill_user", role: "user", text: "question", status: "sent", turnId: "trc_writer_terminal_backfill", traceId: "trc_writer_terminal_backfill" },
|
|
{ messageId: "msg_writer_backfill_agent", role: "agent", text: "BACKFILL_DONE", status: "completed", turnId: "trc_writer_terminal_backfill", traceId: "trc_writer_terminal_backfill" }
|
|
],
|
|
finalResponse: { text: "BACKFILL_DONE", status: "completed", traceId: "trc_writer_terminal_backfill" }
|
|
}
|
|
});
|
|
|
|
const loaded = runtimeStore.queryWorkbenchFacts({ traceId: "trc_writer_terminal_backfill", families: ["traceEvents", "checkpoints"], limit: 20 });
|
|
const events = loaded.facts.traceEvents;
|
|
assert.deepEqual(events.map((event) => event.projectedSeq), [1, 2, 3, 4, 5]);
|
|
assert.deepEqual(events.slice(-2).map((event) => event.label), ["agentrun:assistant:message", "agentrun:result:completed"]);
|
|
assert.equal(events.at(-2).text, "BACKFILL_DONE");
|
|
assert.equal(events.at(-1).terminal, true);
|
|
assert.equal(events.at(-1).sealed, true);
|
|
assert.equal(loaded.facts.checkpoints[0].projectedSeq, 5);
|
|
assert.equal(loaded.facts.checkpoints[0].projectionStatus, "caught_up");
|
|
});
|
|
|
|
test("workbench projection writer does not rewrite prior-turn messages from merged owner session", async () => {
|
|
const factWrites = [];
|
|
const runtimeStore = {
|
|
async writeWorkbenchFacts(params, requestMeta) {
|
|
factWrites.push({ params, requestMeta });
|
|
return { written: true, facts: params.facts };
|
|
}
|
|
};
|
|
const accessController = {
|
|
async recordAgentSessionOwner(input) {
|
|
return {
|
|
id: input.sessionId,
|
|
projectId: input.projectId,
|
|
ownerUserId: input.ownerUserId,
|
|
conversationId: input.conversationId,
|
|
threadId: input.threadId,
|
|
lastTraceId: input.traceId,
|
|
status: input.status,
|
|
updatedAt: "2026-06-20T11:10:00.000Z",
|
|
session: {
|
|
sessionStatus: "completed",
|
|
lastTraceId: input.traceId,
|
|
messages: [
|
|
{ messageId: "msg_writer_r1_user", role: "user", text: "round1 prompt", status: "sent", turnId: "trc_writer_r1", traceId: "trc_writer_r1" },
|
|
{ messageId: "msg_writer_r1_agent", role: "agent", text: "WRONG_ROUND2_TEXT", status: "completed", turnId: "trc_writer_r1", traceId: "trc_writer_r1" },
|
|
...input.session.messages
|
|
]
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
await writeWorkbenchProjectionSession({
|
|
accessController,
|
|
runtimeStore,
|
|
traceId: "trc_writer_r2",
|
|
ownerUserId: "usr_writer",
|
|
ownerRole: "user",
|
|
sessionId: "ses_writer_multi_turn",
|
|
projectId: "prj_writer",
|
|
conversationId: "cnv_writer_multi_turn",
|
|
threadId: "thread-writer-multi-turn",
|
|
status: "completed",
|
|
payload: {
|
|
traceId: "trc_writer_r2",
|
|
status: "completed",
|
|
finalResponse: { text: "ROUND2_DONE", status: "completed", traceId: "trc_writer_r2" },
|
|
agentRun: { runId: "run_writer_r2", commandId: "cmd_writer_r2", status: "completed", terminalStatus: "completed", lastSeq: 9 },
|
|
updatedAt: "2026-06-20T11:10:00.000Z"
|
|
},
|
|
session: {
|
|
sessionStatus: "completed",
|
|
messages: [
|
|
{ messageId: "msg_writer_r2_user", role: "user", text: "round2 prompt", status: "sent", turnId: "trc_writer_r2", traceId: "trc_writer_r2" },
|
|
{ messageId: "msg_writer_r2_agent", role: "agent", text: "ROUND2_DONE", status: "completed", turnId: "trc_writer_r2", traceId: "trc_writer_r2" }
|
|
]
|
|
}
|
|
});
|
|
|
|
assert.equal(factWrites.length, 1);
|
|
const facts = factWrites[0].params.facts;
|
|
assert.deepEqual(facts.messages.map((message) => message.messageId), ["msg_writer_r2_user", "msg_writer_r2_agent"]);
|
|
assert.equal(facts.messages.some((message) => message.traceId === "trc_writer_r1"), false);
|
|
assert.equal(facts.messages.find((message) => message.messageId === "msg_writer_r2_agent").text, "ROUND2_DONE");
|
|
assert.equal(facts.turns[0].traceId, "trc_writer_r2");
|
|
assert.equal(facts.turns[0].finalResponse.text, "ROUND2_DONE");
|
|
});
|
|
|
|
test("workbench projection writer seals canceled AgentRun turns with canonical cancel final response", async () => {
|
|
const factWrites = [];
|
|
const runtimeStore = {
|
|
async writeWorkbenchFacts(params, requestMeta) {
|
|
factWrites.push({ params, requestMeta });
|
|
return { written: true, facts: params.facts };
|
|
}
|
|
};
|
|
const accessController = {
|
|
async recordAgentSessionOwner(input) {
|
|
return {
|
|
id: input.sessionId,
|
|
projectId: input.projectId,
|
|
ownerUserId: input.ownerUserId,
|
|
conversationId: input.conversationId,
|
|
threadId: input.threadId,
|
|
lastTraceId: input.traceId,
|
|
status: input.status,
|
|
session: input.session,
|
|
updatedAt: "2026-06-20T11:02:00.000Z"
|
|
};
|
|
}
|
|
};
|
|
|
|
await writeWorkbenchProjectionSession({
|
|
accessController,
|
|
runtimeStore,
|
|
traceId: "trc_writer_canceled",
|
|
ownerUserId: "usr_writer",
|
|
ownerRole: "user",
|
|
sessionId: "ses_writer_canceled",
|
|
projectId: "prj_writer",
|
|
conversationId: "cnv_writer_canceled",
|
|
threadId: "thread-writer-canceled",
|
|
status: "canceled",
|
|
payload: {
|
|
traceId: "trc_writer_canceled",
|
|
status: "canceled",
|
|
finalResponse: { text: "command cancelled before backend start", status: "canceled", traceId: "trc_writer_canceled" },
|
|
userMessage: "当前 AgentRun 请求已取消;traceId/runId/commandId 已保留,可重试上一条消息。",
|
|
runnerTrace: {
|
|
traceId: "trc_writer_canceled",
|
|
status: "canceled",
|
|
events: [{ type: "cancel", status: "canceled", terminal: true, createdAt: "2026-06-20T11:02:00.000Z" }],
|
|
updatedAt: "2026-06-20T11:02:00.000Z"
|
|
},
|
|
agentRun: { runId: "run_writer_canceled", commandId: "cmd_writer_canceled", status: "cancelled", commandState: "cancelled", terminalStatus: "cancelled", lastSeq: 12 },
|
|
updatedAt: "2026-06-20T11:02:00.000Z"
|
|
},
|
|
session: {
|
|
sessionStatus: "canceled",
|
|
messages: [
|
|
{ messageId: "msg_writer_canceled_user", role: "user", text: "cancel me", status: "sent", turnId: "trc_writer_canceled", traceId: "trc_writer_canceled" },
|
|
{ messageId: "msg_writer_canceled_agent", role: "agent", text: "", status: "canceled", turnId: "trc_writer_canceled", traceId: "trc_writer_canceled" }
|
|
]
|
|
}
|
|
});
|
|
|
|
assert.equal(factWrites.length, 1);
|
|
const facts = factWrites[0].params.facts;
|
|
const agentMessage = facts.messages.find((message) => message.messageId === "msg_writer_canceled_agent");
|
|
assert.equal(facts.sessions[0].status, "canceled");
|
|
assert.equal(facts.turns[0].terminal, true);
|
|
assert.equal(facts.turns[0].finalResponse.text, "hwlab-user-cancel");
|
|
assert.equal(facts.turns[0].assistantText, "hwlab-user-cancel");
|
|
assert.equal(agentMessage.text, "hwlab-user-cancel");
|
|
assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_canceled_agent" && part.partType === "final_response" && part.text === "hwlab-user-cancel" && part.sealed === true), true);
|
|
});
|
|
|
|
test("workbench projection writer does not seal running assistant text as final response", async () => {
|
|
const factWrites = [];
|
|
const runtimeStore = {
|
|
async writeWorkbenchFacts(params, requestMeta) {
|
|
factWrites.push({ params, requestMeta });
|
|
return { written: true, facts: params.facts };
|
|
}
|
|
};
|
|
const accessController = {
|
|
async recordAgentSessionOwner(input) {
|
|
return {
|
|
id: input.sessionId,
|
|
projectId: input.projectId,
|
|
ownerUserId: input.ownerUserId,
|
|
conversationId: input.conversationId,
|
|
threadId: input.threadId,
|
|
lastTraceId: input.traceId,
|
|
status: input.status,
|
|
session: input.session,
|
|
updatedAt: "2026-06-20T11:05:00.000Z"
|
|
};
|
|
}
|
|
};
|
|
|
|
await writeWorkbenchProjectionSession({
|
|
accessController,
|
|
runtimeStore,
|
|
traceId: "trc_writer_running",
|
|
ownerUserId: "usr_writer",
|
|
ownerRole: "user",
|
|
sessionId: "ses_writer_running",
|
|
projectId: "prj_writer",
|
|
conversationId: "cnv_writer_running",
|
|
threadId: "thread-writer-running",
|
|
status: "running",
|
|
payload: {
|
|
traceId: "trc_writer_running",
|
|
status: "running",
|
|
assistantText: "backend diagnostic text should not become final",
|
|
updatedAt: "2026-06-20T11:05:00.000Z"
|
|
},
|
|
session: {
|
|
sessionStatus: "running",
|
|
messages: [
|
|
{ messageId: "msg_writer_running_user", role: "user", text: "question", status: "sent", turnId: "trc_writer_running", traceId: "trc_writer_running" },
|
|
{ messageId: "msg_writer_running_agent", role: "agent", text: "", status: "running", turnId: "trc_writer_running", traceId: "trc_writer_running" }
|
|
]
|
|
}
|
|
});
|
|
|
|
assert.equal(factWrites.length, 1);
|
|
const facts = factWrites[0].params.facts;
|
|
const agentMessage = facts.messages.find((message) => message.messageId === "msg_writer_running_agent");
|
|
assert.equal(agentMessage.text, "");
|
|
assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_running_agent"), false);
|
|
assert.equal(facts.turns[0].terminal, false);
|
|
assert.equal(facts.turns[0].finalResponse, null);
|
|
assert.equal(facts.turns[0].assistantText, null);
|
|
});
|
|
|
|
test("workbench projection event commit writes trace event and checkpoint facts", async () => {
|
|
const factWrites = [];
|
|
let now = "2026-06-20T11:01:05.000Z";
|
|
const baseStore = createCloudRuntimeStore({ now: () => now });
|
|
const runtimeStore = {
|
|
allocateWorkbenchProjectedSeq(params, requestMeta) {
|
|
return baseStore.allocateWorkbenchProjectedSeq(params, requestMeta);
|
|
},
|
|
queryWorkbenchFacts(params) {
|
|
return baseStore.queryWorkbenchFacts(params);
|
|
},
|
|
async writeWorkbenchFacts(params, requestMeta) {
|
|
const result = baseStore.writeWorkbenchFacts(params, requestMeta);
|
|
factWrites.push({ params, requestMeta });
|
|
return result;
|
|
}
|
|
};
|
|
|
|
now = "2026-06-20T11:03:30.000Z";
|
|
await writeWorkbenchProjectionEvent({
|
|
runtimeStore,
|
|
event: {
|
|
traceId: "trc_writer_event",
|
|
sessionId: "ses_writer_event",
|
|
seq: 7,
|
|
sourceSeq: 70,
|
|
type: "backend",
|
|
status: "running",
|
|
label: "agentrun:backend:running",
|
|
runId: "run_writer_event",
|
|
commandId: "cmd_writer_event",
|
|
startedAt: "2026-06-20T11:00:30.000Z",
|
|
durationMs: 3000,
|
|
createdAt: "2026-06-20T11:01:00.000Z"
|
|
}
|
|
});
|
|
await writeWorkbenchProjectionEvent({
|
|
runtimeStore,
|
|
event: {
|
|
traceId: "trc_writer_event",
|
|
sessionId: "ses_writer_event",
|
|
seq: 8,
|
|
sourceSeq: 71,
|
|
type: "result",
|
|
status: "completed",
|
|
label: "agentrun:result:completed",
|
|
terminal: true,
|
|
runId: "run_writer_event",
|
|
commandId: "cmd_writer_event",
|
|
createdAt: "2026-06-20T11:02:00.000Z"
|
|
}
|
|
});
|
|
|
|
assert.equal(factWrites.length, 2);
|
|
assert.equal(factWrites[0].params.facts.traceEvents[0].projectedSeq, 1);
|
|
assert.equal(factWrites[0].params.facts.checkpoints[0].projectionStatus, "projecting");
|
|
assert.equal(factWrites[0].params.facts.checkpoints[0].startedAt, "2026-06-20T11:00:30.000Z");
|
|
assert.equal(factWrites[0].params.facts.checkpoints[0].lastEventAt, "2026-06-20T11:01:00.000Z");
|
|
assert.equal(factWrites[0].params.facts.checkpoints[0].durationMs, null);
|
|
assert.equal(factWrites[0].params.facts.checkpoints[0].timing.durationMs, null);
|
|
assert.equal(factWrites[1].params.facts.sessions[0].status, "completed");
|
|
assert.equal(factWrites[1].params.facts.traceEvents[0].sealed, true);
|
|
assert.equal(factWrites[1].params.facts.traceEvents[0].projectedSeq, 2);
|
|
assert.equal(factWrites[1].params.facts.checkpoints[0].projectionStatus, "caught_up");
|
|
assert.equal(factWrites[1].params.facts.checkpoints[0].durationMs, 90000);
|
|
assert.equal(factWrites[1].params.facts.checkpoints[0].finishedAt, "2026-06-20T11:02:00.000Z");
|
|
assert.equal(factWrites[1].params.facts.checkpoints[0].lastEventAt, "2026-06-20T11:02:00.000Z");
|
|
});
|
|
|
|
test("workbench projection event writer allocates projectedSeq idempotently by source event identity", async () => {
|
|
const runtimeStore = createCloudRuntimeStore({ now: () => "2026-06-20T12:00:00.000Z" });
|
|
|
|
const repeatedSourceEvent = {
|
|
traceId: "trc_writer_idempotent",
|
|
sessionId: "ses_writer_idempotent",
|
|
seq: 1,
|
|
source: "agentrun",
|
|
sourceSeq: 70,
|
|
type: "backend",
|
|
status: "running",
|
|
label: "agentrun:backend:running",
|
|
runId: "run_writer_idempotent",
|
|
commandId: "cmd_writer_idempotent",
|
|
startedAt: "2026-06-20T12:00:01.000Z",
|
|
createdAt: "2026-06-20T12:00:01.000Z"
|
|
};
|
|
await writeWorkbenchProjectionEvent({ runtimeStore, event: repeatedSourceEvent });
|
|
await writeWorkbenchProjectionEvent({
|
|
runtimeStore,
|
|
event: {
|
|
...repeatedSourceEvent,
|
|
seq: 1,
|
|
sourceSeq: 71,
|
|
label: "agentrun:backend:runner-ready",
|
|
startedAt: null,
|
|
durationMs: 999,
|
|
createdAt: "2026-06-20T12:00:02.000Z"
|
|
}
|
|
});
|
|
await writeWorkbenchProjectionEvent({ runtimeStore, event: repeatedSourceEvent });
|
|
|
|
const loaded = runtimeStore.queryWorkbenchFacts({ traceId: "trc_writer_idempotent", families: ["traceEvents", "checkpoints"], limit: 10 });
|
|
assert.deepEqual(loaded.facts.traceEvents.map((event) => event.projectedSeq), [1, 2]);
|
|
assert.deepEqual(loaded.facts.traceEvents.map((event) => event.sourceSeq), [70, 71]);
|
|
assert.equal(loaded.facts.checkpoints[0].projectedSeq, 2);
|
|
assert.equal(loaded.facts.checkpoints[0].startedAt, "2026-06-20T12:00:01.000Z");
|
|
assert.equal(loaded.facts.checkpoints[0].lastEventAt, "2026-06-20T12:00:02.000Z");
|
|
assert.equal(loaded.facts.checkpoints[0].durationMs, null);
|
|
});
|