diff --git a/extensions/matrix/src/__traces__/cancel-mid-stream.trace.jsonl b/extensions/matrix/src/__traces__/cancel-mid-stream.trace.jsonl new file mode 100644 index 000000000000..2bd079665a08 --- /dev/null +++ b/extensions/matrix/src/__traces__/cancel-mid-stream.trace.jsonl @@ -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":"
Working on the fix
","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":"*Working on the fix: patching now.
","m.new_content":{"body":"Working on the fix: patching now.","format":"org.matrix.custom.html","formatted_body":"Working on the fix: patching now.
","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"}} diff --git a/extensions/matrix/src/__traces__/final-in-place.trace.jsonl b/extensions/matrix/src/__traces__/final-in-place.trace.jsonl new file mode 100644 index 000000000000..d1cffdd85092 --- /dev/null +++ b/extensions/matrix/src/__traces__/final-in-place.trace.jsonl @@ -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":"Ship checklist complete.
","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":"*Ship checklist complete. Tagging the release.
","m.new_content":{"body":"Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"Ship checklist complete. Tagging the release.
","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":"*Ship checklist complete. Tagging the release.
","m.new_content":{"body":"Ship checklist complete. Tagging the release.","format":"org.matrix.custom.html","formatted_body":"Ship checklist complete. Tagging the release.
","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"}} diff --git a/extensions/matrix/src/__traces__/final-mentions-fresh.trace.jsonl b/extensions/matrix/src/__traces__/final-mentions-fresh.trace.jsonl new file mode 100644 index 000000000000..560ca7197b1b --- /dev/null +++ b/extensions/matrix/src/__traces__/final-mentions-fresh.trace.jsonl @@ -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":"Paging @oncall:example.org
","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":"*Paging @oncall:example.org for the deploy review.
","m.new_content":{"body":"Paging @oncall:example.org for the deploy review.","format":"org.matrix.custom.html","formatted_body":"Paging @oncall:example.org for the deploy review.
","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":"Paging @oncall:example.org for the deploy review.
","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"}} diff --git a/extensions/matrix/src/__traces__/streaming-happy-multi-generation.trace.jsonl b/extensions/matrix/src/__traces__/streaming-happy-multi-generation.trace.jsonl new file mode 100644 index 000000000000..c9dd17e534bf --- /dev/null +++ b/extensions/matrix/src/__traces__/streaming-happy-multi-generation.trace.jsonl @@ -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":"Build check:
","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":"*Build check: all suites green.
","m.new_content":{"body":"Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"Build check: all suites green.
","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":"*Build check: all suites green.
","m.new_content":{"body":"Build check: all suites green.","format":"org.matrix.custom.html","formatted_body":"Build check: all suites green.
","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":"Deploying to production now.
","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":"*Deploying to production now. Watching health checks.
","m.new_content":{"body":"Deploying to production now. Watching health checks.","format":"org.matrix.custom.html","formatted_body":"Deploying to production now. Watching health checks.
","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":"*Deploying to production now. Health checks green.
","m.mentions":{},"m.new_content":{"body":"Deploying to production now. Health checks green.","format":"org.matrix.custom.html","formatted_body":"Deploying to production now. Health checks green.
","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"}} diff --git a/extensions/matrix/src/delivery-trace.test.ts b/extensions/matrix/src/delivery-trace.test.ts new file mode 100644 index 000000000000..086b88b734d3 --- /dev/null +++ b/extensions/matrix/src/delivery-trace.test.ts @@ -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