@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,71 @@
|
|
|
1
|
+
import type { MatrixEvent } from "@archipelagolab/lobi-js-sdk";
|
|
2
|
+
import type { MatrixRawEvent } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function matrixEventToRaw(event: MatrixEvent): MatrixRawEvent {
|
|
5
|
+
const unsigned = (event.getUnsigned?.() ?? {}) as {
|
|
6
|
+
age?: number;
|
|
7
|
+
redacted_because?: unknown;
|
|
8
|
+
};
|
|
9
|
+
const raw: MatrixRawEvent = {
|
|
10
|
+
event_id: event.getId() ?? "",
|
|
11
|
+
sender: event.getSender() ?? "",
|
|
12
|
+
type: event.getType() ?? "",
|
|
13
|
+
origin_server_ts: event.getTs() ?? 0,
|
|
14
|
+
content: (event.getContent?.() ?? {}) || {},
|
|
15
|
+
unsigned,
|
|
16
|
+
};
|
|
17
|
+
const stateKey = resolveMatrixStateKey(event);
|
|
18
|
+
if (typeof stateKey === "string") {
|
|
19
|
+
raw.state_key = stateKey;
|
|
20
|
+
}
|
|
21
|
+
return raw;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseMxc(url: string): { server: string; mediaId: string } | null {
|
|
25
|
+
const match = /^mxc:\/\/([^/]+)\/(.+)$/.exec(url.trim());
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
server: match[1],
|
|
31
|
+
mediaId: match[2],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildHttpError(
|
|
36
|
+
statusCode: number,
|
|
37
|
+
bodyText: string,
|
|
38
|
+
): Error & { statusCode: number } {
|
|
39
|
+
let message = `Matrix HTTP ${statusCode}`;
|
|
40
|
+
if (bodyText.trim()) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(bodyText) as { error?: string };
|
|
43
|
+
if (typeof parsed.error === "string" && parsed.error.trim()) {
|
|
44
|
+
message = parsed.error.trim();
|
|
45
|
+
} else {
|
|
46
|
+
message = bodyText.slice(0, 500);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
message = bodyText.slice(0, 500);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Object.assign(new Error(message), { statusCode });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveMatrixStateKey(event: MatrixEvent): string | undefined {
|
|
56
|
+
const direct = event.getStateKey?.();
|
|
57
|
+
if (typeof direct === "string") {
|
|
58
|
+
return direct;
|
|
59
|
+
}
|
|
60
|
+
const wireContent = (
|
|
61
|
+
event as { getWireContent?: () => { state_key?: unknown } }
|
|
62
|
+
).getWireContent?.();
|
|
63
|
+
if (wireContent && typeof wireContent.state_key === "string") {
|
|
64
|
+
return wireContent.state_key;
|
|
65
|
+
}
|
|
66
|
+
const rawEvent = (event as { event?: { state_key?: unknown } }).event;
|
|
67
|
+
if (rawEvent && typeof rawEvent.state_key === "string") {
|
|
68
|
+
return rawEvent.state_key;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const { performMatrixRequestMock } = vi.hoisted(() => ({
|
|
4
|
+
performMatrixRequestMock: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("./transport.js", () => ({
|
|
8
|
+
performMatrixRequest: performMatrixRequestMock,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
let MatrixAuthedHttpClient: typeof import("./http-client.js").MatrixAuthedHttpClient;
|
|
12
|
+
|
|
13
|
+
describe("MatrixAuthedHttpClient", () => {
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
({ MatrixAuthedHttpClient } = await import("./http-client.js"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
performMatrixRequestMock.mockReset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses JSON responses and forwards absolute-endpoint opt-in", async () => {
|
|
23
|
+
performMatrixRequestMock.mockResolvedValue({
|
|
24
|
+
response: new Response('{"ok":true}', {
|
|
25
|
+
status: 200,
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
}),
|
|
28
|
+
text: '{"ok":true}',
|
|
29
|
+
buffer: Buffer.from('{"ok":true}', "utf8"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const client = new MatrixAuthedHttpClient({
|
|
33
|
+
homeserver: "https://matrix.example.org",
|
|
34
|
+
accessToken: "token",
|
|
35
|
+
ssrfPolicy: {
|
|
36
|
+
allowPrivateNetwork: true,
|
|
37
|
+
},
|
|
38
|
+
dispatcherPolicy: {
|
|
39
|
+
mode: "explicit-proxy",
|
|
40
|
+
proxyUrl: "http://proxy.internal:8080",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const result = await client.requestJson({
|
|
44
|
+
method: "GET",
|
|
45
|
+
endpoint: "https://matrix.example.org/_lobi/client/v3/account/whoami",
|
|
46
|
+
timeoutMs: 5000,
|
|
47
|
+
allowAbsoluteEndpoint: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result).toEqual({ ok: true });
|
|
51
|
+
expect(performMatrixRequestMock).toHaveBeenCalledWith(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
method: "GET",
|
|
54
|
+
endpoint: "https://matrix.example.org/_lobi/client/v3/account/whoami",
|
|
55
|
+
allowAbsoluteEndpoint: true,
|
|
56
|
+
ssrfPolicy: { allowPrivateNetwork: true },
|
|
57
|
+
dispatcherPolicy: {
|
|
58
|
+
mode: "explicit-proxy",
|
|
59
|
+
proxyUrl: "http://proxy.internal:8080",
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns plain text when response is not JSON", async () => {
|
|
66
|
+
performMatrixRequestMock.mockResolvedValue({
|
|
67
|
+
response: new Response("pong", {
|
|
68
|
+
status: 200,
|
|
69
|
+
headers: { "content-type": "text/plain" },
|
|
70
|
+
}),
|
|
71
|
+
text: "pong",
|
|
72
|
+
buffer: Buffer.from("pong", "utf8"),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const client = new MatrixAuthedHttpClient({
|
|
76
|
+
homeserver: "https://matrix.example.org",
|
|
77
|
+
accessToken: "token",
|
|
78
|
+
});
|
|
79
|
+
const result = await client.requestJson({
|
|
80
|
+
method: "GET",
|
|
81
|
+
endpoint: "/_lobi/client/v3/ping",
|
|
82
|
+
timeoutMs: 5000,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result).toBe("pong");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns raw buffers for media requests", async () => {
|
|
89
|
+
const payload = Buffer.from([1, 2, 3, 4]);
|
|
90
|
+
performMatrixRequestMock.mockResolvedValue({
|
|
91
|
+
response: new Response(payload, { status: 200 }),
|
|
92
|
+
text: payload.toString("utf8"),
|
|
93
|
+
buffer: payload,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const client = new MatrixAuthedHttpClient({
|
|
97
|
+
homeserver: "https://matrix.example.org",
|
|
98
|
+
accessToken: "token",
|
|
99
|
+
});
|
|
100
|
+
const result = await client.requestRaw({
|
|
101
|
+
method: "GET",
|
|
102
|
+
endpoint: "/_lobi/media/v3/download/example/id",
|
|
103
|
+
timeoutMs: 5000,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual(payload);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("raises HTTP errors with status code metadata", async () => {
|
|
110
|
+
performMatrixRequestMock.mockResolvedValue({
|
|
111
|
+
response: new Response(JSON.stringify({ error: "forbidden" }), {
|
|
112
|
+
status: 403,
|
|
113
|
+
headers: { "content-type": "application/json" },
|
|
114
|
+
}),
|
|
115
|
+
text: JSON.stringify({ error: "forbidden" }),
|
|
116
|
+
buffer: Buffer.from(JSON.stringify({ error: "forbidden" }), "utf8"),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const client = new MatrixAuthedHttpClient({
|
|
120
|
+
homeserver: "https://matrix.example.org",
|
|
121
|
+
accessToken: "token",
|
|
122
|
+
});
|
|
123
|
+
await expect(
|
|
124
|
+
client.requestJson({
|
|
125
|
+
method: "GET",
|
|
126
|
+
endpoint: "/_lobi/client/v3/rooms",
|
|
127
|
+
timeoutMs: 5000,
|
|
128
|
+
}),
|
|
129
|
+
).rejects.toMatchObject({
|
|
130
|
+
message: "forbidden",
|
|
131
|
+
statusCode: 403,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { PinnedDispatcherPolicy } from "openclaw/plugin-sdk/infra-runtime";
|
|
2
|
+
import type { SsrFPolicy } from "../../runtime-api.js";
|
|
3
|
+
import { buildHttpError } from "./event-helpers.js";
|
|
4
|
+
import { type HttpMethod, type QueryParams, performMatrixRequest } from "./transport.js";
|
|
5
|
+
|
|
6
|
+
type MatrixAuthedHttpClientParams = {
|
|
7
|
+
homeserver: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
ssrfPolicy?: SsrFPolicy;
|
|
10
|
+
dispatcherPolicy?: PinnedDispatcherPolicy;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class MatrixAuthedHttpClient {
|
|
14
|
+
private readonly homeserver: string;
|
|
15
|
+
private readonly accessToken: string;
|
|
16
|
+
private readonly ssrfPolicy?: SsrFPolicy;
|
|
17
|
+
private readonly dispatcherPolicy?: PinnedDispatcherPolicy;
|
|
18
|
+
|
|
19
|
+
constructor(params: MatrixAuthedHttpClientParams) {
|
|
20
|
+
this.homeserver = params.homeserver;
|
|
21
|
+
this.accessToken = params.accessToken;
|
|
22
|
+
this.ssrfPolicy = params.ssrfPolicy;
|
|
23
|
+
this.dispatcherPolicy = params.dispatcherPolicy;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async requestJson(params: {
|
|
27
|
+
method: HttpMethod;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
qs?: QueryParams;
|
|
30
|
+
body?: unknown;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
allowAbsoluteEndpoint?: boolean;
|
|
33
|
+
}): Promise<unknown> {
|
|
34
|
+
const { response, text } = await performMatrixRequest({
|
|
35
|
+
homeserver: this.homeserver,
|
|
36
|
+
accessToken: this.accessToken,
|
|
37
|
+
method: params.method,
|
|
38
|
+
endpoint: params.endpoint,
|
|
39
|
+
qs: params.qs,
|
|
40
|
+
body: params.body,
|
|
41
|
+
timeoutMs: params.timeoutMs,
|
|
42
|
+
ssrfPolicy: this.ssrfPolicy,
|
|
43
|
+
dispatcherPolicy: this.dispatcherPolicy,
|
|
44
|
+
allowAbsoluteEndpoint: params.allowAbsoluteEndpoint,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw buildHttpError(response.status, text);
|
|
48
|
+
}
|
|
49
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
50
|
+
if (contentType.includes("application/json")) {
|
|
51
|
+
if (!text.trim()) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async requestRaw(params: {
|
|
60
|
+
method: HttpMethod;
|
|
61
|
+
endpoint: string;
|
|
62
|
+
qs?: QueryParams;
|
|
63
|
+
timeoutMs: number;
|
|
64
|
+
maxBytes?: number;
|
|
65
|
+
readIdleTimeoutMs?: number;
|
|
66
|
+
allowAbsoluteEndpoint?: boolean;
|
|
67
|
+
}): Promise<Buffer> {
|
|
68
|
+
const { response, buffer } = await performMatrixRequest({
|
|
69
|
+
homeserver: this.homeserver,
|
|
70
|
+
accessToken: this.accessToken,
|
|
71
|
+
method: params.method,
|
|
72
|
+
endpoint: params.endpoint,
|
|
73
|
+
qs: params.qs,
|
|
74
|
+
timeoutMs: params.timeoutMs,
|
|
75
|
+
raw: true,
|
|
76
|
+
maxBytes: params.maxBytes,
|
|
77
|
+
readIdleTimeoutMs: params.readIdleTimeoutMs,
|
|
78
|
+
ssrfPolicy: this.ssrfPolicy,
|
|
79
|
+
dispatcherPolicy: this.dispatcherPolicy,
|
|
80
|
+
allowAbsoluteEndpoint: params.allowAbsoluteEndpoint,
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw buildHttpError(response.status, buffer.toString("utf8"));
|
|
84
|
+
}
|
|
85
|
+
return buffer;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FileLockOptions } from "openclaw/plugin-sdk/infra-runtime";
|
|
2
|
+
|
|
3
|
+
export const MATRIX_IDB_PERSIST_INTERVAL_MS = 60_000;
|
|
4
|
+
|
|
5
|
+
const IDB_SNAPSHOT_LOCK_STALE_MS = 5 * 60_000;
|
|
6
|
+
const IDB_SNAPSHOT_LOCK_RETRY_BASE = {
|
|
7
|
+
factor: 2,
|
|
8
|
+
minTimeout: 50,
|
|
9
|
+
maxTimeout: 5_000,
|
|
10
|
+
randomize: true,
|
|
11
|
+
} satisfies Omit<FileLockOptions["retries"], "retries">;
|
|
12
|
+
|
|
13
|
+
function computeRetryDelayMs(retries: FileLockOptions["retries"], attempt: number): number {
|
|
14
|
+
return Math.min(
|
|
15
|
+
retries.maxTimeout,
|
|
16
|
+
Math.max(retries.minTimeout, retries.minTimeout * retries.factor ** attempt),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function computeMinimumRetryWindowMs(retries: FileLockOptions["retries"]): number {
|
|
21
|
+
let total = 0;
|
|
22
|
+
const attempts = Math.max(1, retries.retries + 1);
|
|
23
|
+
for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
|
|
24
|
+
total += computeRetryDelayMs(retries, attempt);
|
|
25
|
+
}
|
|
26
|
+
return total;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveRetriesForMinimumWindowMs(
|
|
30
|
+
retries: Omit<FileLockOptions["retries"], "retries">,
|
|
31
|
+
minimumWindowMs: number,
|
|
32
|
+
): FileLockOptions["retries"] {
|
|
33
|
+
const resolved: FileLockOptions["retries"] = {
|
|
34
|
+
...retries,
|
|
35
|
+
retries: 0,
|
|
36
|
+
};
|
|
37
|
+
while (computeMinimumRetryWindowMs(resolved) < minimumWindowMs) {
|
|
38
|
+
resolved.retries += 1;
|
|
39
|
+
}
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const MATRIX_IDB_SNAPSHOT_LOCK_OPTIONS: FileLockOptions = {
|
|
44
|
+
// Wait longer than one periodic persist interval so a concurrent restore
|
|
45
|
+
// or large snapshot dump finishes instead of forcing warn-and-continue.
|
|
46
|
+
retries: resolveRetriesForMinimumWindowMs(
|
|
47
|
+
IDB_SNAPSHOT_LOCK_RETRY_BASE,
|
|
48
|
+
MATRIX_IDB_PERSIST_INTERVAL_MS,
|
|
49
|
+
),
|
|
50
|
+
stale: IDB_SNAPSHOT_LOCK_STALE_MS,
|
|
51
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "fake-indexeddb/auto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
computeMinimumRetryWindowMs,
|
|
8
|
+
MATRIX_IDB_PERSIST_INTERVAL_MS,
|
|
9
|
+
} from "./idb-persistence-lock.js";
|
|
10
|
+
import { clearAllIndexedDbState, seedDatabase } from "./idb-persistence.test-helpers.js";
|
|
11
|
+
|
|
12
|
+
const { withFileLockMock } = vi.hoisted(() => ({
|
|
13
|
+
withFileLockMock: vi.fn(
|
|
14
|
+
async <T>(_filePath: string, _options: unknown, fn: () => Promise<T>) => await fn(),
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("openclaw/plugin-sdk/infra-runtime", async () => {
|
|
19
|
+
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/infra-runtime")>(
|
|
20
|
+
"openclaw/plugin-sdk/infra-runtime",
|
|
21
|
+
);
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
withFileLock: withFileLockMock,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let persistIdbToDisk: typeof import("./idb-persistence.js").persistIdbToDisk;
|
|
29
|
+
let restoreIdbFromDisk: typeof import("./idb-persistence.js").restoreIdbFromDisk;
|
|
30
|
+
type CapturedLockOptions =
|
|
31
|
+
typeof import("./idb-persistence-lock.js").MATRIX_IDB_SNAPSHOT_LOCK_OPTIONS;
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
({ persistIdbToDisk, restoreIdbFromDisk } = await import("./idb-persistence.js"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Matrix IndexedDB persistence lock ordering", () => {
|
|
38
|
+
let tmpDir: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-idb-lock-order-"));
|
|
42
|
+
withFileLockMock.mockReset();
|
|
43
|
+
withFileLockMock.mockImplementation(
|
|
44
|
+
async <T>(_filePath: string, _options: unknown, fn: () => Promise<T>) => await fn(),
|
|
45
|
+
);
|
|
46
|
+
await clearAllIndexedDbState();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await clearAllIndexedDbState();
|
|
51
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("captures the snapshot after the file lock is acquired", async () => {
|
|
55
|
+
const snapshotPath = path.join(tmpDir, "crypto-idb-snapshot.json");
|
|
56
|
+
const dbName = "openclaw-matrix-test::matrix-sdk-crypto";
|
|
57
|
+
await seedDatabase({
|
|
58
|
+
name: dbName,
|
|
59
|
+
storeName: "sessions",
|
|
60
|
+
records: [{ key: "room-1", value: { session: "old-session" } }],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
withFileLockMock.mockImplementationOnce(async (_filePath, _options, fn) => {
|
|
64
|
+
await seedDatabase({
|
|
65
|
+
name: dbName,
|
|
66
|
+
storeName: "sessions",
|
|
67
|
+
records: [{ key: "room-1", value: { session: "new-session" } }],
|
|
68
|
+
});
|
|
69
|
+
return await fn();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" });
|
|
73
|
+
|
|
74
|
+
const data = JSON.parse(fs.readFileSync(snapshotPath, "utf8")) as Array<{
|
|
75
|
+
stores: Array<{
|
|
76
|
+
name: string;
|
|
77
|
+
records: Array<{ key: IDBValidKey; value: { session: string } }>;
|
|
78
|
+
}>;
|
|
79
|
+
}>;
|
|
80
|
+
const sessionsStore = data[0]?.stores.find((store) => store.name === "sessions");
|
|
81
|
+
expect(sessionsStore?.records).toEqual([{ key: "room-1", value: { session: "new-session" } }]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("waits at least one persist interval before timing out on snapshot lock contention", async () => {
|
|
85
|
+
const snapshotPath = path.join(tmpDir, "crypto-idb-snapshot.json");
|
|
86
|
+
const capturedOptions: CapturedLockOptions[] = [];
|
|
87
|
+
|
|
88
|
+
withFileLockMock.mockImplementationOnce(async (_filePath, options) => {
|
|
89
|
+
capturedOptions.push(options as CapturedLockOptions);
|
|
90
|
+
return 0;
|
|
91
|
+
});
|
|
92
|
+
await persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" });
|
|
93
|
+
|
|
94
|
+
withFileLockMock.mockImplementationOnce(async (_filePath, options) => {
|
|
95
|
+
capturedOptions.push(options as CapturedLockOptions);
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
98
|
+
await restoreIdbFromDisk(snapshotPath);
|
|
99
|
+
|
|
100
|
+
expect(capturedOptions).toHaveLength(2);
|
|
101
|
+
for (const options of capturedOptions) {
|
|
102
|
+
expect(computeMinimumRetryWindowMs(options.retries)).toBeGreaterThanOrEqual(
|
|
103
|
+
MATRIX_IDB_PERSIST_INTERVAL_MS,
|
|
104
|
+
);
|
|
105
|
+
expect(options.stale).toBe(5 * 60_000);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export async function clearAllIndexedDbState(): Promise<void> {
|
|
2
|
+
const databases = await indexedDB.databases();
|
|
3
|
+
await Promise.all(
|
|
4
|
+
databases
|
|
5
|
+
.map((entry) => entry.name)
|
|
6
|
+
.filter((name): name is string => Boolean(name))
|
|
7
|
+
.map(
|
|
8
|
+
(name) =>
|
|
9
|
+
new Promise<void>((resolve, reject) => {
|
|
10
|
+
const req = indexedDB.deleteDatabase(name);
|
|
11
|
+
req.addEventListener("success", () => resolve(), { once: true });
|
|
12
|
+
req.addEventListener("error", () => reject(req.error), { once: true });
|
|
13
|
+
req.addEventListener("blocked", () => resolve(), { once: true });
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function seedDatabase(params: {
|
|
20
|
+
name: string;
|
|
21
|
+
version?: number;
|
|
22
|
+
storeName: string;
|
|
23
|
+
records: Array<{ key: IDBValidKey; value: unknown }>;
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
await new Promise<void>((resolve, reject) => {
|
|
26
|
+
const req = indexedDB.open(params.name, params.version ?? 1);
|
|
27
|
+
req.addEventListener("upgradeneeded", () => {
|
|
28
|
+
const db = req.result;
|
|
29
|
+
if (!db.objectStoreNames.contains(params.storeName)) {
|
|
30
|
+
db.createObjectStore(params.storeName);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
req.addEventListener("success", () => {
|
|
34
|
+
const db = req.result;
|
|
35
|
+
const tx = db.transaction(params.storeName, "readwrite");
|
|
36
|
+
const store = tx.objectStore(params.storeName);
|
|
37
|
+
for (const record of params.records) {
|
|
38
|
+
store.put(record.value, record.key);
|
|
39
|
+
}
|
|
40
|
+
tx.addEventListener("complete", () => {
|
|
41
|
+
db.close();
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
tx.addEventListener("error", () => reject(tx.error), { once: true });
|
|
45
|
+
});
|
|
46
|
+
req.addEventListener("error", () => reject(req.error), { once: true });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function readDatabaseRecords(params: {
|
|
51
|
+
name: string;
|
|
52
|
+
version?: number;
|
|
53
|
+
storeName: string;
|
|
54
|
+
}): Promise<Array<{ key: IDBValidKey; value: unknown }>> {
|
|
55
|
+
return await new Promise((resolve, reject) => {
|
|
56
|
+
const req = indexedDB.open(params.name, params.version ?? 1);
|
|
57
|
+
req.addEventListener("success", () => {
|
|
58
|
+
const db = req.result;
|
|
59
|
+
const tx = db.transaction(params.storeName, "readonly");
|
|
60
|
+
const store = tx.objectStore(params.storeName);
|
|
61
|
+
const keysReq = store.getAllKeys();
|
|
62
|
+
const valuesReq = store.getAll();
|
|
63
|
+
let keys: IDBValidKey[] | null = null;
|
|
64
|
+
let values: unknown[] | null = null;
|
|
65
|
+
|
|
66
|
+
const maybeResolve = () => {
|
|
67
|
+
if (!keys || !values) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
db.close();
|
|
71
|
+
const resolvedValues = values;
|
|
72
|
+
resolve(keys.map((key, index) => ({ key, value: resolvedValues[index] })));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
keysReq.addEventListener("success", () => {
|
|
76
|
+
keys = keysReq.result;
|
|
77
|
+
maybeResolve();
|
|
78
|
+
});
|
|
79
|
+
valuesReq.addEventListener("success", () => {
|
|
80
|
+
values = valuesReq.result;
|
|
81
|
+
maybeResolve();
|
|
82
|
+
});
|
|
83
|
+
keysReq.addEventListener("error", () => reject(keysReq.error), { once: true });
|
|
84
|
+
valuesReq.addEventListener("error", () => reject(valuesReq.error), { once: true });
|
|
85
|
+
});
|
|
86
|
+
req.addEventListener("error", () => reject(req.error), { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import "fake-indexeddb/auto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
drainFileLockStateForTest,
|
|
7
|
+
resetFileLockStateForTest,
|
|
8
|
+
} from "openclaw/plugin-sdk/infra-runtime";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { persistIdbToDisk, restoreIdbFromDisk } from "./idb-persistence.js";
|
|
11
|
+
import {
|
|
12
|
+
clearAllIndexedDbState,
|
|
13
|
+
readDatabaseRecords,
|
|
14
|
+
seedDatabase,
|
|
15
|
+
} from "./idb-persistence.test-helpers.js";
|
|
16
|
+
import { LogService } from "./logger.js";
|
|
17
|
+
|
|
18
|
+
describe("Matrix IndexedDB persistence", () => {
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-idb-persist-"));
|
|
24
|
+
warnSpy = vi.spyOn(LogService, "warn").mockImplementation(() => {});
|
|
25
|
+
await clearAllIndexedDbState();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
warnSpy.mockRestore();
|
|
30
|
+
await clearAllIndexedDbState();
|
|
31
|
+
resetFileLockStateForTest();
|
|
32
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("persists and restores database contents for the selected prefix", async () => {
|
|
36
|
+
const snapshotPath = path.join(tmpDir, "crypto-idb-snapshot.json");
|
|
37
|
+
await seedDatabase({
|
|
38
|
+
name: "openclaw-matrix-test::matrix-sdk-crypto",
|
|
39
|
+
storeName: "sessions",
|
|
40
|
+
records: [{ key: "room-1", value: { session: "abc123" } }],
|
|
41
|
+
});
|
|
42
|
+
await seedDatabase({
|
|
43
|
+
name: "other-prefix::matrix-sdk-crypto",
|
|
44
|
+
storeName: "sessions",
|
|
45
|
+
records: [{ key: "room-2", value: { session: "should-not-restore" } }],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await persistIdbToDisk({
|
|
49
|
+
snapshotPath,
|
|
50
|
+
databasePrefix: "openclaw-matrix-test",
|
|
51
|
+
});
|
|
52
|
+
expect(fs.existsSync(snapshotPath)).toBe(true);
|
|
53
|
+
|
|
54
|
+
const mode = fs.statSync(snapshotPath).mode & 0o777;
|
|
55
|
+
expect(mode).toBe(0o600);
|
|
56
|
+
|
|
57
|
+
await clearAllIndexedDbState();
|
|
58
|
+
|
|
59
|
+
const restored = await restoreIdbFromDisk(snapshotPath);
|
|
60
|
+
expect(restored).toBe(true);
|
|
61
|
+
|
|
62
|
+
const restoredRecords = await readDatabaseRecords({
|
|
63
|
+
name: "openclaw-matrix-test::matrix-sdk-crypto",
|
|
64
|
+
storeName: "sessions",
|
|
65
|
+
});
|
|
66
|
+
expect(restoredRecords).toEqual([{ key: "room-1", value: { session: "abc123" } }]);
|
|
67
|
+
|
|
68
|
+
const dbs = await indexedDB.databases();
|
|
69
|
+
expect(dbs.some((entry) => entry.name === "other-prefix::matrix-sdk-crypto")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns false and logs a warning for malformed snapshots", async () => {
|
|
73
|
+
const snapshotPath = path.join(tmpDir, "bad-snapshot.json");
|
|
74
|
+
fs.writeFileSync(snapshotPath, JSON.stringify([{ nope: true }]), "utf8");
|
|
75
|
+
|
|
76
|
+
const restored = await restoreIdbFromDisk(snapshotPath);
|
|
77
|
+
expect(restored).toBe(false);
|
|
78
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
79
|
+
"IdbPersistence",
|
|
80
|
+
expect.stringContaining(`Failed to restore IndexedDB snapshot from ${snapshotPath}:`),
|
|
81
|
+
expect.any(Error),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns false for empty snapshot payloads without restoring databases", async () => {
|
|
86
|
+
const snapshotPath = path.join(tmpDir, "empty-snapshot.json");
|
|
87
|
+
fs.writeFileSync(snapshotPath, JSON.stringify([]), "utf8");
|
|
88
|
+
|
|
89
|
+
const restored = await restoreIdbFromDisk(snapshotPath);
|
|
90
|
+
expect(restored).toBe(false);
|
|
91
|
+
|
|
92
|
+
const dbs = await indexedDB.databases();
|
|
93
|
+
expect(dbs).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("serializes concurrent persist operations via file lock", async () => {
|
|
97
|
+
const snapshotPath = path.join(tmpDir, "concurrent-persist.json");
|
|
98
|
+
await seedDatabase({
|
|
99
|
+
name: "openclaw-matrix-test::matrix-sdk-crypto",
|
|
100
|
+
storeName: "sessions",
|
|
101
|
+
records: [{ key: "room-1", value: { session: "abc123" } }],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await Promise.all([
|
|
105
|
+
persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" }),
|
|
106
|
+
persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" }),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect(fs.existsSync(snapshotPath)).toBe(true);
|
|
110
|
+
|
|
111
|
+
const data = JSON.parse(fs.readFileSync(snapshotPath, "utf8"));
|
|
112
|
+
expect(Array.isArray(data)).toBe(true);
|
|
113
|
+
expect(data.length).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("releases lock after persist completes", async () => {
|
|
117
|
+
const snapshotPath = path.join(tmpDir, "lock-release.json");
|
|
118
|
+
await seedDatabase({
|
|
119
|
+
name: "openclaw-matrix-test::matrix-sdk-crypto",
|
|
120
|
+
storeName: "sessions",
|
|
121
|
+
records: [{ key: "room-1", value: { session: "abc123" } }],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" });
|
|
125
|
+
|
|
126
|
+
const lockPath = `${snapshotPath}.lock`;
|
|
127
|
+
expect(fs.existsSync(lockPath)).toBe(false);
|
|
128
|
+
await drainFileLockStateForTest();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("releases lock after restore completes", async () => {
|
|
132
|
+
const snapshotPath = path.join(tmpDir, "lock-release-restore.json");
|
|
133
|
+
await seedDatabase({
|
|
134
|
+
name: "openclaw-matrix-test::matrix-sdk-crypto",
|
|
135
|
+
storeName: "sessions",
|
|
136
|
+
records: [{ key: "room-1", value: { session: "abc123" } }],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await persistIdbToDisk({ snapshotPath, databasePrefix: "openclaw-matrix-test" });
|
|
140
|
+
await clearAllIndexedDbState();
|
|
141
|
+
await drainFileLockStateForTest();
|
|
142
|
+
|
|
143
|
+
await restoreIdbFromDisk(snapshotPath);
|
|
144
|
+
|
|
145
|
+
const lockPath = `${snapshotPath}.lock`;
|
|
146
|
+
expect(fs.existsSync(lockPath)).toBe(false);
|
|
147
|
+
await drainFileLockStateForTest();
|
|
148
|
+
});
|
|
149
|
+
});
|