test(matrix): delivery trace goldens for generation-scoped edit streaming (#104596)

This commit is contained in:
Peter Steinberger
2026-07-11 10:55:27 -07:00
committed by GitHub
parent e9b7a9a912
commit 8bebdd0d03
5 changed files with 424 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{"seq":1,"at":0,"dir":"out","kind":"sendReadReceipt","data":{"target":"$inbound-1"}}
{"seq":2,"at":0,"dir":"in","kind":"reply-start"}
{"seq":3,"at":0,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":true},"target":"!room:example.org"}}
{"seq":4,"at":0,"dir":"in","kind":"partial","data":{"text":"Working on the fix"}}
{"seq":5,"at":0,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Working on the fix","format":"org.matrix.custom.html","formatted_body":"<p>Working on the fix</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e1"},"target":"!room:example.org"}}
{"seq":6,"at":300,"dir":"in","kind":"partial","data":{"text":"Working on the fix: patching now."}}
{"seq":7,"at":600,"dir":"in","kind":"cancel"}
{"seq":8,"at":600,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":9,"at":600,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Working on the fix: patching now.","format":"org.matrix.custom.html","formatted_body":"* <p>Working on the fix: patching now.</p>","m.new_content":{"body":"Working on the fix: patching now.","format":"org.matrix.custom.html","formatted_body":"<p>Working on the fix: patching now.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e2"},"target":"!room:example.org"}}
{"seq":10,"at":600,"dir":"out","kind":"redactEvent","data":{"result":{"event_id":"$e3"},"target":"$e1"}}
{"seq":11,"at":600,"dir":"in","kind":"idle"}
{"seq":12,"at":600,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":false},"target":"!room:example.org"}}

View File

@@ -0,0 +1,13 @@
{"seq":1,"at":0,"dir":"out","kind":"sendReadReceipt","data":{"target":"$inbound-1"}}
{"seq":2,"at":0,"dir":"in","kind":"reply-start"}
{"seq":3,"at":0,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":true},"target":"!room:example.org"}}
{"seq":4,"at":0,"dir":"in","kind":"partial","data":{"text":"Ship checklist complete."}}
{"seq":5,"at":0,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Ship checklist complete.","format":"org.matrix.custom.html","formatted_body":"<p>Ship checklist complete.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e1"},"target":"!room:example.org"}}
{"seq":6,"at":400,"dir":"in","kind":"partial","data":{"text":"Ship checklist complete. Tagging the release."}}
{"seq":7,"at":1000,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":8,"at":1000,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"* <p>Ship checklist complete. Tagging the release.</p>","m.new_content":{"body":"Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"<p>Ship checklist complete. Tagging the release.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e2"},"target":"!room:example.org"}}
{"seq":9,"at":1100,"dir":"in","kind":"final","data":{"text":"Ship checklist complete. Tagging the release."}}
{"seq":10,"at":1100,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":11,"at":1100,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"* <p>Ship checklist complete. Tagging the release.</p>","m.new_content":{"body":"Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"<p>Ship checklist complete. Tagging the release.</p>","msgtype":"m.text"},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text"},"result":{"event_id":"$e3"},"target":"!room:example.org"}}
{"seq":12,"at":1100,"dir":"in","kind":"idle"}
{"seq":13,"at":1100,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":false},"target":"!room:example.org"}}

View File

@@ -0,0 +1,13 @@
{"seq":1,"at":0,"dir":"out","kind":"sendReadReceipt","data":{"target":"$inbound-1"}}
{"seq":2,"at":0,"dir":"in","kind":"reply-start"}
{"seq":3,"at":0,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":true},"target":"!room:example.org"}}
{"seq":4,"at":0,"dir":"in","kind":"partial","data":{"text":"Paging @oncall:example.org"}}
{"seq":5,"at":0,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Paging @oncall:example.org","format":"org.matrix.custom.html","formatted_body":"<p>Paging @oncall:example.org</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e1"},"target":"!room:example.org"}}
{"seq":6,"at":400,"dir":"in","kind":"partial","data":{"text":"Paging @oncall:example.org for the deploy review."}}
{"seq":7,"at":1000,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":8,"at":1000,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Paging @oncall:example.org for the deploy review.","format":"org.matrix.custom.html","formatted_body":"* <p>Paging @oncall:example.org for the deploy review.</p>","m.new_content":{"body":"Paging @oncall:example.org for the deploy review.","format":"org.matrix.custom.html","formatted_body":"<p>Paging @oncall:example.org for the deploy review.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e2"},"target":"!room:example.org"}}
{"seq":9,"at":1100,"dir":"in","kind":"final","data":{"text":"Paging @oncall:example.org for the deploy review."}}
{"seq":10,"at":1100,"dir":"out","kind":"redactEvent","data":{"result":{"event_id":"$e3"},"target":"$e1"}}
{"seq":11,"at":1100,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Paging @oncall:example.org for the deploy review.","format":"org.matrix.custom.html","formatted_body":"<p>Paging <a href=\"https://matrix.to/#/%40oncall%3Aexample.org\">@oncall:example.org</a> for the deploy review.</p>","m.mentions":{"user_ids":["@oncall:example.org"]},"msgtype":"m.text"},"result":{"event_id":"$e4"},"target":"!room:example.org"}}
{"seq":12,"at":1100,"dir":"in","kind":"idle"}
{"seq":13,"at":1100,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":false},"target":"!room:example.org"}}

View File

@@ -0,0 +1,24 @@
{"seq":1,"at":0,"dir":"out","kind":"sendReadReceipt","data":{"target":"$inbound-1"}}
{"seq":2,"at":0,"dir":"in","kind":"reply-start"}
{"seq":3,"at":0,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":true},"target":"!room:example.org"}}
{"seq":4,"at":0,"dir":"in","kind":"partial","data":{"text":"Build check:"}}
{"seq":5,"at":0,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Build check:","format":"org.matrix.custom.html","formatted_body":"<p>Build check:</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e1"},"target":"!room:example.org"}}
{"seq":6,"at":400,"dir":"in","kind":"partial","data":{"text":"Build check: all suites green."}}
{"seq":7,"at":1000,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":8,"at":1000,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"* <p>Build check: all suites green.</p>","m.new_content":{"body":"Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"<p>Build check: all suites green.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e2"},"target":"!room:example.org"}}
{"seq":9,"at":1100,"dir":"in","kind":"block-final","data":{"text":"Build check: all suites green."}}
{"seq":10,"at":1100,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e1"}}
{"seq":11,"at":1100,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"* <p>Build check: all suites green.</p>","m.new_content":{"body":"Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"<p>Build check: all suites green.</p>","msgtype":"m.text"},"m.relates_to":{"event_id":"$e1","rel_type":"m.replace"},"msgtype":"m.text"},"result":{"event_id":"$e3"},"target":"!room:example.org"}}
{"seq":12,"at":1100,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":true},"target":"!room:example.org"}}
{"seq":13,"at":1100,"dir":"in","kind":"tool-progress","data":{"name":"deploy","phase":"start"}}
{"seq":14,"at":1100,"dir":"in","kind":"tool-progress","data":{"name":"deploy","phase":"result"}}
{"seq":15,"at":1100,"dir":"in","kind":"partial","data":{"text":"Deploying to production now."}}
{"seq":16,"at":1100,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"Deploying to production now.","format":"org.matrix.custom.html","formatted_body":"<p>Deploying to production now.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e4"},"target":"!room:example.org"}}
{"seq":17,"at":1500,"dir":"in","kind":"partial","data":{"text":"Deploying to production now. Watching health checks."}}
{"seq":18,"at":2100,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e4"}}
{"seq":19,"at":2100,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Deploying to production now. Watching health checks.","format":"org.matrix.custom.html","formatted_body":"* <p>Deploying to production now. Watching health checks.</p>","m.new_content":{"body":"Deploying to production now. Watching health checks.","format":"org.matrix.custom.html","formatted_body":"<p>Deploying to production now. Watching health checks.</p>","msgtype":"m.text","org.matrix.msc4357.live":{}},"m.relates_to":{"event_id":"$e4","rel_type":"m.replace"},"msgtype":"m.text","org.matrix.msc4357.live":{}},"result":{"event_id":"$e5"},"target":"!room:example.org"}}
{"seq":20,"at":2200,"dir":"in","kind":"final","data":{"text":"Deploying to production now. Health checks green."}}
{"seq":21,"at":2200,"dir":"out","kind":"getEvent","data":{"result":{"found":true},"target":"$e4"}}
{"seq":22,"at":2200,"dir":"out","kind":"sendMessage","data":{"payload":{"body":"* Deploying to production now. Health checks green.","format":"org.matrix.custom.html","formatted_body":"* <p>Deploying to production now. Health checks green.</p>","m.mentions":{},"m.new_content":{"body":"Deploying to production now. Health checks green.","format":"org.matrix.custom.html","formatted_body":"<p>Deploying to production now. Health checks green.</p>","m.mentions":{},"msgtype":"m.text"},"m.relates_to":{"event_id":"$e4","rel_type":"m.replace"},"msgtype":"m.text"},"result":{"event_id":"$e6"},"target":"!room:example.org"}}
{"seq":23,"at":2200,"dir":"in","kind":"idle"}
{"seq":24,"at":2200,"dir":"out","kind":"setTyping","data":{"payload":{"timeoutMs":30000,"typing":false},"target":"!room:example.org"}}

View File

@@ -0,0 +1,362 @@
// Matrix delivery trace goldens: replayable wire-level lifecycle recordings.
//
// Drives the real monitor handler (createMatrixRoomMessageHandler) end to end
// through the shared handler test harness: IN steps map onto the captured
// reply-dispatcher wiring (deliver, typing callbacks, onPartialReply,
// onBlockReplyQueued, onAssistantMessageStart), OUT events are the raw Matrix
// client calls (sendMessage/getEvent/redactEvent/setTyping/sendReadReceipt)
// observed at a recording client. The room is plain (E2EE out of scope), so
// goldens capture wire content and event-id semantics: every m.replace edit
// mints a new event id while the logical target id stays stable, and previews
// are mentions-inert until a final either edits in place or gets redacted and
// re-sent as a fresh mention-bearing event.
// Refresh goldens with OPENCLAW_TRACE_UPDATE=1 (see delivery-trace harness docs).
import {
deliveryTraceScenarios,
expectDeliveryTraceMatchesGolden,
runDeliveryTraceScenario,
type DeliveryTraceInStep,
type DeliveryTraceScenario,
type DeliveryTraceScenarioName,
type DeliveryTraceStep,
type WireRecorder,
} from "openclaw/plugin-sdk/channel-contract-testing";
import {
implicitMentionKindWhen,
resolveInboundMentionDecision,
} from "openclaw/plugin-sdk/channel-mention-gating";
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
import {
chunkMarkdownTextWithMode,
resolveChunkMode,
resolveTextChunkLimit,
} from "openclaw/plugin-sdk/reply-chunking";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
import { beforeAll, describe, it, vi } from "vitest";
import {
createMatrixHandlerTestHarness,
createMatrixTextMessageEvent,
} from "./matrix/monitor/handler.test-helpers.js";
import type { MatrixClient } from "./matrix/sdk.js";
import type { PluginRuntime, ReplyPayload } from "./runtime-api.js";
import { installMatrixTestRuntime } from "./test-runtime.js";
const ROOM_ID = "!room:example.org";
const BOT_USER_ID = "@openclaw:example.org";
const INBOUND_EVENT_ID = "$inbound-1";
beforeAll(() => {
// send.ts/replies.ts read text helpers and the monitor's mention gating from
// the module runtime slot; bind the real chunking/table implementations so
// rendered wire content is part of the recorded lifecycle (mirrors the
// feishu adoption's runtime stub). Media helpers stay absent: these
// scenarios are text-only and a media lookup should fail loudly.
installMatrixTestRuntime({
logging: { shouldLogVerbose: () => false },
channel: {
text: {
resolveMarkdownTableMode,
convertMarkdownTables,
resolveTextChunkLimit,
resolveChunkMode,
chunkMarkdownTextWithMode,
},
mentions: {
buildMentionRegexes: () => [],
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
patterns.some((pattern) => pattern.test(text)),
matchesMentionWithExplicit: () => false,
implicitMentionKindWhen,
resolveInboundMentionDecision,
},
} as unknown as PluginRuntime["channel"],
});
});
function createRecordingMatrixClient(recorder: WireRecorder): Partial<MatrixClient> {
// Deterministic sequential event ids replace homeserver-minted opaque ids
// (the normalizer seam from the trace spec, applied at the mock boundary):
// every event on the wire consumes one id, so goldens show edits minting new
// ids while their m.relates_to target stays the original event.
const eventContentById = new Map<string, Record<string, unknown>>();
let eventCount = 0;
const mintEventId = () => {
eventCount += 1;
return `$e${eventCount}`;
};
const client: Partial<MatrixClient> = {
getUserId: async () => BOT_USER_ID,
sendMessage: async (roomId: string, content: Record<string, unknown>) => {
const eventId = mintEventId();
// Snapshot before recording: edit flows reuse content structures, and the
// recorder serializes at compare time.
const payload = structuredClone(content);
eventContentById.set(eventId, payload);
recorder.recordWireCall({
method: "sendMessage",
target: roomId,
payload,
result: { event_id: eventId },
});
return eventId;
},
// editMessageMatrix fetches the prior event before every edit (mention
// diffing / thread guard) — an extra RTT per edit that the trace captures
// even for mentions-inert previews where the result goes unused.
getEvent: async (_roomId: string, eventId: string) => {
recorder.recordWireCall({
method: "getEvent",
target: eventId,
result: { found: eventContentById.has(eventId) },
});
return { event_id: eventId, content: eventContentById.get(eventId) ?? {} };
},
redactEvent: async (_roomId: string, eventId: string) => {
// Redactions are events too: abandoning a draft consumes an event id.
const redactionEventId = mintEventId();
recorder.recordWireCall({
method: "redactEvent",
target: eventId,
result: { event_id: redactionEventId },
});
return redactionEventId;
},
setTyping: async (roomId: string, typing: boolean, timeoutMs: number) => {
recorder.recordWireCall({
method: "setTyping",
target: roomId,
payload: { typing, timeoutMs },
});
},
sendReadReceipt: async (_roomId: string, eventId: string) => {
recorder.recordWireCall({ method: "sendReadReceipt", target: eventId });
},
};
return client;
}
type MatrixTraceDeliver = (payload: ReplyPayload, info: { kind: string }) => Promise<void>;
type MatrixTraceReplyOptions = {
onPartialReply?: (payload: { text: string }) => void;
onBlockReplyQueued?: (
payload: { text?: string },
context?: { assistantMessageIndex?: number },
) => Promise<void> | void;
onAssistantMessageStart?: () => void;
};
async function setupMatrixTrace(recorder: WireRecorder) {
const client = createRecordingMatrixClient(recorder);
let capturedDeliver: MatrixTraceDeliver | undefined;
let capturedOnReplyStart: (() => Promise<void> | void) | undefined;
let capturedOnIdle: (() => void) | undefined;
let capturedReplyOptions: MatrixTraceReplyOptions | undefined;
let resolveCaptured: (() => void) | undefined;
const captured = new Promise<void>((resolve) => {
resolveCaptured = resolve;
});
const notifyCaptured = () => {
if (capturedDeliver && capturedReplyOptions) {
resolveCaptured?.();
}
};
// The scripted steps stand in for the model run: dispatchReplyFromConfig
// stays pending until the script's final/cancel step settles it, so the
// handler's post-dispatch flow (including the finally-block draft abandon
// path) runs exactly where the real run would settle.
type DispatchResult = { queuedFinal: boolean; counts: { final: number; block: number } };
let releaseRun: ((result: DispatchResult) => void) | undefined;
const runGate = new Promise<DispatchResult>((resolve) => {
releaseRun = resolve;
});
const { handler } = createMatrixHandlerTestHarness({
streaming: "partial",
blockStreamingEnabled: true,
client,
resolveMarkdownTableMode: () => resolveMarkdownTableMode({ cfg: {}, channel: "matrix" }),
createReplyDispatcherWithTyping: (options: Record<string, unknown> | undefined) => {
capturedDeliver = options?.deliver as MatrixTraceDeliver | undefined;
capturedOnReplyStart = options?.onReplyStart as typeof capturedOnReplyStart;
capturedOnIdle = options?.onIdle as typeof capturedOnIdle;
notifyCaptured();
return {
dispatcher: { markComplete: () => {}, waitForIdle: async () => {} },
replyOptions: {},
markDispatchIdle: () => {},
markRunComplete: () => {},
};
},
dispatchReplyFromConfig: (async (args: { replyOptions?: MatrixTraceReplyOptions }) => {
capturedReplyOptions = args?.replyOptions;
notifyCaptured();
const result = await runGate;
return { queuedFinal: result.queuedFinal, counts: { ...result.counts, tool: 0 } };
}) as never,
});
const handlerDone = handler(
ROOM_ID,
createMatrixTextMessageEvent({
eventId: INBOUND_EVENT_ID,
body: "deploy status?",
originServerTs: Date.now(),
}),
);
await Promise.race([
captured,
handlerDone.then(() => {
throw new Error("matrix handler settled before capturing dispatcher wiring");
}),
]);
// Drain the fire-and-forget inbound read receipt so it lands at a fixed
// position (before the first scripted step) in every recording.
await vi.advanceTimersByTimeAsync(0);
// Mirrors the runner's assistant message_start boundaries: the first index
// is minted at reply start and each tool round ends with the next assistant
// message beginning (handler resets its cumulative-text offsets there).
let assistantMessageIndex = 0;
const startAssistantMessage = () => {
assistantMessageIndex += 1;
capturedReplyOptions?.onAssistantMessageStart?.();
};
return async (step: DeliveryTraceInStep) => {
switch (step.kind) {
case "reply-start":
await capturedOnReplyStart?.();
startAssistantMessage();
break;
case "partial":
// Cumulative text for the current assistant message; the handler
// slices it into per-generation draft updates.
capturedReplyOptions?.onPartialReply?.({ text: step.text });
break;
case "block-final":
await capturedReplyOptions?.onBlockReplyQueued?.(
{ text: step.text },
{ assistantMessageIndex },
);
await capturedDeliver?.({ text: step.text }, { kind: "block" });
break;
case "tool-progress":
// Preview tool progress is not adopted here; a completed tool round
// means the next assistant message (generation) starts streaming.
if (step.phase === "result") {
startAssistantMessage();
}
break;
case "final":
await capturedDeliver?.(
{
...(step.text !== undefined ? { text: step.text } : {}),
...(step.mediaUrls ? { mediaUrls: step.mediaUrls } : {}),
...(step.isError ? { isError: true } : {}),
},
{ kind: "final" },
);
releaseRun?.({ queuedFinal: true, counts: { final: 1, block: 0 } });
await handlerDone;
break;
case "cancel":
// An aborted run settles the dispatch without a final payload; the
// handler's finally block flushes and redacts the unconsumed draft.
releaseRun?.({ queuedFinal: false, counts: { final: 0, block: 0 } });
await handlerDone;
break;
case "idle":
capturedOnIdle?.();
break;
case "wire-fault":
throw new Error("matrix trace scenarios do not script wire faults");
}
};
}
// Matrix records generation-scoped edit-streaming shapes the shared v1
// library does not model; the runner treats the scenario name as opaque data
// (it only keys the golden filename), so widening the closed union stays
// test-local instead of leaking matrix names into the core scenario library.
function matrixScenario(name: string, steps: readonly DeliveryTraceStep[]): DeliveryTraceScenario {
return { name: name as DeliveryTraceScenarioName, steps };
}
const GEN_ONE_PARTIAL = "Build check:";
const GEN_ONE_BLOCK = "Build check: all suites green.";
const GEN_TWO_PARTIAL = "Deploying to production now.";
const GEN_TWO_PREVIEW = "Deploying to production now. Watching health checks.";
const GEN_TWO_FINAL = "Deploying to production now. Health checks green.";
const IN_PLACE_PARTIAL = "Ship checklist complete.";
const IN_PLACE_FINAL = "Ship checklist complete. Tagging the release.";
const MENTION_PARTIAL = "Paging @oncall:example.org";
const MENTION_FINAL = "Paging @oncall:example.org for the deploy review.";
// The draft stream throttles edits at 1000ms; each generation streams one
// create plus one throttled m.replace edit before its boundary settles it.
const MATRIX_TRACE_SCENARIOS: readonly DeliveryTraceScenario[] = [
// Two generations separated by a tool round: one preview event per
// generation, cumulative slices, block finalize-in-place for generation one,
// and a final replace-edit because the final text differs from the last
// rendered preview of generation two.
matrixScenario("streaming-happy-multi-generation", [
{ kind: "reply-start" },
{ kind: "partial", text: GEN_ONE_PARTIAL },
{ kind: "advance", ms: 400 },
{ kind: "partial", text: GEN_ONE_BLOCK },
{ kind: "advance", ms: 700 },
{ kind: "block-final", text: GEN_ONE_BLOCK },
{ kind: "tool-progress", name: "deploy", phase: "start" },
{ kind: "tool-progress", name: "deploy", phase: "result" },
{ kind: "partial", text: GEN_TWO_PARTIAL },
{ kind: "advance", ms: 400 },
{ kind: "partial", text: GEN_TWO_PREVIEW },
{ kind: "advance", ms: 700 },
{ kind: "final", text: GEN_TWO_FINAL },
{ kind: "idle" },
]),
// Final text equals the last rendered preview: the draft is finalized in
// place with an edit that only clears the MSC4357 live marker.
matrixScenario("final-in-place", [
{ kind: "reply-start" },
{ kind: "partial", text: IN_PLACE_PARTIAL },
{ kind: "advance", ms: 400 },
{ kind: "partial", text: IN_PLACE_FINAL },
{ kind: "advance", ms: 700 },
{ kind: "final", text: IN_PLACE_FINAL },
{ kind: "idle" },
]),
// Previews are mentions-inert and an edit cannot retro-notify, so a final
// whose text would activate mentions redacts the preview and re-sends the
// final as a fresh mention-bearing event.
matrixScenario("final-mentions-fresh", [
{ kind: "reply-start" },
{ kind: "partial", text: MENTION_PARTIAL },
{ kind: "advance", ms: 400 },
{ kind: "partial", text: MENTION_FINAL },
{ kind: "advance", ms: 700 },
{ kind: "final", text: MENTION_FINAL },
{ kind: "idle" },
]),
// Shared abandon shape: the handler's finally block flushes the pending
// throttled edit, then redacts the unconsumed draft.
deliveryTraceScenarios["cancel-mid-stream"],
];
describe("matrix delivery trace goldens", () => {
for (const scenario of MATRIX_TRACE_SCENARIOS) {
it(`records ${scenario.name}`, async () => {
const events = await runDeliveryTraceScenario({
scenario,
setup: setupMatrixTrace,
});
expectDeliveryTraceMatchesGolden({
goldenUrl: new URL(`./__traces__/${scenario.name}.trace.jsonl`, import.meta.url),
events,
});
});
}
});