@archipelagolab/lobi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -0
- package/ENDOFFILE +0 -0
- package/EOF +0 -0
- package/LICENSE +21 -0
- package/SPEC-SUPPORT.md +116 -0
- package/YAMLEND +0 -0
- package/api.ts +18 -0
- package/archipelagolab-lobi-1.0.0.tgz +0 -0
- package/auth-presence.ts +56 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/docs/CHECKLIST.md +83 -0
- package/docs/FORK_SDK_GUIDE.md +279 -0
- package/helper-api.ts +3 -0
- package/index.test.ts +61 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +52 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +54 -0
- package/runtime-heavy-api.ts +1 -0
- package/scripts/migrate-to-lobi.sh +72 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/account-selection.test.ts +124 -0
- package/src/account-selection.ts +226 -0
- package/src/actions.account-propagation.test.ts +251 -0
- package/src/actions.test.ts +251 -0
- package/src/actions.ts +336 -0
- package/src/approval-auth.test.ts +23 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.test.ts +46 -0
- package/src/approval-handler.runtime.ts +400 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.test.ts +329 -0
- package/src/approval-native.ts +336 -0
- package/src/approval-reactions.test.ts +107 -0
- package/src/approval-reactions.ts +158 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +92 -0
- package/src/channel.account-paths.test.ts +102 -0
- package/src/channel.directory.test.ts +601 -0
- package/src/channel.resolve.test.ts +38 -0
- package/src/channel.runtime.ts +16 -0
- package/src/channel.setup.test.ts +269 -0
- package/src/channel.ts +570 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.test.ts +1015 -0
- package/src/cli.ts +1198 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.test.ts +90 -0
- package/src/config-schema.ts +114 -0
- package/src/directory-live.test.ts +200 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.test.ts +440 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.test.ts +68 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.test.ts +483 -0
- package/src/exec-approvals.ts +290 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.test.ts +81 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.test.ts +234 -0
- package/src/legacy-crypto.ts +549 -0
- package/src/legacy-state.test.ts +86 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +150 -0
- package/src/matrix/accounts.readiness.test.ts +27 -0
- package/src/matrix/accounts.test.ts +757 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.test.ts +215 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.test.ts +114 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.test.ts +15 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.test.ts +289 -0
- package/src/matrix/actions/messages.ts +123 -0
- package/src/matrix/actions/pins.test.ts +74 -0
- package/src/matrix/actions/pins.ts +64 -0
- package/src/matrix/actions/polls.test.ts +71 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.test.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.test.ts +135 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.test.ts +79 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.test.ts +87 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +82 -0
- package/src/matrix/actions/verification.test.ts +105 -0
- package/src/matrix/actions/verification.ts +237 -0
- package/src/matrix/actions.ts +37 -0
- package/src/matrix/active-client.ts +26 -0
- package/src/matrix/async-lock.ts +18 -0
- package/src/matrix/backup-health.ts +115 -0
- package/src/matrix/client/config-runtime-api.ts +14 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +982 -0
- package/src/matrix/client/create-client.test.ts +115 -0
- package/src/matrix/client/create-client.ts +101 -0
- package/src/matrix/client/env-auth.ts +6 -0
- package/src/matrix/client/file-sync-store.test.ts +265 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +123 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +56 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.test.ts +344 -0
- package/src/matrix/client/shared.ts +306 -0
- package/src/matrix/client/storage.test.ts +634 -0
- package/src/matrix/client/storage.ts +544 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client-bootstrap.test.ts +84 -0
- package/src/matrix/client-bootstrap.ts +164 -0
- package/src/matrix/client-resolver.test-helpers.ts +147 -0
- package/src/matrix/client.test.ts +1521 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.test.ts +237 -0
- package/src/matrix/config-update.ts +291 -0
- package/src/matrix/credentials-read.ts +206 -0
- package/src/matrix/credentials-write.runtime.ts +26 -0
- package/src/matrix/credentials.test.ts +501 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +225 -0
- package/src/matrix/device-health.test.ts +45 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.test.ts +350 -0
- package/src/matrix/direct-management.ts +347 -0
- package/src/matrix/direct-room.test.ts +61 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.test.ts +406 -0
- package/src/matrix/draft-stream.ts +216 -0
- package/src/matrix/encryption-guidance.ts +27 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.test.ts +340 -0
- package/src/matrix/format.ts +428 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +169 -0
- package/src/matrix/monitor/access-state.test.ts +45 -0
- package/src/matrix/monitor/access-state.ts +77 -0
- package/src/matrix/monitor/ack-config.test.ts +57 -0
- package/src/matrix/monitor/ack-config.ts +26 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.test.ts +203 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.test.ts +197 -0
- package/src/matrix/monitor/config.ts +303 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.test.ts +529 -0
- package/src/matrix/monitor/direct.ts +270 -0
- package/src/matrix/monitor/events.test.ts +1524 -0
- package/src/matrix/monitor/events.ts +213 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
- package/src/matrix/monitor/handler.group-history.test.ts +648 -0
- package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
- package/src/matrix/monitor/handler.test-helpers.ts +308 -0
- package/src/matrix/monitor/handler.test.ts +2952 -0
- package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
- package/src/matrix/monitor/handler.ts +1679 -0
- package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.test.ts +920 -0
- package/src/matrix/monitor/index.ts +434 -0
- package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.test.ts +159 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.test.ts +289 -0
- package/src/matrix/monitor/mentions.ts +177 -0
- package/src/matrix/monitor/reaction-events.test.ts +326 -0
- package/src/matrix/monitor/reaction-events.ts +187 -0
- package/src/matrix/monitor/recent-invite.test.ts +92 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.test.ts +265 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.test.ts +276 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.test.ts +258 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.test.ts +201 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.test.ts +121 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.test.ts +255 -0
- package/src/matrix/monitor/route.ts +178 -0
- package/src/matrix/monitor/runtime-api.ts +31 -0
- package/src/matrix/monitor/startup-verification.test.ts +294 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.test.ts +257 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +111 -0
- package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/thread-context.test.ts +149 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.test.ts +68 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +627 -0
- package/src/matrix/monitor/verification-utils.test.ts +47 -0
- package/src/matrix/monitor/verification-utils.ts +46 -0
- package/src/matrix/outbound-media-runtime.ts +1 -0
- package/src/matrix/poll-summary.ts +110 -0
- package/src/matrix/poll-types.test.ts +205 -0
- package/src/matrix/poll-types.ts +433 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.test.ts +154 -0
- package/src/matrix/probe.ts +96 -0
- package/src/matrix/profile.test.ts +154 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.test.ts +96 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
- package/src/matrix/sdk/crypto-facade.test.ts +197 -0
- package/src/matrix/sdk/crypto-facade.ts +207 -0
- package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
- package/src/matrix/sdk/crypto-runtime.ts +11 -0
- package/src/matrix/sdk/decrypt-bridge.ts +356 -0
- package/src/matrix/sdk/event-helpers.test.ts +60 -0
- package/src/matrix/sdk/event-helpers.ts +71 -0
- package/src/matrix/sdk/http-client.test.ts +134 -0
- package/src/matrix/sdk/http-client.ts +87 -0
- package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
- package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
- package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
- package/src/matrix/sdk/idb-persistence.test.ts +149 -0
- package/src/matrix/sdk/idb-persistence.ts +283 -0
- package/src/matrix/sdk/logger.test.ts +25 -0
- package/src/matrix/sdk/logger.ts +108 -0
- package/src/matrix/sdk/read-response-with-limit.ts +19 -0
- package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
- package/src/matrix/sdk/recovery-key-store.ts +430 -0
- package/src/matrix/sdk/transport.test.ts +161 -0
- package/src/matrix/sdk/transport.ts +344 -0
- package/src/matrix/sdk/types.ts +236 -0
- package/src/matrix/sdk/verification-manager.test.ts +509 -0
- package/src/matrix/sdk/verification-manager.ts +694 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.test.ts +2568 -0
- package/src/matrix/sdk.ts +1789 -0
- package/src/matrix/send/client.test.ts +174 -0
- package/src/matrix/send/client.ts +90 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.test.ts +254 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +134 -0
- package/src/matrix/send.test.ts +958 -0
- package/src/matrix/send.ts +609 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +102 -0
- package/src/matrix/thread-bindings-shared.ts +201 -0
- package/src/matrix/thread-bindings.test.ts +673 -0
- package/src/matrix/thread-bindings.ts +577 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.test.ts +228 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +117 -0
- package/src/migration-snapshot.test.ts +184 -0
- package/src/migration-snapshot.ts +55 -0
- package/src/onboarding.resolve.test.ts +55 -0
- package/src/onboarding.test-harness.ts +158 -0
- package/src/onboarding.test.ts +665 -0
- package/src/onboarding.ts +773 -0
- package/src/outbound.test.ts +173 -0
- package/src/outbound.ts +78 -0
- package/src/plugin-entry.runtime.js +159 -0
- package/src/plugin-entry.runtime.test.ts +108 -0
- package/src/plugin-entry.runtime.ts +68 -0
- package/src/profile-update.ts +68 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.test.ts +178 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +144 -0
- package/src/runtime.ts +7 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.test.ts +315 -0
- package/src/session-route.ts +113 -0
- package/src/setup-bootstrap.ts +94 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +89 -0
- package/src/setup-core.test.ts +326 -0
- package/src/setup-core.ts +50 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.test.ts +227 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/test-helpers.ts +42 -0
- package/src/test-mocks.ts +55 -0
- package/src/test-runtime.ts +72 -0
- package/src/test-support/monitor-route-test-support.ts +8 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.test.ts +422 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +230 -0
- package/test-api.ts +2 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1524 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { CoreConfig } from "../../types.js";
|
|
3
|
+
import type { MatrixAuth } from "../client.js";
|
|
4
|
+
import type { MatrixClient } from "../sdk.js";
|
|
5
|
+
import type { MatrixVerificationSummary } from "../sdk/verification-manager.js";
|
|
6
|
+
import { registerMatrixMonitorEvents } from "./events.js";
|
|
7
|
+
import type { MatrixRawEvent } from "./types.js";
|
|
8
|
+
import { EventType } from "./types.js";
|
|
9
|
+
|
|
10
|
+
type RoomEventListener = (roomId: string, event: MatrixRawEvent) => void;
|
|
11
|
+
type FailedDecryptListener = (roomId: string, event: MatrixRawEvent, error: Error) => Promise<void>;
|
|
12
|
+
type VerificationSummaryListener = (summary: MatrixVerificationSummary) => void;
|
|
13
|
+
|
|
14
|
+
function getSentNoticeBody(sendMessage: ReturnType<typeof vi.fn>, index = 0): string {
|
|
15
|
+
const calls = sendMessage.mock.calls as unknown[][];
|
|
16
|
+
return getSentNoticeBodyFromCall(calls[index] ?? []);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getSentNoticeBodyFromCall(call: unknown[]): string {
|
|
20
|
+
const payload = (call[1] ?? {}) as { body?: string };
|
|
21
|
+
return payload.body ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getSentNoticeBodies(sendMessage: ReturnType<typeof vi.fn>): string[] {
|
|
25
|
+
return (sendMessage.mock.calls as unknown[][]).map(getSentNoticeBodyFromCall);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createHarness(params?: {
|
|
29
|
+
cfg?: CoreConfig;
|
|
30
|
+
accountId?: string;
|
|
31
|
+
authEncryption?: boolean;
|
|
32
|
+
cryptoAvailable?: boolean;
|
|
33
|
+
selfUserId?: string;
|
|
34
|
+
selfUserIdError?: Error;
|
|
35
|
+
allowFrom?: string[];
|
|
36
|
+
dmEnabled?: boolean;
|
|
37
|
+
dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
|
|
38
|
+
storeAllowFrom?: string[];
|
|
39
|
+
accountDataByType?: Record<string, unknown>;
|
|
40
|
+
joinedMembersByRoom?: Record<string, string[]>;
|
|
41
|
+
getJoinedRoomsError?: Error;
|
|
42
|
+
memberStateByRoomUser?: Record<string, Record<string, { is_direct?: boolean }>>;
|
|
43
|
+
verifications?: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
transactionId?: string;
|
|
46
|
+
roomId?: string;
|
|
47
|
+
otherUserId: string;
|
|
48
|
+
updatedAt?: string;
|
|
49
|
+
completed?: boolean;
|
|
50
|
+
pending?: boolean;
|
|
51
|
+
phase?: number;
|
|
52
|
+
phaseName?: string;
|
|
53
|
+
sas?: {
|
|
54
|
+
decimal?: [number, number, number];
|
|
55
|
+
emoji?: Array<[string, string]>;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
ensureVerificationDmTracked?: () => Promise<{
|
|
59
|
+
id: string;
|
|
60
|
+
transactionId?: string;
|
|
61
|
+
roomId?: string;
|
|
62
|
+
otherUserId: string;
|
|
63
|
+
updatedAt?: string;
|
|
64
|
+
completed?: boolean;
|
|
65
|
+
pending?: boolean;
|
|
66
|
+
phase?: number;
|
|
67
|
+
phaseName?: string;
|
|
68
|
+
sas?: {
|
|
69
|
+
decimal?: [number, number, number];
|
|
70
|
+
emoji?: Array<[string, string]>;
|
|
71
|
+
};
|
|
72
|
+
} | null>;
|
|
73
|
+
}) {
|
|
74
|
+
const listeners = new Map<string, (...args: unknown[]) => void>();
|
|
75
|
+
const onRoomMessage = vi.fn(async () => {});
|
|
76
|
+
const listVerifications = vi.fn(async () => params?.verifications ?? []);
|
|
77
|
+
const ensureVerificationDmTracked = vi.fn(
|
|
78
|
+
params?.ensureVerificationDmTracked ?? (async () => null),
|
|
79
|
+
);
|
|
80
|
+
const sendMessage = vi.fn(async (_roomId: string, _payload: { body?: string }) => "$notice");
|
|
81
|
+
const invalidateRoom = vi.fn();
|
|
82
|
+
const rememberInvite = vi.fn();
|
|
83
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
84
|
+
const formatNativeDependencyHint = vi.fn(() => "install hint");
|
|
85
|
+
const logVerboseMessage = vi.fn();
|
|
86
|
+
const readStoreAllowFrom = vi.fn(async () => params?.storeAllowFrom ?? []);
|
|
87
|
+
const client = {
|
|
88
|
+
on: vi.fn((eventName: string, listener: (...args: unknown[]) => void) => {
|
|
89
|
+
listeners.set(eventName, listener);
|
|
90
|
+
return client;
|
|
91
|
+
}),
|
|
92
|
+
sendMessage,
|
|
93
|
+
getUserId: vi.fn(async () => {
|
|
94
|
+
if (params?.selfUserIdError) {
|
|
95
|
+
throw params.selfUserIdError;
|
|
96
|
+
}
|
|
97
|
+
return params?.selfUserId ?? "@bot:example.org";
|
|
98
|
+
}),
|
|
99
|
+
getJoinedRoomMembers: vi.fn(
|
|
100
|
+
async (roomId: string) =>
|
|
101
|
+
params?.joinedMembersByRoom?.[roomId] ?? ["@bot:example.org", "@alice:example.org"],
|
|
102
|
+
),
|
|
103
|
+
getJoinedRooms: vi.fn(async () =>
|
|
104
|
+
params?.getJoinedRoomsError
|
|
105
|
+
? await Promise.reject(params.getJoinedRoomsError)
|
|
106
|
+
: Object.keys(params?.joinedMembersByRoom ?? {}).length > 0
|
|
107
|
+
? Object.keys(params?.joinedMembersByRoom ?? {})
|
|
108
|
+
: ["!room:example.org"],
|
|
109
|
+
),
|
|
110
|
+
getAccountData: vi.fn(
|
|
111
|
+
async (eventType: string) =>
|
|
112
|
+
(params?.accountDataByType?.[eventType] as Record<string, unknown> | undefined) ??
|
|
113
|
+
undefined,
|
|
114
|
+
),
|
|
115
|
+
getRoomStateEvent: vi.fn(
|
|
116
|
+
async (roomId: string, _eventType: string, stateKey: string) =>
|
|
117
|
+
params?.memberStateByRoomUser?.[roomId]?.[stateKey] ?? {},
|
|
118
|
+
),
|
|
119
|
+
...(params?.cryptoAvailable === false
|
|
120
|
+
? {}
|
|
121
|
+
: {
|
|
122
|
+
crypto: {
|
|
123
|
+
listVerifications,
|
|
124
|
+
ensureVerificationDmTracked,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
} as unknown as MatrixClient;
|
|
128
|
+
|
|
129
|
+
registerMatrixMonitorEvents({
|
|
130
|
+
cfg: params?.cfg ?? { channels: { matrix: {} } },
|
|
131
|
+
client,
|
|
132
|
+
auth: {
|
|
133
|
+
accountId: params?.accountId ?? "default",
|
|
134
|
+
encryption: params?.authEncryption ?? true,
|
|
135
|
+
} as MatrixAuth,
|
|
136
|
+
allowFrom: params?.allowFrom ?? [],
|
|
137
|
+
dmEnabled: params?.dmEnabled ?? true,
|
|
138
|
+
dmPolicy: params?.dmPolicy ?? "open",
|
|
139
|
+
readStoreAllowFrom,
|
|
140
|
+
directTracker: {
|
|
141
|
+
invalidateRoom,
|
|
142
|
+
rememberInvite,
|
|
143
|
+
},
|
|
144
|
+
logVerboseMessage,
|
|
145
|
+
warnedEncryptedRooms: new Set<string>(),
|
|
146
|
+
warnedCryptoMissingRooms: new Set<string>(),
|
|
147
|
+
logger,
|
|
148
|
+
formatNativeDependencyHint,
|
|
149
|
+
onRoomMessage,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const roomEventListener = listeners.get("room.event") as RoomEventListener | undefined;
|
|
153
|
+
if (!roomEventListener) {
|
|
154
|
+
throw new Error("room.event listener was not registered");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
onRoomMessage,
|
|
159
|
+
sendMessage,
|
|
160
|
+
invalidateRoom,
|
|
161
|
+
rememberInvite,
|
|
162
|
+
roomEventListener,
|
|
163
|
+
listVerifications,
|
|
164
|
+
readStoreAllowFrom,
|
|
165
|
+
logger,
|
|
166
|
+
formatNativeDependencyHint,
|
|
167
|
+
logVerboseMessage,
|
|
168
|
+
roomMessageListener: listeners.get("room.message") as RoomEventListener | undefined,
|
|
169
|
+
failedDecryptListener: listeners.get("room.failed_decryption") as
|
|
170
|
+
| FailedDecryptListener
|
|
171
|
+
| undefined,
|
|
172
|
+
verificationSummaryListener: listeners.get("verification.summary") as
|
|
173
|
+
| VerificationSummaryListener
|
|
174
|
+
| undefined,
|
|
175
|
+
roomInviteListener: listeners.get("room.invite") as RoomEventListener | undefined,
|
|
176
|
+
roomJoinListener: listeners.get("room.join") as RoomEventListener | undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe("registerMatrixMonitorEvents verification routing", () => {
|
|
181
|
+
it("does not repost historical verification completions during startup catch-up", async () => {
|
|
182
|
+
vi.useFakeTimers();
|
|
183
|
+
vi.setSystemTime(new Date("2026-03-14T13:10:00.000Z"));
|
|
184
|
+
try {
|
|
185
|
+
const { sendMessage, roomEventListener } = createHarness();
|
|
186
|
+
|
|
187
|
+
roomEventListener("!room:example.org", {
|
|
188
|
+
event_id: "$done-old",
|
|
189
|
+
sender: "@alice:example.org",
|
|
190
|
+
type: "m.key.verification.done",
|
|
191
|
+
origin_server_ts: Date.now() - 10 * 60 * 1000,
|
|
192
|
+
content: {
|
|
193
|
+
"m.relates_to": { event_id: "$req-old" },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await vi.runAllTimersAsync();
|
|
198
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
199
|
+
} finally {
|
|
200
|
+
vi.useRealTimers();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("still posts fresh verification completions", async () => {
|
|
205
|
+
const { sendMessage, roomEventListener } = createHarness();
|
|
206
|
+
|
|
207
|
+
roomEventListener("!room:example.org", {
|
|
208
|
+
event_id: "$done-fresh",
|
|
209
|
+
sender: "@alice:example.org",
|
|
210
|
+
type: "m.key.verification.done",
|
|
211
|
+
origin_server_ts: Date.now(),
|
|
212
|
+
content: {
|
|
213
|
+
"m.relates_to": { event_id: "$req-fresh" },
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await vi.dynamicImportSettled();
|
|
218
|
+
await vi.waitFor(() => {
|
|
219
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
expect(getSentNoticeBody(sendMessage)).toContain(
|
|
222
|
+
"Matrix verification completed with @alice:example.org.",
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("forwards reaction room events into the shared room handler", async () => {
|
|
227
|
+
const { onRoomMessage, sendMessage, roomEventListener } = createHarness();
|
|
228
|
+
|
|
229
|
+
roomEventListener("!room:example.org", {
|
|
230
|
+
event_id: "$reaction1",
|
|
231
|
+
sender: "@alice:example.org",
|
|
232
|
+
type: EventType.Reaction,
|
|
233
|
+
origin_server_ts: Date.now(),
|
|
234
|
+
content: {
|
|
235
|
+
"m.relates_to": {
|
|
236
|
+
rel_type: "m.annotation",
|
|
237
|
+
event_id: "$msg1",
|
|
238
|
+
key: "👍",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await vi.waitFor(() => {
|
|
244
|
+
expect(onRoomMessage).toHaveBeenCalledWith(
|
|
245
|
+
"!room:example.org",
|
|
246
|
+
expect.objectContaining({ event_id: "$reaction1", type: EventType.Reaction }),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("invalidates direct-room membership cache on room member events", async () => {
|
|
253
|
+
const { invalidateRoom, roomEventListener } = createHarness();
|
|
254
|
+
|
|
255
|
+
roomEventListener("!room:example.org", {
|
|
256
|
+
event_id: "$member1",
|
|
257
|
+
sender: "@alice:example.org",
|
|
258
|
+
state_key: "@mallory:example.org",
|
|
259
|
+
type: EventType.RoomMember,
|
|
260
|
+
origin_server_ts: Date.now(),
|
|
261
|
+
content: {
|
|
262
|
+
membership: "join",
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("remembers invite provenance on room invites", async () => {
|
|
270
|
+
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
|
|
271
|
+
if (!roomInviteListener) {
|
|
272
|
+
throw new Error("room.invite listener was not registered");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
roomInviteListener("!room:example.org", {
|
|
276
|
+
event_id: "$invite1",
|
|
277
|
+
sender: "@alice:example.org",
|
|
278
|
+
type: EventType.RoomMember,
|
|
279
|
+
origin_server_ts: Date.now(),
|
|
280
|
+
content: {
|
|
281
|
+
membership: "invite",
|
|
282
|
+
is_direct: true,
|
|
283
|
+
},
|
|
284
|
+
state_key: "@bot:example.org",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
|
|
288
|
+
expect(rememberInvite).toHaveBeenCalledWith("!room:example.org", "@alice:example.org");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("ignores lifecycle-only invite events emitted with self sender ids", async () => {
|
|
292
|
+
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
|
|
293
|
+
if (!roomInviteListener) {
|
|
294
|
+
throw new Error("room.invite listener was not registered");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
roomInviteListener("!room:example.org", {
|
|
298
|
+
event_id: "$invite-self",
|
|
299
|
+
sender: "@bot:example.org",
|
|
300
|
+
type: EventType.RoomMember,
|
|
301
|
+
origin_server_ts: Date.now(),
|
|
302
|
+
content: {
|
|
303
|
+
membership: "invite",
|
|
304
|
+
},
|
|
305
|
+
state_key: "@bot:example.org",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
|
|
309
|
+
expect(rememberInvite).not.toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("remembers invite provenance even when Matrix omits the direct invite hint", async () => {
|
|
313
|
+
const { invalidateRoom, rememberInvite, roomInviteListener } = createHarness();
|
|
314
|
+
if (!roomInviteListener) {
|
|
315
|
+
throw new Error("room.invite listener was not registered");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
roomInviteListener("!room:example.org", {
|
|
319
|
+
event_id: "$invite-group",
|
|
320
|
+
sender: "@alice:example.org",
|
|
321
|
+
type: EventType.RoomMember,
|
|
322
|
+
origin_server_ts: Date.now(),
|
|
323
|
+
content: {
|
|
324
|
+
membership: "invite",
|
|
325
|
+
},
|
|
326
|
+
state_key: "@bot:example.org",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
|
|
330
|
+
expect(rememberInvite).toHaveBeenCalledWith("!room:example.org", "@alice:example.org");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("does not synthesize invite provenance from room joins", async () => {
|
|
334
|
+
const { invalidateRoom, rememberInvite, roomJoinListener } = createHarness();
|
|
335
|
+
if (!roomJoinListener) {
|
|
336
|
+
throw new Error("room.join listener was not registered");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
roomJoinListener("!room:example.org", {
|
|
340
|
+
event_id: "$join1",
|
|
341
|
+
sender: "@bot:example.org",
|
|
342
|
+
type: EventType.RoomMember,
|
|
343
|
+
origin_server_ts: Date.now(),
|
|
344
|
+
content: {
|
|
345
|
+
membership: "join",
|
|
346
|
+
},
|
|
347
|
+
state_key: "@bot:example.org",
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(invalidateRoom).toHaveBeenCalledWith("!room:example.org");
|
|
351
|
+
expect(rememberInvite).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("posts verification request notices directly into the room", async () => {
|
|
355
|
+
const { onRoomMessage, sendMessage, roomMessageListener } = createHarness();
|
|
356
|
+
if (!roomMessageListener) {
|
|
357
|
+
throw new Error("room.message listener was not registered");
|
|
358
|
+
}
|
|
359
|
+
roomMessageListener("!room:example.org", {
|
|
360
|
+
event_id: "$req1",
|
|
361
|
+
sender: "@alice:example.org",
|
|
362
|
+
type: EventType.RoomMessage,
|
|
363
|
+
origin_server_ts: Date.now(),
|
|
364
|
+
content: {
|
|
365
|
+
msgtype: "m.key.verification.request",
|
|
366
|
+
body: "verification request",
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
await vi.waitFor(() => {
|
|
371
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
372
|
+
});
|
|
373
|
+
expect(onRoomMessage).not.toHaveBeenCalled();
|
|
374
|
+
const body = getSentNoticeBody(sendMessage, 0);
|
|
375
|
+
expect(body).toContain("Matrix verification request received from @alice:example.org.");
|
|
376
|
+
expect(body).toContain('Open "Verify by emoji"');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("blocks verification request notices when dmPolicy pairing would block the sender", async () => {
|
|
380
|
+
const { onRoomMessage, sendMessage, roomMessageListener, logVerboseMessage } = createHarness({
|
|
381
|
+
dmPolicy: "pairing",
|
|
382
|
+
});
|
|
383
|
+
if (!roomMessageListener) {
|
|
384
|
+
throw new Error("room.message listener was not registered");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
roomMessageListener("!room:example.org", {
|
|
388
|
+
event_id: "$req-pairing-blocked",
|
|
389
|
+
sender: "@alice:example.org",
|
|
390
|
+
type: EventType.RoomMessage,
|
|
391
|
+
origin_server_ts: Date.now(),
|
|
392
|
+
content: {
|
|
393
|
+
msgtype: "m.key.verification.request",
|
|
394
|
+
body: "verification request",
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await vi.waitFor(() => {
|
|
399
|
+
expect(logVerboseMessage).toHaveBeenCalledWith(
|
|
400
|
+
expect.stringContaining("blocked verification sender @alice:example.org"),
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
404
|
+
expect(onRoomMessage).not.toHaveBeenCalled();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("allows verification notices for pairing-authorized DM senders from the allow store", async () => {
|
|
408
|
+
const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({
|
|
409
|
+
dmPolicy: "pairing",
|
|
410
|
+
storeAllowFrom: ["@alice:example.org"],
|
|
411
|
+
});
|
|
412
|
+
if (!roomMessageListener) {
|
|
413
|
+
throw new Error("room.message listener was not registered");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
roomMessageListener("!room:example.org", {
|
|
417
|
+
event_id: "$req-pairing-allowed",
|
|
418
|
+
sender: "@alice:example.org",
|
|
419
|
+
type: EventType.RoomMessage,
|
|
420
|
+
origin_server_ts: Date.now(),
|
|
421
|
+
content: {
|
|
422
|
+
msgtype: "m.key.verification.request",
|
|
423
|
+
body: "verification request",
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await vi.waitFor(() => {
|
|
428
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
429
|
+
});
|
|
430
|
+
expect(readStoreAllowFrom).toHaveBeenCalled();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("does not consult the allow store when dmPolicy is open", async () => {
|
|
434
|
+
const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({
|
|
435
|
+
dmPolicy: "open",
|
|
436
|
+
});
|
|
437
|
+
if (!roomMessageListener) {
|
|
438
|
+
throw new Error("room.message listener was not registered");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
roomMessageListener("!room:example.org", {
|
|
442
|
+
event_id: "$req-open-policy",
|
|
443
|
+
sender: "@alice:example.org",
|
|
444
|
+
type: EventType.RoomMessage,
|
|
445
|
+
origin_server_ts: Date.now(),
|
|
446
|
+
content: {
|
|
447
|
+
msgtype: "m.key.verification.request",
|
|
448
|
+
body: "verification request",
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await vi.waitFor(() => {
|
|
453
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
454
|
+
});
|
|
455
|
+
expect(readStoreAllowFrom).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("blocks verification notices when Matrix DMs are disabled", async () => {
|
|
459
|
+
const { sendMessage, roomMessageListener, logVerboseMessage } = createHarness({
|
|
460
|
+
dmEnabled: false,
|
|
461
|
+
});
|
|
462
|
+
if (!roomMessageListener) {
|
|
463
|
+
throw new Error("room.message listener was not registered");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
roomMessageListener("!room:example.org", {
|
|
467
|
+
event_id: "$req-dm-disabled",
|
|
468
|
+
sender: "@alice:example.org",
|
|
469
|
+
type: EventType.RoomMessage,
|
|
470
|
+
origin_server_ts: Date.now(),
|
|
471
|
+
content: {
|
|
472
|
+
msgtype: "m.key.verification.request",
|
|
473
|
+
body: "verification request",
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await vi.waitFor(() => {
|
|
478
|
+
expect(logVerboseMessage).toHaveBeenCalledWith(
|
|
479
|
+
expect.stringContaining("blocked verification sender @alice:example.org"),
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("posts ready-stage guidance for emoji verification", async () => {
|
|
486
|
+
const { sendMessage, roomEventListener } = createHarness();
|
|
487
|
+
roomEventListener("!room:example.org", {
|
|
488
|
+
event_id: "$ready-1",
|
|
489
|
+
sender: "@alice:example.org",
|
|
490
|
+
type: "m.key.verification.ready",
|
|
491
|
+
origin_server_ts: Date.now(),
|
|
492
|
+
content: {
|
|
493
|
+
"m.relates_to": { event_id: "$req-ready-1" },
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await vi.waitFor(() => {
|
|
498
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
499
|
+
});
|
|
500
|
+
const body = getSentNoticeBody(sendMessage, 0);
|
|
501
|
+
expect(body).toContain("Matrix verification is ready with @alice:example.org.");
|
|
502
|
+
expect(body).toContain('Choose "Verify by emoji"');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("posts SAS emoji/decimal details when verification summaries expose them", async () => {
|
|
506
|
+
const {
|
|
507
|
+
sendMessage,
|
|
508
|
+
roomEventListener,
|
|
509
|
+
listVerifications: _listVerifications,
|
|
510
|
+
} = createHarness({
|
|
511
|
+
joinedMembersByRoom: {
|
|
512
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
513
|
+
},
|
|
514
|
+
verifications: [
|
|
515
|
+
{
|
|
516
|
+
id: "verification-1",
|
|
517
|
+
transactionId: "$different-flow-id",
|
|
518
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
519
|
+
otherUserId: "@alice:example.org",
|
|
520
|
+
sas: {
|
|
521
|
+
decimal: [6158, 1986, 3513],
|
|
522
|
+
emoji: [
|
|
523
|
+
["🎁", "Gift"],
|
|
524
|
+
["🌍", "Globe"],
|
|
525
|
+
["🐴", "Horse"],
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
roomEventListener("!dm:example.org", {
|
|
533
|
+
event_id: "$start2",
|
|
534
|
+
sender: "@alice:example.org",
|
|
535
|
+
type: "m.key.verification.start",
|
|
536
|
+
origin_server_ts: Date.now(),
|
|
537
|
+
content: {
|
|
538
|
+
"m.relates_to": { event_id: "$req2" },
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await vi.waitFor(() => {
|
|
543
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
544
|
+
expect(bodies.some((body) => body.includes("SAS emoji:"))).toBe(true);
|
|
545
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 6158 1986 3513"))).toBe(true);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("rehydrates an in-progress DM verification before resolving SAS notices", async () => {
|
|
550
|
+
const verifications: Array<{
|
|
551
|
+
id: string;
|
|
552
|
+
transactionId?: string;
|
|
553
|
+
roomId?: string;
|
|
554
|
+
otherUserId: string;
|
|
555
|
+
updatedAt?: string;
|
|
556
|
+
completed?: boolean;
|
|
557
|
+
pending?: boolean;
|
|
558
|
+
phase?: number;
|
|
559
|
+
phaseName?: string;
|
|
560
|
+
sas?: {
|
|
561
|
+
decimal?: [number, number, number];
|
|
562
|
+
emoji?: Array<[string, string]>;
|
|
563
|
+
};
|
|
564
|
+
}> = [];
|
|
565
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
566
|
+
joinedMembersByRoom: {
|
|
567
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
568
|
+
},
|
|
569
|
+
verifications,
|
|
570
|
+
ensureVerificationDmTracked: async () => {
|
|
571
|
+
verifications.splice(0, verifications.length, {
|
|
572
|
+
id: "verification-rehydrated",
|
|
573
|
+
transactionId: "$req-hydrated",
|
|
574
|
+
roomId: "!dm:example.org",
|
|
575
|
+
otherUserId: "@alice:example.org",
|
|
576
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
577
|
+
phase: 3,
|
|
578
|
+
phaseName: "started",
|
|
579
|
+
pending: true,
|
|
580
|
+
sas: {
|
|
581
|
+
decimal: [2468, 1357, 9753],
|
|
582
|
+
emoji: [
|
|
583
|
+
["🔔", "Bell"],
|
|
584
|
+
["📁", "Folder"],
|
|
585
|
+
["🐴", "Horse"],
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
return verifications[0] ?? null;
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
roomEventListener("!dm:example.org", {
|
|
594
|
+
event_id: "$start-hydrated",
|
|
595
|
+
sender: "@alice:example.org",
|
|
596
|
+
type: "m.key.verification.start",
|
|
597
|
+
origin_server_ts: Date.now(),
|
|
598
|
+
content: {
|
|
599
|
+
"m.relates_to": { event_id: "$req-hydrated" },
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await vi.waitFor(() => {
|
|
604
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
605
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 2468 1357 9753"))).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("posts SAS notices directly from verification summary updates", async () => {
|
|
610
|
+
const { sendMessage, verificationSummaryListener } = createHarness({
|
|
611
|
+
joinedMembersByRoom: {
|
|
612
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
if (!verificationSummaryListener) {
|
|
616
|
+
throw new Error("verification.summary listener was not registered");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
verificationSummaryListener({
|
|
620
|
+
id: "verification-direct",
|
|
621
|
+
roomId: "!dm:example.org",
|
|
622
|
+
otherUserId: "@alice:example.org",
|
|
623
|
+
isSelfVerification: false,
|
|
624
|
+
initiatedByMe: false,
|
|
625
|
+
phase: 3,
|
|
626
|
+
phaseName: "started",
|
|
627
|
+
pending: true,
|
|
628
|
+
methods: ["m.sas.v1"],
|
|
629
|
+
canAccept: false,
|
|
630
|
+
hasSas: true,
|
|
631
|
+
sas: {
|
|
632
|
+
decimal: [6158, 1986, 3513],
|
|
633
|
+
emoji: [
|
|
634
|
+
["🎁", "Gift"],
|
|
635
|
+
["🌍", "Globe"],
|
|
636
|
+
["🐴", "Horse"],
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
hasReciprocateQr: false,
|
|
640
|
+
completed: false,
|
|
641
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
642
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
await vi.waitFor(() => {
|
|
646
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
647
|
+
});
|
|
648
|
+
const body = getSentNoticeBody(sendMessage, 0);
|
|
649
|
+
expect(body).toContain("Matrix verification SAS with @alice:example.org:");
|
|
650
|
+
expect(body).toContain("SAS decimal: 6158 1986 3513");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("blocks summary SAS notices when dmPolicy allowlist would block the sender", async () => {
|
|
654
|
+
const { sendMessage, verificationSummaryListener, logVerboseMessage } = createHarness({
|
|
655
|
+
dmPolicy: "allowlist",
|
|
656
|
+
joinedMembersByRoom: {
|
|
657
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
if (!verificationSummaryListener) {
|
|
661
|
+
throw new Error("verification.summary listener was not registered");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
verificationSummaryListener({
|
|
665
|
+
id: "verification-blocked-summary",
|
|
666
|
+
roomId: "!dm:example.org",
|
|
667
|
+
otherUserId: "@alice:example.org",
|
|
668
|
+
isSelfVerification: false,
|
|
669
|
+
initiatedByMe: false,
|
|
670
|
+
phase: 3,
|
|
671
|
+
phaseName: "started",
|
|
672
|
+
pending: true,
|
|
673
|
+
methods: ["m.sas.v1"],
|
|
674
|
+
canAccept: false,
|
|
675
|
+
hasSas: true,
|
|
676
|
+
sas: {
|
|
677
|
+
decimal: [6158, 1986, 3513],
|
|
678
|
+
emoji: [
|
|
679
|
+
["🎁", "Gift"],
|
|
680
|
+
["🌍", "Globe"],
|
|
681
|
+
["🐴", "Horse"],
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
hasReciprocateQr: false,
|
|
685
|
+
completed: false,
|
|
686
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
687
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
await vi.waitFor(() => {
|
|
691
|
+
expect(logVerboseMessage).toHaveBeenCalledWith(
|
|
692
|
+
expect.stringContaining("blocked verification sender @alice:example.org"),
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("posts SAS notices from summary updates using the room mapped by earlier flow events", async () => {
|
|
699
|
+
const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness({
|
|
700
|
+
joinedMembersByRoom: {
|
|
701
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
if (!verificationSummaryListener) {
|
|
705
|
+
throw new Error("verification.summary listener was not registered");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
roomEventListener("!dm:example.org", {
|
|
709
|
+
event_id: "$start-mapped",
|
|
710
|
+
sender: "@alice:example.org",
|
|
711
|
+
type: "m.key.verification.start",
|
|
712
|
+
origin_server_ts: Date.now(),
|
|
713
|
+
content: {
|
|
714
|
+
transaction_id: "txn-mapped-room",
|
|
715
|
+
"m.relates_to": { event_id: "$req-mapped" },
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
verificationSummaryListener({
|
|
720
|
+
id: "verification-mapped",
|
|
721
|
+
transactionId: "txn-mapped-room",
|
|
722
|
+
otherUserId: "@alice:example.org",
|
|
723
|
+
isSelfVerification: false,
|
|
724
|
+
initiatedByMe: false,
|
|
725
|
+
phase: 3,
|
|
726
|
+
phaseName: "started",
|
|
727
|
+
pending: true,
|
|
728
|
+
methods: ["m.sas.v1"],
|
|
729
|
+
canAccept: false,
|
|
730
|
+
hasSas: true,
|
|
731
|
+
sas: {
|
|
732
|
+
decimal: [1111, 2222, 3333],
|
|
733
|
+
emoji: [
|
|
734
|
+
["🚀", "Rocket"],
|
|
735
|
+
["🦋", "Butterfly"],
|
|
736
|
+
["📕", "Book"],
|
|
737
|
+
],
|
|
738
|
+
},
|
|
739
|
+
hasReciprocateQr: false,
|
|
740
|
+
completed: false,
|
|
741
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
742
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
await vi.waitFor(() => {
|
|
746
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
747
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 1111 2222 3333"))).toBe(true);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("posts SAS notices from summary updates using the active strict DM when room mapping is missing", async () => {
|
|
752
|
+
const { sendMessage, verificationSummaryListener } = createHarness({
|
|
753
|
+
joinedMembersByRoom: {
|
|
754
|
+
"!dm-active:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
if (!verificationSummaryListener) {
|
|
758
|
+
throw new Error("verification.summary listener was not registered");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
verificationSummaryListener({
|
|
762
|
+
id: "verification-unmapped",
|
|
763
|
+
otherUserId: "@alice:example.org",
|
|
764
|
+
isSelfVerification: false,
|
|
765
|
+
initiatedByMe: false,
|
|
766
|
+
phase: 3,
|
|
767
|
+
phaseName: "started",
|
|
768
|
+
pending: true,
|
|
769
|
+
methods: ["m.sas.v1"],
|
|
770
|
+
canAccept: false,
|
|
771
|
+
hasSas: true,
|
|
772
|
+
sas: {
|
|
773
|
+
decimal: [4321, 8765, 2109],
|
|
774
|
+
emoji: [
|
|
775
|
+
["🚀", "Rocket"],
|
|
776
|
+
["🦋", "Butterfly"],
|
|
777
|
+
["📕", "Book"],
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
hasReciprocateQr: false,
|
|
781
|
+
completed: false,
|
|
782
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
783
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
await vi.waitFor(() => {
|
|
787
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
788
|
+
});
|
|
789
|
+
const roomId = ((sendMessage.mock.calls as unknown[][])[0]?.[0] ?? "") as string;
|
|
790
|
+
const body = getSentNoticeBody(sendMessage, 0);
|
|
791
|
+
expect(roomId).toBe("!dm-active:example.org");
|
|
792
|
+
expect(body).toContain("SAS decimal: 4321 8765 2109");
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it("prefers the canonical active DM over the most recent verification room for unmapped SAS summaries", async () => {
|
|
796
|
+
const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness({
|
|
797
|
+
joinedMembersByRoom: {
|
|
798
|
+
"!dm-active:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
799
|
+
"!dm-current:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
if (!verificationSummaryListener) {
|
|
803
|
+
throw new Error("verification.summary listener was not registered");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
roomEventListener("!dm-current:example.org", {
|
|
807
|
+
event_id: "$start-current",
|
|
808
|
+
sender: "@alice:example.org",
|
|
809
|
+
type: "m.key.verification.start",
|
|
810
|
+
origin_server_ts: Date.now(),
|
|
811
|
+
content: {
|
|
812
|
+
"m.relates_to": { event_id: "$req-current" },
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
await vi.waitFor(() => {
|
|
817
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
818
|
+
expect(bodies.some((body) => body.includes("Matrix verification started with"))).toBe(true);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
verificationSummaryListener({
|
|
822
|
+
id: "verification-current-room",
|
|
823
|
+
otherUserId: "@alice:example.org",
|
|
824
|
+
isSelfVerification: false,
|
|
825
|
+
initiatedByMe: false,
|
|
826
|
+
phase: 3,
|
|
827
|
+
phaseName: "started",
|
|
828
|
+
pending: true,
|
|
829
|
+
methods: ["m.sas.v1"],
|
|
830
|
+
canAccept: false,
|
|
831
|
+
hasSas: true,
|
|
832
|
+
sas: {
|
|
833
|
+
decimal: [2468, 1357, 9753],
|
|
834
|
+
emoji: [
|
|
835
|
+
["🔔", "Bell"],
|
|
836
|
+
["📁", "Folder"],
|
|
837
|
+
["🐴", "Horse"],
|
|
838
|
+
],
|
|
839
|
+
},
|
|
840
|
+
hasReciprocateQr: false,
|
|
841
|
+
completed: false,
|
|
842
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
843
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
await vi.waitFor(() => {
|
|
847
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
848
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 2468 1357 9753"))).toBe(true);
|
|
849
|
+
});
|
|
850
|
+
const calls = sendMessage.mock.calls as unknown[][];
|
|
851
|
+
const sasCall = calls.find((call) =>
|
|
852
|
+
getSentNoticeBodyFromCall(call).includes("SAS decimal: 2468 1357 9753"),
|
|
853
|
+
);
|
|
854
|
+
expect((sasCall?.[0] ?? "") as string).toBe("!dm-active:example.org");
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("retries SAS notice lookup when start arrives before SAS payload is available", async () => {
|
|
858
|
+
vi.useFakeTimers();
|
|
859
|
+
const verifications: Array<{
|
|
860
|
+
id: string;
|
|
861
|
+
transactionId?: string;
|
|
862
|
+
otherUserId: string;
|
|
863
|
+
updatedAt?: string;
|
|
864
|
+
sas?: {
|
|
865
|
+
decimal?: [number, number, number];
|
|
866
|
+
emoji?: Array<[string, string]>;
|
|
867
|
+
};
|
|
868
|
+
}> = [
|
|
869
|
+
{
|
|
870
|
+
id: "verification-race",
|
|
871
|
+
transactionId: "$req-race",
|
|
872
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
873
|
+
otherUserId: "@alice:example.org",
|
|
874
|
+
},
|
|
875
|
+
];
|
|
876
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
877
|
+
joinedMembersByRoom: {
|
|
878
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
879
|
+
},
|
|
880
|
+
verifications,
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
roomEventListener("!dm:example.org", {
|
|
885
|
+
event_id: "$start-race",
|
|
886
|
+
sender: "@alice:example.org",
|
|
887
|
+
type: "m.key.verification.start",
|
|
888
|
+
origin_server_ts: Date.now(),
|
|
889
|
+
content: {
|
|
890
|
+
"m.relates_to": { event_id: "$req-race" },
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
895
|
+
verifications[0] = {
|
|
896
|
+
...verifications[0],
|
|
897
|
+
sas: {
|
|
898
|
+
decimal: [1234, 5678, 9012],
|
|
899
|
+
emoji: [
|
|
900
|
+
["🚀", "Rocket"],
|
|
901
|
+
["🦋", "Butterfly"],
|
|
902
|
+
["📕", "Book"],
|
|
903
|
+
],
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
907
|
+
|
|
908
|
+
await vi.waitFor(() => {
|
|
909
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
910
|
+
expect(bodies.some((body) => body.includes("SAS emoji:"))).toBe(true);
|
|
911
|
+
});
|
|
912
|
+
} finally {
|
|
913
|
+
vi.useRealTimers();
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("ignores verification notices in unrelated non-DM rooms", async () => {
|
|
918
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
919
|
+
joinedMembersByRoom: {
|
|
920
|
+
"!group:example.org": ["@alice:example.org", "@bot:example.org", "@ops:example.org"],
|
|
921
|
+
},
|
|
922
|
+
verifications: [
|
|
923
|
+
{
|
|
924
|
+
id: "verification-2",
|
|
925
|
+
transactionId: "$different-flow-id",
|
|
926
|
+
otherUserId: "@alice:example.org",
|
|
927
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
928
|
+
sas: {
|
|
929
|
+
decimal: [6158, 1986, 3513],
|
|
930
|
+
emoji: [
|
|
931
|
+
["🎁", "Gift"],
|
|
932
|
+
["🌍", "Globe"],
|
|
933
|
+
["🐴", "Horse"],
|
|
934
|
+
],
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
],
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
roomEventListener("!group:example.org", {
|
|
941
|
+
event_id: "$start-group",
|
|
942
|
+
sender: "@alice:example.org",
|
|
943
|
+
type: "m.key.verification.start",
|
|
944
|
+
origin_server_ts: Date.now(),
|
|
945
|
+
content: {
|
|
946
|
+
"m.relates_to": { event_id: "$req-group" },
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
await vi.waitFor(() => {
|
|
951
|
+
expect(sendMessage).toHaveBeenCalledTimes(0);
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it("routes unmapped verification summaries to the room marked direct in member state", async () => {
|
|
956
|
+
const { sendMessage, verificationSummaryListener } = createHarness({
|
|
957
|
+
joinedMembersByRoom: {
|
|
958
|
+
"!fallback:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
959
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
960
|
+
},
|
|
961
|
+
memberStateByRoomUser: {
|
|
962
|
+
"!dm:example.org": {
|
|
963
|
+
"@bot:example.org": { is_direct: true },
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
});
|
|
967
|
+
if (!verificationSummaryListener) {
|
|
968
|
+
throw new Error("verification.summary listener was not registered");
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
verificationSummaryListener({
|
|
972
|
+
id: "verification-explicit-room",
|
|
973
|
+
otherUserId: "@alice:example.org",
|
|
974
|
+
isSelfVerification: false,
|
|
975
|
+
initiatedByMe: false,
|
|
976
|
+
phase: 3,
|
|
977
|
+
phaseName: "started",
|
|
978
|
+
pending: true,
|
|
979
|
+
methods: ["m.sas.v1"],
|
|
980
|
+
canAccept: false,
|
|
981
|
+
hasSas: true,
|
|
982
|
+
sas: {
|
|
983
|
+
decimal: [6158, 1986, 3513],
|
|
984
|
+
emoji: [
|
|
985
|
+
["🎁", "Gift"],
|
|
986
|
+
["🌍", "Globe"],
|
|
987
|
+
["🐴", "Horse"],
|
|
988
|
+
],
|
|
989
|
+
},
|
|
990
|
+
hasReciprocateQr: false,
|
|
991
|
+
completed: false,
|
|
992
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
993
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
await vi.waitFor(() => {
|
|
997
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
998
|
+
});
|
|
999
|
+
expect((sendMessage.mock.calls as unknown[][])[0]?.[0]).toBe("!dm:example.org");
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("prefers the active direct room over a stale remembered strict room for unmapped summaries", async () => {
|
|
1003
|
+
const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness({
|
|
1004
|
+
joinedMembersByRoom: {
|
|
1005
|
+
"!fallback:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1006
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1007
|
+
},
|
|
1008
|
+
memberStateByRoomUser: {
|
|
1009
|
+
"!dm:example.org": {
|
|
1010
|
+
"@bot:example.org": { is_direct: true },
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
if (!verificationSummaryListener) {
|
|
1015
|
+
throw new Error("verification.summary listener was not registered");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
roomEventListener("!fallback:example.org", {
|
|
1019
|
+
event_id: "$start-fallback",
|
|
1020
|
+
sender: "@alice:example.org",
|
|
1021
|
+
type: "m.key.verification.start",
|
|
1022
|
+
origin_server_ts: Date.now(),
|
|
1023
|
+
content: {
|
|
1024
|
+
"m.relates_to": { event_id: "$req-fallback" },
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
await vi.waitFor(() => {
|
|
1029
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
1030
|
+
});
|
|
1031
|
+
sendMessage.mockClear();
|
|
1032
|
+
|
|
1033
|
+
verificationSummaryListener({
|
|
1034
|
+
id: "verification-stale-room",
|
|
1035
|
+
otherUserId: "@alice:example.org",
|
|
1036
|
+
isSelfVerification: false,
|
|
1037
|
+
initiatedByMe: false,
|
|
1038
|
+
phase: 3,
|
|
1039
|
+
phaseName: "started",
|
|
1040
|
+
pending: true,
|
|
1041
|
+
methods: ["m.sas.v1"],
|
|
1042
|
+
canAccept: false,
|
|
1043
|
+
hasSas: true,
|
|
1044
|
+
sas: {
|
|
1045
|
+
decimal: [6158, 1986, 3513],
|
|
1046
|
+
emoji: [
|
|
1047
|
+
["🎁", "Gift"],
|
|
1048
|
+
["🌍", "Globe"],
|
|
1049
|
+
["🐴", "Horse"],
|
|
1050
|
+
],
|
|
1051
|
+
},
|
|
1052
|
+
hasReciprocateQr: false,
|
|
1053
|
+
completed: false,
|
|
1054
|
+
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
1055
|
+
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
await vi.waitFor(() => {
|
|
1059
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
1060
|
+
});
|
|
1061
|
+
expect((sendMessage.mock.calls as unknown[][])[0]?.[0]).toBe("!dm:example.org");
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it("does not emit duplicate SAS notices for the same verification payload", async () => {
|
|
1065
|
+
const { sendMessage, roomEventListener, listVerifications } = createHarness({
|
|
1066
|
+
verifications: [
|
|
1067
|
+
{
|
|
1068
|
+
id: "verification-3",
|
|
1069
|
+
transactionId: "$req3",
|
|
1070
|
+
otherUserId: "@alice:example.org",
|
|
1071
|
+
sas: {
|
|
1072
|
+
decimal: [1111, 2222, 3333],
|
|
1073
|
+
emoji: [
|
|
1074
|
+
["🚀", "Rocket"],
|
|
1075
|
+
["🦋", "Butterfly"],
|
|
1076
|
+
["📕", "Book"],
|
|
1077
|
+
],
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
roomEventListener("!room:example.org", {
|
|
1084
|
+
event_id: "$start3",
|
|
1085
|
+
sender: "@alice:example.org",
|
|
1086
|
+
type: "m.key.verification.start",
|
|
1087
|
+
origin_server_ts: Date.now(),
|
|
1088
|
+
content: {
|
|
1089
|
+
"m.relates_to": { event_id: "$req3" },
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
await vi.waitFor(() => {
|
|
1093
|
+
expect(sendMessage.mock.calls.length).toBeGreaterThan(0);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
roomEventListener("!room:example.org", {
|
|
1097
|
+
event_id: "$key3",
|
|
1098
|
+
sender: "@alice:example.org",
|
|
1099
|
+
type: "m.key.verification.key",
|
|
1100
|
+
origin_server_ts: Date.now(),
|
|
1101
|
+
content: {
|
|
1102
|
+
"m.relates_to": { event_id: "$req3" },
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
await vi.waitFor(() => {
|
|
1106
|
+
expect(listVerifications).toHaveBeenCalledTimes(2);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
const sasBodies = getSentNoticeBodies(sendMessage).filter((body) =>
|
|
1110
|
+
body.includes("SAS emoji:"),
|
|
1111
|
+
);
|
|
1112
|
+
expect(sasBodies).toHaveLength(1);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("ignores cancelled verification flows when DM fallback resolves SAS notices", async () => {
|
|
1116
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
1117
|
+
joinedMembersByRoom: {
|
|
1118
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1119
|
+
},
|
|
1120
|
+
verifications: [
|
|
1121
|
+
{
|
|
1122
|
+
id: "verification-old-cancelled",
|
|
1123
|
+
transactionId: "$old-flow",
|
|
1124
|
+
otherUserId: "@alice:example.org",
|
|
1125
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
1126
|
+
phaseName: "cancelled",
|
|
1127
|
+
phase: 4,
|
|
1128
|
+
pending: false,
|
|
1129
|
+
sas: {
|
|
1130
|
+
decimal: [1111, 2222, 3333],
|
|
1131
|
+
emoji: [
|
|
1132
|
+
["🚀", "Rocket"],
|
|
1133
|
+
["🦋", "Butterfly"],
|
|
1134
|
+
["📕", "Book"],
|
|
1135
|
+
],
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
id: "verification-new-active",
|
|
1140
|
+
transactionId: "$different-flow-id",
|
|
1141
|
+
otherUserId: "@alice:example.org",
|
|
1142
|
+
updatedAt: new Date("2026-02-25T21:43:54.000Z").toISOString(),
|
|
1143
|
+
phaseName: "started",
|
|
1144
|
+
phase: 3,
|
|
1145
|
+
pending: true,
|
|
1146
|
+
sas: {
|
|
1147
|
+
decimal: [6158, 1986, 3513],
|
|
1148
|
+
emoji: [
|
|
1149
|
+
["🎁", "Gift"],
|
|
1150
|
+
["🌍", "Globe"],
|
|
1151
|
+
["🐴", "Horse"],
|
|
1152
|
+
],
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
],
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
roomEventListener("!dm:example.org", {
|
|
1159
|
+
event_id: "$start-active",
|
|
1160
|
+
sender: "@alice:example.org",
|
|
1161
|
+
type: "m.key.verification.start",
|
|
1162
|
+
origin_server_ts: Date.now(),
|
|
1163
|
+
content: {
|
|
1164
|
+
"m.relates_to": { event_id: "$req-active" },
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
await vi.waitFor(() => {
|
|
1169
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
1170
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 6158 1986 3513"))).toBe(true);
|
|
1171
|
+
});
|
|
1172
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
1173
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 1111 2222 3333"))).toBe(false);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it("preserves strict-room SAS fallback when active DM inspection cannot resolve a room", async () => {
|
|
1177
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
1178
|
+
joinedMembersByRoom: {
|
|
1179
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1180
|
+
},
|
|
1181
|
+
getJoinedRoomsError: new Error("temporary joined-room lookup failure"),
|
|
1182
|
+
verifications: [
|
|
1183
|
+
{
|
|
1184
|
+
id: "verification-active",
|
|
1185
|
+
transactionId: "$different-flow-id",
|
|
1186
|
+
otherUserId: "@alice:example.org",
|
|
1187
|
+
updatedAt: new Date("2026-02-25T21:43:54.000Z").toISOString(),
|
|
1188
|
+
phaseName: "started",
|
|
1189
|
+
phase: 3,
|
|
1190
|
+
pending: true,
|
|
1191
|
+
sas: {
|
|
1192
|
+
decimal: [6158, 1986, 3513],
|
|
1193
|
+
emoji: [
|
|
1194
|
+
["🎁", "Gift"],
|
|
1195
|
+
["🌍", "Globe"],
|
|
1196
|
+
["🐴", "Horse"],
|
|
1197
|
+
],
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
roomEventListener("!dm:example.org", {
|
|
1204
|
+
event_id: "$start-active",
|
|
1205
|
+
sender: "@alice:example.org",
|
|
1206
|
+
type: "m.key.verification.start",
|
|
1207
|
+
origin_server_ts: Date.now(),
|
|
1208
|
+
content: {
|
|
1209
|
+
"m.relates_to": { event_id: "$req-active" },
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
await vi.waitFor(() => {
|
|
1214
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
1215
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 6158 1986 3513"))).toBe(true);
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it("prefers the active verification for the current DM when multiple active summaries exist", async () => {
|
|
1220
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
1221
|
+
joinedMembersByRoom: {
|
|
1222
|
+
"!dm-current:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1223
|
+
},
|
|
1224
|
+
verifications: [
|
|
1225
|
+
{
|
|
1226
|
+
id: "verification-other-room",
|
|
1227
|
+
roomId: "!dm-other:example.org",
|
|
1228
|
+
transactionId: "$different-flow-other",
|
|
1229
|
+
otherUserId: "@alice:example.org",
|
|
1230
|
+
updatedAt: new Date("2026-02-25T21:44:54.000Z").toISOString(),
|
|
1231
|
+
phaseName: "started",
|
|
1232
|
+
phase: 3,
|
|
1233
|
+
pending: true,
|
|
1234
|
+
sas: {
|
|
1235
|
+
decimal: [1111, 2222, 3333],
|
|
1236
|
+
emoji: [
|
|
1237
|
+
["🚀", "Rocket"],
|
|
1238
|
+
["🦋", "Butterfly"],
|
|
1239
|
+
["📕", "Book"],
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
id: "verification-current-room",
|
|
1245
|
+
roomId: "!dm-current:example.org",
|
|
1246
|
+
transactionId: "$different-flow-current",
|
|
1247
|
+
otherUserId: "@alice:example.org",
|
|
1248
|
+
updatedAt: new Date("2026-02-25T21:43:54.000Z").toISOString(),
|
|
1249
|
+
phaseName: "started",
|
|
1250
|
+
phase: 3,
|
|
1251
|
+
pending: true,
|
|
1252
|
+
sas: {
|
|
1253
|
+
decimal: [6158, 1986, 3513],
|
|
1254
|
+
emoji: [
|
|
1255
|
+
["🎁", "Gift"],
|
|
1256
|
+
["🌍", "Globe"],
|
|
1257
|
+
["🐴", "Horse"],
|
|
1258
|
+
],
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
],
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
roomEventListener("!dm-current:example.org", {
|
|
1265
|
+
event_id: "$start-room-scoped",
|
|
1266
|
+
sender: "@alice:example.org",
|
|
1267
|
+
type: "m.key.verification.start",
|
|
1268
|
+
origin_server_ts: Date.now(),
|
|
1269
|
+
content: {
|
|
1270
|
+
"m.relates_to": { event_id: "$req-room-scoped" },
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
await vi.waitFor(() => {
|
|
1275
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
1276
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 6158 1986 3513"))).toBe(true);
|
|
1277
|
+
});
|
|
1278
|
+
const bodies = getSentNoticeBodies(sendMessage);
|
|
1279
|
+
expect(bodies.some((body) => body.includes("SAS decimal: 1111 2222 3333"))).toBe(false);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it("does not emit SAS notices for cancelled verification events", async () => {
|
|
1283
|
+
const { sendMessage, roomEventListener } = createHarness({
|
|
1284
|
+
joinedMembersByRoom: {
|
|
1285
|
+
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
|
1286
|
+
},
|
|
1287
|
+
verifications: [
|
|
1288
|
+
{
|
|
1289
|
+
id: "verification-cancelled",
|
|
1290
|
+
transactionId: "$req-cancelled",
|
|
1291
|
+
otherUserId: "@alice:example.org",
|
|
1292
|
+
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
|
1293
|
+
phaseName: "cancelled",
|
|
1294
|
+
phase: 4,
|
|
1295
|
+
pending: false,
|
|
1296
|
+
sas: {
|
|
1297
|
+
decimal: [1111, 2222, 3333],
|
|
1298
|
+
emoji: [
|
|
1299
|
+
["🚀", "Rocket"],
|
|
1300
|
+
["🦋", "Butterfly"],
|
|
1301
|
+
["📕", "Book"],
|
|
1302
|
+
],
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
roomEventListener("!dm:example.org", {
|
|
1309
|
+
event_id: "$cancelled-1",
|
|
1310
|
+
sender: "@alice:example.org",
|
|
1311
|
+
type: "m.key.verification.cancel",
|
|
1312
|
+
origin_server_ts: Date.now(),
|
|
1313
|
+
content: {
|
|
1314
|
+
code: "m.mismatched_sas",
|
|
1315
|
+
reason: "The SAS did not match.",
|
|
1316
|
+
"m.relates_to": { event_id: "$req-cancelled" },
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
await vi.waitFor(() => {
|
|
1321
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
1322
|
+
});
|
|
1323
|
+
const body = getSentNoticeBody(sendMessage, 0);
|
|
1324
|
+
expect(body).toContain("Matrix verification cancelled by @alice:example.org");
|
|
1325
|
+
expect(body).not.toContain("SAS decimal:");
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it("warns once when encrypted events arrive without Matrix encryption enabled", () => {
|
|
1329
|
+
const { logger, roomEventListener } = createHarness({
|
|
1330
|
+
authEncryption: false,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
roomEventListener("!room:example.org", {
|
|
1334
|
+
event_id: "$enc1",
|
|
1335
|
+
sender: "@alice:example.org",
|
|
1336
|
+
type: EventType.RoomMessageEncrypted,
|
|
1337
|
+
origin_server_ts: Date.now(),
|
|
1338
|
+
content: {},
|
|
1339
|
+
});
|
|
1340
|
+
roomEventListener("!room:example.org", {
|
|
1341
|
+
event_id: "$enc2",
|
|
1342
|
+
sender: "@alice:example.org",
|
|
1343
|
+
type: EventType.RoomMessageEncrypted,
|
|
1344
|
+
origin_server_ts: Date.now(),
|
|
1345
|
+
content: {},
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
1349
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1350
|
+
"matrix: encrypted event received without encryption enabled; set channels.lobi.encryption=true and verify the device to decrypt",
|
|
1351
|
+
{ roomId: "!room:example.org" },
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
it("uses the active Matrix account path in encrypted-event warnings", () => {
|
|
1356
|
+
const { logger, roomEventListener } = createHarness({
|
|
1357
|
+
accountId: "ops",
|
|
1358
|
+
authEncryption: false,
|
|
1359
|
+
cfg: {
|
|
1360
|
+
channels: {
|
|
1361
|
+
matrix: {
|
|
1362
|
+
accounts: {
|
|
1363
|
+
ops: {},
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
roomEventListener("!room:example.org", {
|
|
1371
|
+
event_id: "$enc1",
|
|
1372
|
+
sender: "@alice:example.org",
|
|
1373
|
+
type: EventType.RoomMessageEncrypted,
|
|
1374
|
+
origin_server_ts: Date.now(),
|
|
1375
|
+
content: {},
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1379
|
+
"matrix: encrypted event received without encryption enabled; set channels.lobi.accounts.ops.encryption=true and verify the device to decrypt",
|
|
1380
|
+
{ roomId: "!room:example.org" },
|
|
1381
|
+
);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
it("warns once when crypto bindings are unavailable for encrypted rooms", () => {
|
|
1385
|
+
const { formatNativeDependencyHint, logger, roomEventListener } = createHarness({
|
|
1386
|
+
authEncryption: true,
|
|
1387
|
+
cryptoAvailable: false,
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
roomEventListener("!room:example.org", {
|
|
1391
|
+
event_id: "$enc1",
|
|
1392
|
+
sender: "@alice:example.org",
|
|
1393
|
+
type: EventType.RoomMessageEncrypted,
|
|
1394
|
+
origin_server_ts: Date.now(),
|
|
1395
|
+
content: {},
|
|
1396
|
+
});
|
|
1397
|
+
roomEventListener("!room:example.org", {
|
|
1398
|
+
event_id: "$enc2",
|
|
1399
|
+
sender: "@alice:example.org",
|
|
1400
|
+
type: EventType.RoomMessageEncrypted,
|
|
1401
|
+
origin_server_ts: Date.now(),
|
|
1402
|
+
content: {},
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
expect(formatNativeDependencyHint).toHaveBeenCalledTimes(1);
|
|
1406
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
1407
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1408
|
+
"matrix: encryption enabled but crypto is unavailable; install hint",
|
|
1409
|
+
{ roomId: "!room:example.org" },
|
|
1410
|
+
);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("adds self-device guidance when decrypt failures come from the same Matrix user", async () => {
|
|
1414
|
+
const { logger, failedDecryptListener } = createHarness({
|
|
1415
|
+
accountId: "ops",
|
|
1416
|
+
selfUserId: "@gumadeiras:matrix.example.org",
|
|
1417
|
+
});
|
|
1418
|
+
if (!failedDecryptListener) {
|
|
1419
|
+
throw new Error("room.failed_decryption listener was not registered");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
await failedDecryptListener(
|
|
1423
|
+
"!room:example.org",
|
|
1424
|
+
{
|
|
1425
|
+
event_id: "$enc-self",
|
|
1426
|
+
sender: "@gumadeiras:matrix.example.org",
|
|
1427
|
+
type: EventType.RoomMessageEncrypted,
|
|
1428
|
+
origin_server_ts: Date.now(),
|
|
1429
|
+
content: {},
|
|
1430
|
+
},
|
|
1431
|
+
new Error("The sender's device has not sent us the keys for this message."),
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
expect(logger.warn).toHaveBeenNthCalledWith(
|
|
1435
|
+
1,
|
|
1436
|
+
"Failed to decrypt message",
|
|
1437
|
+
expect.objectContaining({
|
|
1438
|
+
roomId: "!room:example.org",
|
|
1439
|
+
eventId: "$enc-self",
|
|
1440
|
+
sender: "@gumadeiras:matrix.example.org",
|
|
1441
|
+
senderMatchesOwnUser: true,
|
|
1442
|
+
}),
|
|
1443
|
+
);
|
|
1444
|
+
expect(logger.warn).toHaveBeenNthCalledWith(
|
|
1445
|
+
2,
|
|
1446
|
+
"matrix: failed to decrypt a message from this same Matrix user. This usually means another Matrix device did not share the room key, or another OpenClaw runtime is using the same account. Check 'openclaw matrix verify status --verbose --account ops' and 'openclaw matrix devices list --account ops'.",
|
|
1447
|
+
{
|
|
1448
|
+
roomId: "!room:example.org",
|
|
1449
|
+
eventId: "$enc-self",
|
|
1450
|
+
sender: "@gumadeiras:matrix.example.org",
|
|
1451
|
+
},
|
|
1452
|
+
);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it("does not add self-device guidance for decrypt failures from another sender", async () => {
|
|
1456
|
+
const { logger, failedDecryptListener } = createHarness({
|
|
1457
|
+
accountId: "ops",
|
|
1458
|
+
selfUserId: "@gumadeiras:matrix.example.org",
|
|
1459
|
+
});
|
|
1460
|
+
if (!failedDecryptListener) {
|
|
1461
|
+
throw new Error("room.failed_decryption listener was not registered");
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
await failedDecryptListener(
|
|
1465
|
+
"!room:example.org",
|
|
1466
|
+
{
|
|
1467
|
+
event_id: "$enc-other",
|
|
1468
|
+
sender: "@alice:matrix.example.org",
|
|
1469
|
+
type: EventType.RoomMessageEncrypted,
|
|
1470
|
+
origin_server_ts: Date.now(),
|
|
1471
|
+
content: {},
|
|
1472
|
+
},
|
|
1473
|
+
new Error("The sender's device has not sent us the keys for this message."),
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
1477
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1478
|
+
"Failed to decrypt message",
|
|
1479
|
+
expect.objectContaining({
|
|
1480
|
+
roomId: "!room:example.org",
|
|
1481
|
+
eventId: "$enc-other",
|
|
1482
|
+
sender: "@alice:matrix.example.org",
|
|
1483
|
+
senderMatchesOwnUser: false,
|
|
1484
|
+
}),
|
|
1485
|
+
);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
it("does not throw when getUserId fails during decrypt guidance lookup", async () => {
|
|
1489
|
+
const { logger, logVerboseMessage, failedDecryptListener } = createHarness({
|
|
1490
|
+
accountId: "ops",
|
|
1491
|
+
selfUserIdError: new Error("lookup failed"),
|
|
1492
|
+
});
|
|
1493
|
+
if (!failedDecryptListener) {
|
|
1494
|
+
throw new Error("room.failed_decryption listener was not registered");
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
await expect(
|
|
1498
|
+
failedDecryptListener(
|
|
1499
|
+
"!room:example.org",
|
|
1500
|
+
{
|
|
1501
|
+
event_id: "$enc-lookup-fail",
|
|
1502
|
+
sender: "@gumadeiras:matrix.example.org",
|
|
1503
|
+
type: EventType.RoomMessageEncrypted,
|
|
1504
|
+
origin_server_ts: Date.now(),
|
|
1505
|
+
content: {},
|
|
1506
|
+
},
|
|
1507
|
+
new Error("The sender's device has not sent us the keys for this message."),
|
|
1508
|
+
),
|
|
1509
|
+
).resolves.toBeUndefined();
|
|
1510
|
+
|
|
1511
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
1512
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
1513
|
+
"Failed to decrypt message",
|
|
1514
|
+
expect.objectContaining({
|
|
1515
|
+
roomId: "!room:example.org",
|
|
1516
|
+
eventId: "$enc-lookup-fail",
|
|
1517
|
+
senderMatchesOwnUser: false,
|
|
1518
|
+
}),
|
|
1519
|
+
);
|
|
1520
|
+
expect(logVerboseMessage).toHaveBeenCalledWith(
|
|
1521
|
+
"matrix: failed resolving self user id for decrypt warning: Error: lookup failed",
|
|
1522
|
+
);
|
|
1523
|
+
});
|
|
1524
|
+
});
|