@mh-gg/relay-runtime 0.1.1-alpha.20260613T085325975Z

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.
@@ -0,0 +1,651 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ const { manifestHash } = require("@mh-gg/base");
5
+ const { createMemoryOperationLog, createMemoryRoomStore } = require("@mh-gg/host-runtime");
6
+ const { createRelayPluginRuntimeManager } = require("../src/index.cjs");
7
+ const { kanbanAppPack, kanbanHostPlugins, kanbanHostPlugin, KANBAN_PLUGIN_ID } = require("../../../examples/kanban/src/sdk-app.mjs");
8
+ const { COMMENTS_PLUGIN_ID } = require("@mh-gg/base-plugins");
9
+ const { ensureOperationIdentity } = require("@mh-gg/protocol");
10
+
11
+ function actor(role = "admin", memberId = "admin") {
12
+ return { memberId, deviceId: `dev_${memberId}`, role, displayName: memberId };
13
+ }
14
+
15
+ function operation(overrides = {}) {
16
+ const op = {
17
+ clientOperationId: overrides.id || "op_1",
18
+ roomId: "room_resilient_board",
19
+ appPackId: kanbanAppPack.id,
20
+ appPackHash: manifestHash(kanbanAppPack),
21
+ pluginId: overrides.pluginId || KANBAN_PLUGIN_ID,
22
+ type: overrides.type || "list.create",
23
+ actor: overrides.actor || actor(),
24
+ seq: overrides.seq || 1,
25
+ createdAt: overrides.createdAt || 1000,
26
+ payload: overrides.payload || { title: "Backlog" },
27
+ auth: { credentialId: "cred", signature: "sig" },
28
+ ...overrides
29
+ };
30
+ delete op.id;
31
+ return ensureOperationIdentity(op, { now: op.createdAt, nodeId: op.actor?.deviceId || "dev_admin" });
32
+ }
33
+
34
+ async function enable(manager, extra = {}) {
35
+ return manager.enableRoomComposition({
36
+ roomName: "room_resilient_board",
37
+ appPack: kanbanAppPack,
38
+ plugins: kanbanHostPlugins,
39
+ authenticateActor: async (_auth, opActor) => opActor,
40
+ now: () => 1700000000000,
41
+ ...extra
42
+ });
43
+ }
44
+
45
+ test("relay plugin runtime manager enables every plugin in the composition set", async () => {
46
+ const manager = createRelayPluginRuntimeManager();
47
+ await enable(manager);
48
+
49
+ assert.equal(manager.hasRoom("room_resilient_board"), true);
50
+ assert.deepEqual(manager.enabledPlugins("room_resilient_board").map((plugin) => plugin.id), kanbanHostPlugins.map((plugin) => plugin.id));
51
+ assert.ok(manager.enabledPlugins("room_resilient_board").some((plugin) => plugin.id === COMMENTS_PLUGIN_ID), "shared comments plugin should be enabled too");
52
+ });
53
+
54
+ test("relay plugin runtime manager exposes partial capabilities and preserves missing plugin state", async () => {
55
+ const source = createRelayPluginRuntimeManager();
56
+ await enable(source);
57
+ const fullSnapshot = await source.snapshot("room_resilient_board");
58
+ const partialPlugins = kanbanHostPlugins.filter((plugin) => plugin.id !== COMMENTS_PLUGIN_ID);
59
+ const partial = createRelayPluginRuntimeManager();
60
+ await partial.enableRoomComposition({
61
+ roomName: "room_resilient_board",
62
+ appPack: kanbanAppPack,
63
+ plugins: partialPlugins,
64
+ expectedPlugins: kanbanHostPlugins,
65
+ authenticateActor: async (_auth, opActor) => opActor,
66
+ state: fullSnapshot.state,
67
+ now: () => 1700000000000
68
+ });
69
+
70
+ const capabilities = partial.runtimeCapabilities("room_resilient_board");
71
+ assert.equal(capabilities.mode, "partial");
72
+ assert.equal(capabilities.unavailablePlugins.some((plugin) => plugin.id === COMMENTS_PLUGIN_ID), true);
73
+ assert.equal(capabilities.unavailableOperations.some((item) => item.pluginId === COMMENTS_PLUGIN_ID && item.type === "comments.add"), true);
74
+ assert.equal(partial.roomApp("room_resilient_board").runtimeCapabilities.mode, "partial");
75
+ assert.ok((await partial.snapshot("room_resilient_board")).state.plugins[COMMENTS_PLUGIN_ID], "missing plugin state should remain opaque");
76
+
77
+ const available = await partial.handleClientOperation({
78
+ roomName: "room_resilient_board",
79
+ peerId: "peer",
80
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "partial_available", seq: 1 }) },
81
+ sendToClient: () => {},
82
+ broadcastToRoom: () => {}
83
+ });
84
+ assert.equal(available.ok, true);
85
+
86
+ const sent = [];
87
+ const missing = await partial.handleClientOperation({
88
+ roomName: "room_resilient_board",
89
+ peerId: "peer",
90
+ message: {
91
+ type: "client/operation",
92
+ protocol: 1,
93
+ roomName: "room_resilient_board",
94
+ clientId: "alice",
95
+ operation: operation({
96
+ id: "partial_missing_comments",
97
+ seq: 2,
98
+ pluginId: COMMENTS_PLUGIN_ID,
99
+ type: "comments.add",
100
+ payload: { scopeType: "card", scopeId: "card-1", body: "Needs discussion" }
101
+ })
102
+ },
103
+ sendToClient: (peerId, message) => sent.push({ peerId, message }),
104
+ broadcastToRoom: () => {}
105
+ });
106
+
107
+ assert.equal(missing.ok, false);
108
+ assert.equal(missing.code, "PLUGIN_UNAVAILABLE");
109
+ assert.equal(missing.retryableWhenCapabilityAvailable, true);
110
+ assert.equal(sent[0].message.result.code, "PLUGIN_UNAVAILABLE");
111
+ assert.equal(sent[0].message.result.retryableWhenCapabilityAvailable, true);
112
+ });
113
+
114
+ test("relay plugin runtime manager logs authoritative runtime decisions", async () => {
115
+ const logs = [];
116
+ const logger = {
117
+ info: (message, fields) => logs.push({ level: "info", message, fields }),
118
+ warn: (message, fields) => logs.push({ level: "warn", message, fields })
119
+ };
120
+ const manager = createRelayPluginRuntimeManager({ logger });
121
+ await enable(manager);
122
+
123
+ const acceptedOperation = operation({ id: "logged_accept" });
124
+ const accepted = await manager.handleClientOperation({
125
+ roomName: "room_resilient_board",
126
+ peerId: "peer",
127
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", operation: acceptedOperation },
128
+ sendToClient: () => {},
129
+ broadcastToRoom: () => {}
130
+ });
131
+ assert.equal(accepted.ok, true);
132
+
133
+ const rejected = await manager.handleRelayOperation({
134
+ roomName: "room_resilient_board",
135
+ operation: { nope: true }
136
+ });
137
+ assert.equal(rejected.ok, false);
138
+
139
+ assert.equal(logs.some((entry) =>
140
+ entry.level === "info" &&
141
+ entry.message.includes("relay runtime accepted") &&
142
+ entry.fields.roomName === "room_resilient_board" &&
143
+ entry.fields.operationId === acceptedOperation.id
144
+ ), true);
145
+ assert.equal(logs.some((entry) =>
146
+ entry.level === "warn" &&
147
+ entry.message.includes("relay runtime rejected") &&
148
+ entry.fields.roomName === "room_resilient_board" &&
149
+ entry.fields.relayDirection === "relay"
150
+ ), true);
151
+ });
152
+
153
+ test("relay plugin runtime manager refreshes room app metadata", async () => {
154
+ const manager = createRelayPluginRuntimeManager();
155
+ await enable(manager, {
156
+ roomApp: { appPack: { id: kanbanAppPack.id, hash: "sha256-stale" }, frontends: [{ id: "player", integrity: "sha256-old" }] }
157
+ });
158
+
159
+ assert.equal(manager.roomApp("room_resilient_board").frontends[0].integrity, "sha256-old");
160
+ assert.equal(manager.roomApp("room_resilient_board").appPack.hash, manifestHash(kanbanAppPack));
161
+ assert.equal(manager.updateRoomApp("room_resilient_board", { frontends: [{ id: "player", dynamic: true, integrity: "sha256-new" }] }), true);
162
+ assert.equal(manager.roomApp("room_resilient_board").frontends[0].dynamic, true);
163
+ assert.equal(manager.roomApp("room_resilient_board").frontends[0].integrity, "sha256-new");
164
+ assert.equal(manager.updateRoomApp("missing", { frontends: [] }), false);
165
+
166
+ await enable(manager, {
167
+ roomApp: { frontends: [{ id: "player", integrity: "sha256-reregistered" }] }
168
+ });
169
+ assert.equal(manager.roomApp("room_resilient_board").frontends[0].integrity, "sha256-reregistered");
170
+ });
171
+
172
+ test("relay plugin runtime manager does not retain failed plugin starts", async () => {
173
+ const manager = createRelayPluginRuntimeManager();
174
+ await assert.rejects(() => manager.enableRoomComposition({
175
+ roomName: "room_bad_start",
176
+ appPack: kanbanAppPack,
177
+ plugins: [{ id: KANBAN_PLUGIN_ID, version: "0.0.0" }],
178
+ authenticateActor: async (_auth, opActor) => opActor
179
+ }), /createInitialState is required/);
180
+
181
+ assert.equal(manager.hasRoom("room_bad_start"), false);
182
+
183
+ await manager.enableRoomComposition({
184
+ roomName: "room_bad_start",
185
+ appPack: kanbanAppPack,
186
+ plugins: kanbanHostPlugins,
187
+ authenticateActor: async (_auth, opActor) => opActor
188
+ });
189
+ assert.equal(manager.hasRoom("room_bad_start"), true);
190
+ assert.deepEqual(manager.enabledPlugins("room_bad_start").map((plugin) => plugin.id), kanbanHostPlugins.map((plugin) => plugin.id));
191
+ });
192
+
193
+ test("relay plugin runtime manager repairs stale non-executable room entries", async () => {
194
+ const manager = createRelayPluginRuntimeManager();
195
+ const appPack = {
196
+ id: kanbanAppPack.id,
197
+ version: kanbanAppPack.version,
198
+ hash: manifestHash(kanbanAppPack),
199
+ protocolHash: kanbanAppPack.compatibility.appProtocolHash
200
+ };
201
+ const room = { id: "room_stale_bad_start", appPack };
202
+ manager.rooms.set(room.id, {
203
+ roomName: room.id,
204
+ room,
205
+ hostPack: null,
206
+ plugins: [{ id: KANBAN_PLUGIN_ID, version: "0.0.0" }],
207
+ store: createMemoryRoomStore(),
208
+ operationLog: createMemoryOperationLog(),
209
+ roomApp: {},
210
+ runtime: { authenticateActor: async (_auth, opActor) => opActor, capabilities: [] }
211
+ });
212
+
213
+ await manager.enableRoomComposition({
214
+ roomName: room.id,
215
+ appPack: kanbanAppPack,
216
+ plugins: kanbanHostPlugins,
217
+ authenticateActor: async (_auth, opActor) => opActor
218
+ });
219
+
220
+ assert.equal(manager.hasRoom(room.id), true);
221
+ assert.deepEqual(manager.enabledPlugins(room.id).map((plugin) => plugin.id), kanbanHostPlugins.map((plugin) => plugin.id));
222
+ assert.equal((await manager.snapshot(room.id)).state.appPack.hash, appPack.hash);
223
+ });
224
+
225
+ test("relay plugin runtime manager upgrades same-app hash without dropping state", async () => {
226
+ const manager = createRelayPluginRuntimeManager();
227
+ await enable(manager);
228
+ await manager.handleClientOperation({
229
+ roomName: "room_resilient_board",
230
+ peerId: "peer",
231
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", operation: operation({ id: "before_upgrade" }) },
232
+ sendToClient: () => {}
233
+ });
234
+
235
+ const upgradedAppPack = { ...kanbanAppPack, hash: "sha256-upgraded-kanban" };
236
+ await enable(manager, { appPack: upgradedAppPack });
237
+ const upgraded = await manager.snapshot("room_resilient_board");
238
+
239
+ assert.equal(upgraded.room.appPack.hash, upgradedAppPack.hash);
240
+ assert.equal(upgraded.state.appPack.hash, upgradedAppPack.hash);
241
+ assert.equal(upgraded.state.version, 1);
242
+ assert.equal(Object.keys(upgraded.state.plugins[KANBAN_PLUGIN_ID].lists).length, 1);
243
+
244
+ const afterOperation = operation({ id: "after_upgrade", seq: 2, appPackHash: upgradedAppPack.hash, payload: { title: "Later" } });
245
+ const after = await manager.handleClientOperation({
246
+ roomName: "room_resilient_board",
247
+ peerId: "peer",
248
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", operation: afterOperation },
249
+ sendToClient: () => {},
250
+ broadcastToRoom: async (roomName, createMessage) => {
251
+ assert.equal(roomName, "room_resilient_board");
252
+ assert.equal((await createMessage()).acceptedOperationId, afterOperation.id);
253
+ assert.equal((await createMessage(actor("member", "viewer"))).state.roomId, "room_resilient_board");
254
+ }
255
+ });
256
+
257
+ assert.equal(after.ok, true);
258
+ assert.equal((await manager.snapshot("room_resilient_board")).state.version, 2);
259
+ });
260
+
261
+ test("relay plugin runtime manager rolls back failed same-app runtime swaps", async () => {
262
+ const manager = createRelayPluginRuntimeManager();
263
+ await enable(manager);
264
+ await manager.handleClientOperation({
265
+ roomName: "room_resilient_board",
266
+ peerId: "peer",
267
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", operation: operation({ id: "before_failed_swap" }) },
268
+ sendToClient: () => {}
269
+ });
270
+ const before = await manager.snapshot("room_resilient_board");
271
+ const failingPlugins = kanbanHostPlugins.map((plugin) => plugin.id === KANBAN_PLUGIN_ID
272
+ ? {
273
+ ...plugin,
274
+ schemas: {
275
+ ...plugin.schemas,
276
+ state: {
277
+ parse() {
278
+ throw new Error("schema upgrade rejected");
279
+ }
280
+ }
281
+ }
282
+ }
283
+ : plugin);
284
+
285
+ await assert.rejects(() => enable(manager, {
286
+ appPack: { ...kanbanAppPack, hash: "sha256-bad-upgrade" },
287
+ plugins: failingPlugins
288
+ }), /schema upgrade rejected/);
289
+ const after = await manager.snapshot("room_resilient_board");
290
+
291
+ assert.deepEqual(after.state, before.state);
292
+ assert.equal(after.room.appPack.hash, before.room.appPack.hash);
293
+ assert.deepEqual(manager.enabledPlugins("room_resilient_board").map((plugin) => plugin.id), kanbanHostPlugins.map((plugin) => plugin.id));
294
+ });
295
+
296
+ test("relay plugin runtime manager applies same-app historical operations during catch-up", async () => {
297
+ const manager = createRelayPluginRuntimeManager();
298
+ await enable(manager);
299
+ const oldHashOperation = operation({
300
+ id: "op_old_hash_catchup",
301
+ appPackHash: "sha256-older-kanban-schema",
302
+ payload: { title: "Catch-up state" }
303
+ });
304
+
305
+ const result = await manager.handleRelayOperation({
306
+ type: "relay.matterhorn-operation",
307
+ roomName: "room_resilient_board",
308
+ operation: oldHashOperation
309
+ });
310
+ const snapshot = await manager.snapshot("room_resilient_board");
311
+
312
+ assert.equal(result.ok, true, result.reason);
313
+ assert.equal(snapshot.state.plugins[KANBAN_PLUGIN_ID].lists[0].title, "Catch-up state");
314
+ });
315
+
316
+ test("relay plugin runtime manager ignores relay operations for another app", async () => {
317
+ const warnings = [];
318
+ const logger = {
319
+ debug() {},
320
+ info() {},
321
+ warn(...args) { warnings.push(args); },
322
+ error() {}
323
+ };
324
+ const manager = createRelayPluginRuntimeManager({ logger });
325
+ await enable(manager);
326
+ const wrongAppOperation = operation({
327
+ id: "op_wrong_app_catchup",
328
+ appPackId: "com.matterhorn.other"
329
+ });
330
+
331
+ const result = await manager.handleRelayOperation({
332
+ type: "relay.matterhorn-operation",
333
+ roomName: "room_resilient_board",
334
+ operation: wrongAppOperation
335
+ });
336
+ const snapshot = await manager.snapshot("room_resilient_board");
337
+
338
+ assert.equal(result.ok, false);
339
+ assert.equal(result.ignored, true);
340
+ assert.equal(result.code, "APP_PACK_MISMATCH");
341
+ assert.equal(result.reason, "app-mismatch");
342
+ assert.equal(snapshot.state.version, 0);
343
+ assert.equal(warnings.length, 1);
344
+ assert.match(warnings[0][0], /relay runtime rejected/);
345
+ assert.equal(warnings[0][1].reason, "app-mismatch");
346
+ });
347
+
348
+ test("relay plugin runtimes keep a room running and converge when a peer relay returns", async () => {
349
+ const sentFromA = [];
350
+ const sentFromB = [];
351
+ const relayA = createRelayPluginRuntimeManager({ relayAddress: "relay-a", sendRelayEnvelope: (message) => sentFromA.push(message) });
352
+ const relayB = createRelayPluginRuntimeManager({ relayAddress: "relay-b", sendRelayEnvelope: (message) => sentFromB.push(message) });
353
+ await enable(relayA);
354
+ await enable(relayB);
355
+
356
+ const result1 = await relayA.handleClientOperation({
357
+ roomName: "room_resilient_board",
358
+ peerId: "alice-peer",
359
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_list", seq: 1 }) },
360
+ sendToClient: () => {},
361
+ broadcastToRoom: () => {}
362
+ });
363
+ assert.equal(result1.ok, true);
364
+ assert.equal(sentFromA[0].type, "relay.matterhorn-operation");
365
+
366
+ const replayOnB = await relayB.handleRelayOperation(sentFromA[0]);
367
+ assert.equal(replayOnB.ok, true);
368
+ assert.equal((await relayB.snapshot("room_resilient_board")).state.plugins[KANBAN_PLUGIN_ID].lists[0].title, "Backlog");
369
+
370
+ const backlogId = (await relayB.snapshot("room_resilient_board")).state.plugins[KANBAN_PLUGIN_ID].lists[0].id;
371
+ const result2 = await relayB.handleClientOperation({
372
+ roomName: "room_resilient_board",
373
+ peerId: "bob-peer",
374
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "bob", operation: operation({ id: "op_card", seq: 2, type: "card.create", payload: { listId: backlogId, title: "Continue while relay A is down" }, actor: actor("member", "bob"), createdAt: 1001 }) },
375
+ sendToClient: () => {},
376
+ broadcastToRoom: () => {}
377
+ });
378
+ assert.equal(result2.ok, true);
379
+
380
+ const snapshotWhileADown = await relayB.snapshot("room_resilient_board");
381
+ assert.equal(snapshotWhileADown.state.version, 2);
382
+ assert.equal(snapshotWhileADown.state.plugins[KANBAN_PLUGIN_ID].lists[0].cardIds.length, 1);
383
+
384
+ const staleA = await relayA.snapshot("room_resilient_board");
385
+ assert.equal(staleA.state.version, 1);
386
+ const synced = await relayA.syncSnapshot("room_resilient_board", snapshotWhileADown.state);
387
+ assert.equal(synced.updated, true);
388
+ assert.equal((await relayA.snapshot("room_resilient_board")).state.version, 2);
389
+ });
390
+
391
+ test("relay plugin runtimes reject host re-registration snapshots that are older than relay state", async () => {
392
+ const manager = createRelayPluginRuntimeManager();
393
+ await enable(manager);
394
+ await manager.handleClientOperation({
395
+ roomName: "room_resilient_board",
396
+ peerId: "peer",
397
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_list", seq: 1 }) },
398
+ sendToClient: () => {},
399
+ broadcastToRoom: () => {}
400
+ });
401
+ const fresh = await manager.snapshot("room_resilient_board");
402
+ const stale = { ...fresh.state, version: 0, plugins: {} };
403
+
404
+ const result = await manager.syncSnapshot("room_resilient_board", stale);
405
+ assert.equal(result.updated, false);
406
+ assert.equal((await manager.snapshot("room_resilient_board")).state.version, 1);
407
+ });
408
+
409
+ test("relay plugin runtimes reject destructive newer snapshots without operation proof", async () => {
410
+ const manager = createRelayPluginRuntimeManager();
411
+ await enable(manager);
412
+ await manager.handleClientOperation({
413
+ roomName: "room_resilient_board",
414
+ peerId: "peer",
415
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_list_before_destructive", seq: 1 }) },
416
+ sendToClient: () => {},
417
+ broadcastToRoom: () => {}
418
+ });
419
+ const fresh = await manager.snapshot("room_resilient_board");
420
+ const destructive = {
421
+ ...fresh.state,
422
+ version: fresh.state.version + 1,
423
+ updatedAt: fresh.state.updatedAt + 1,
424
+ plugins: {
425
+ ...fresh.state.plugins,
426
+ [KANBAN_PLUGIN_ID]: { lists: [], cards: {}, activity: [] }
427
+ },
428
+ seenOperations: []
429
+ };
430
+
431
+ const result = await manager.syncSnapshot("room_resilient_board", destructive);
432
+ const after = await manager.snapshot("room_resilient_board");
433
+
434
+ assert.equal(result.ok, false);
435
+ assert.equal(result.reason, "snapshot-operation-proof-required");
436
+ assert.equal(after.state.version, fresh.state.version);
437
+ assert.equal(after.state.plugins[KANBAN_PLUGIN_ID].lists[0].title, "Backlog");
438
+ assert.deepEqual(after.state.seenOperations, fresh.state.seenOperations);
439
+ });
440
+
441
+ test("relay plugin runtime manager pulls newer same-app snapshot state across hash changes", async () => {
442
+ const host = createRelayPluginRuntimeManager();
443
+ await enable(host);
444
+ await host.handleClientOperation({
445
+ roomName: "room_resilient_board",
446
+ peerId: "peer",
447
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_host_state", seq: 1, payload: { title: "Host snapshot state" } }) },
448
+ sendToClient: () => {},
449
+ broadcastToRoom: () => {}
450
+ });
451
+ const hostSnapshot = await host.snapshot("room_resilient_board");
452
+ const relay = createRelayPluginRuntimeManager();
453
+ await enable(relay);
454
+
455
+ const synced = await relay.syncSnapshot("room_resilient_board", {
456
+ ...hostSnapshot.state,
457
+ appPack: { ...hostSnapshot.state.appPack, hash: "sha256-older-kanban-schema" }
458
+ });
459
+ const relaySnapshot = await relay.snapshot("room_resilient_board");
460
+
461
+ assert.equal(synced.updated, true);
462
+ assert.equal(relaySnapshot.state.appPack.hash, manifestHash(kanbanAppPack));
463
+ assert.equal(relaySnapshot.state.plugins[KANBAN_PLUGIN_ID].lists[0].title, "Host snapshot state");
464
+ });
465
+
466
+ test("relay plugin runtime manager reuses existing rooms and trusts newer registration snapshots", async () => {
467
+ const source = createRelayPluginRuntimeManager();
468
+ await enable(source);
469
+ await source.handleClientOperation({
470
+ roomName: "room_resilient_board",
471
+ peerId: "peer",
472
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_existing_list", seq: 1 }) },
473
+ sendToClient: () => {},
474
+ broadcastToRoom: () => {}
475
+ });
476
+ const newerSnapshot = await source.snapshot("room_resilient_board");
477
+
478
+ const manager = createRelayPluginRuntimeManager();
479
+ const existing = await enable(manager);
480
+ const reused = await manager.enableRoomComposition({
481
+ roomName: "room_resilient_board",
482
+ appPack: kanbanAppPack,
483
+ plugins: kanbanHostPlugins,
484
+ authenticateActor: async (_auth, opActor) => opActor
485
+ });
486
+ assert.equal(reused, existing);
487
+ assert.equal((await manager.snapshot("room_resilient_board")).state.version, 0);
488
+
489
+ const synced = await manager.enableRoomComposition({
490
+ roomName: "room_resilient_board",
491
+ appPack: kanbanAppPack,
492
+ plugins: kanbanHostPlugins,
493
+ authenticateActor: async (_auth, opActor) => opActor,
494
+ state: newerSnapshot.state
495
+ });
496
+ assert.equal(synced, existing);
497
+ assert.equal((await manager.snapshot("room_resilient_board")).state.version, 1);
498
+ });
499
+
500
+ test("relay plugin runtime manager exposes stable error paths and timestamp freshness", async () => {
501
+ const { isNewerRoomState } = require("../src/index.cjs");
502
+ assert.equal(isNewerRoomState({ version: 1, updatedAt: 20 }, { version: 1, updatedAt: 10 }), true);
503
+ assert.equal(isNewerRoomState({ version: 1, updatedAt: 5 }, { version: 2, updatedAt: 1 }), false);
504
+
505
+ const manager = createRelayPluginRuntimeManager();
506
+ await assert.rejects(() => manager.enableRoomComposition({ plugins: kanbanHostPlugins }), /roomName/);
507
+ await assert.rejects(() => manager.enableRoomComposition({ roomName: "missing_plugins", appPack: kanbanAppPack, plugins: [] }), /at least one plugin/);
508
+ assert.deepEqual(manager.enabledPlugins("missing"), []);
509
+ assert.equal(manager.roomApp("missing"), undefined);
510
+ assert.equal(await manager.snapshot("missing"), undefined);
511
+ assert.deepEqual(await manager.syncSnapshot("missing", {}), { ok: false, updated: false, reason: "room-not-enabled" });
512
+ assert.deepEqual(manager.runtimeCapabilities("missing"), { mode: "unavailable", availablePlugins: [], unavailablePlugins: [], availableOperations: [], unavailableOperations: [] });
513
+
514
+ const sent = [];
515
+ const rejected = await manager.handleClientOperation({
516
+ roomName: "missing",
517
+ peerId: "peer",
518
+ message: {},
519
+ sendToClient: (peerId, message) => sent.push({ peerId, message })
520
+ });
521
+ assert.equal(rejected.ok, false);
522
+ assert.equal(sent[0].message.code, "room-runtime-unavailable");
523
+ });
524
+
525
+ test("relay plugin runtime manager rejects bad operations and invalid snapshots without corrupting state", async () => {
526
+ const manager = createRelayPluginRuntimeManager({ authenticateActor: async (_auth, opActor) => opActor });
527
+ await enable(manager);
528
+ const state = (await manager.snapshot("room_resilient_board")).state;
529
+
530
+ assert.equal((await manager.syncSnapshot("room_resilient_board", null)).reason, "snapshot-required");
531
+ assert.equal((await manager.syncSnapshot("room_resilient_board", { ...state, roomId: "other" })).reason, "room-mismatch");
532
+ assert.equal((await manager.syncSnapshot("room_resilient_board", { ...state, appPack: { ...state.appPack, id: "other-app" } })).reason, "app-mismatch");
533
+ const sameAppHash = await manager.syncSnapshot("room_resilient_board", {
534
+ ...state,
535
+ appPack: { ...state.appPack, hash: "sha256-older-schema" },
536
+ version: state.version + 1,
537
+ updatedAt: state.updatedAt + 1
538
+ });
539
+ assert.equal(sameAppHash.updated, true);
540
+ assert.equal((await manager.snapshot("room_resilient_board")).state.appPack.hash, manifestHash(kanbanAppPack));
541
+ const current = (await manager.snapshot("room_resilient_board")).state;
542
+ const invalidMigratedSnapshot = await manager.syncSnapshot("room_resilient_board", {
543
+ ...current,
544
+ version: current.version + 1,
545
+ updatedAt: current.updatedAt + 1,
546
+ pluginVersions: {
547
+ ...current.pluginVersions,
548
+ [KANBAN_PLUGIN_ID]: "0.9.0"
549
+ }
550
+ });
551
+ assert.equal(invalidMigratedSnapshot.ok, false);
552
+ assert.match(invalidMigratedSnapshot.reason, /No migration/);
553
+
554
+ const sent = [];
555
+ const bad = await manager.handleClientOperation({
556
+ roomName: "room_resilient_board",
557
+ peerId: "peer",
558
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: { nope: true } },
559
+ sendToClient: (peerId, message) => sent.push({ peerId, message })
560
+ });
561
+ assert.equal(bad.ok, false);
562
+ assert.equal(sent[0].message.code, "bad-operation");
563
+
564
+ const relayBad = await manager.handleRelayOperation({ roomName: "room_resilient_board", operation: { nope: true } });
565
+ assert.equal(relayBad.ok, false);
566
+ });
567
+
568
+ test("relay plugin runtime manager can be created from installed metadata and refuses missing authenticators", async () => {
569
+ const manager = createRelayPluginRuntimeManager();
570
+ await manager.enableRoomComposition({
571
+ room: { id: "room_installed", appPack: { id: kanbanAppPack.id, version: kanbanAppPack.version, hash: manifestHash(kanbanAppPack), protocolHash: kanbanAppPack.compatibility.appProtocolHash } },
572
+ hostPack: { id: "host", version: "1.0.0", hash: "sha256-host", compatibility: { pluginGraphHash: "sha256-plugins" }, capabilities: { required: [] } },
573
+ installed: { plugins: kanbanHostPlugins }
574
+ });
575
+ assert.equal(manager.roomApp("room_installed").hostPack.id, "host");
576
+ const rejected = await manager.handleClientOperation({
577
+ roomName: "room_installed",
578
+ peerId: "peer",
579
+ message: { type: "client/operation", protocol: 1, roomName: "room_installed", clientId: "alice", operation: operation({ roomId: "room_installed" }) },
580
+ sendToClient: () => {}
581
+ });
582
+ assert.equal(rejected.ok, false);
583
+ assert.match(rejected.reason, /authenticateActor/);
584
+ });
585
+
586
+ test("relay plugin runtime manager handles expectedPlugins with operation descriptors", async () => {
587
+ const manager = createRelayPluginRuntimeManager();
588
+ await manager.enableRoomComposition({
589
+ roomName: "room_descriptor_test",
590
+ appPack: kanbanAppPack,
591
+ plugins: kanbanHostPlugins,
592
+ expectedPlugins: [
593
+ { id: KANBAN_PLUGIN_ID, version: "1.0.0", operations: ["list.create", "card.create"] }
594
+ ],
595
+ authenticateActor: async (_auth, opActor) => opActor
596
+ });
597
+ assert.equal(manager.hasRoom("room_descriptor_test"), true);
598
+ const capabilities = manager.runtimeCapabilities("room_descriptor_test");
599
+ assert.equal(capabilities.mode, "full");
600
+ });
601
+
602
+ test("relay plugin runtime manager rejects invalid device-signed operations", async () => {
603
+ const manager = createRelayPluginRuntimeManager();
604
+ await enable(manager);
605
+ const sent = [];
606
+ const badDeviceOp = operation({
607
+ id: "bad_device_op",
608
+ seq: 99,
609
+ auth: {
610
+ scheme: "matterhorn.device-signing.v1",
611
+ keyId: "bad",
612
+ memberId: "bad",
613
+ deviceId: "bad",
614
+ rootPublicKey: "bad",
615
+ publicKey: "bad",
616
+ publicKeyFingerprint: "bad",
617
+ claim: { roomName: "room_resilient_board", roomId: "room_resilient_board", memberId: "bad", deviceId: "bad", keyId: "bad", rootPublicKey: "bad", publicKey: "bad", publicKeyFingerprint: "bad" },
618
+ signature: "00"
619
+ }
620
+ });
621
+ const result = await manager.handleClientOperation({
622
+ roomName: "room_resilient_board",
623
+ peerId: "peer",
624
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: badDeviceOp },
625
+ sendToClient: (peerId, message) => sent.push({ peerId, message })
626
+ });
627
+ assert.equal(result.ok, false);
628
+ assert.equal(sent[0].message.code, "bad-operation");
629
+ });
630
+
631
+ test("relay plugin runtime manager detects destructive snapshots with meaningful primitive data", async () => {
632
+ const manager = createRelayPluginRuntimeManager();
633
+ await enable(manager);
634
+ await manager.handleClientOperation({
635
+ roomName: "room_resilient_board",
636
+ peerId: "peer",
637
+ message: { type: "client/operation", protocol: 1, roomName: "room_resilient_board", clientId: "alice", operation: operation({ id: "op_list", seq: 1 }) },
638
+ sendToClient: () => {},
639
+ broadcastToRoom: () => {}
640
+ });
641
+ const current = (await manager.snapshot("room_resilient_board")).state;
642
+ const destructive = {
643
+ ...current,
644
+ version: current.version + 1,
645
+ updatedAt: current.updatedAt + 1,
646
+ plugins: {}
647
+ };
648
+ const result = await manager.syncSnapshot("room_resilient_board", destructive);
649
+ assert.equal(result.ok, false);
650
+ assert.equal(result.reason, "snapshot-operation-proof-required");
651
+ });