atproto user agency toolkit for individuals and groups
1/**
2 * Tests for PolicyEngine integration with ReplicationManager.
3 *
4 * Verifies that:
5 * - Policy-driven DID list merges correctly with config DIDs
6 * - Per-DID sync intervals are respected
7 * - Priority ordering works
8 * - shouldReplicate=false skips DIDs
9 * - Backward compatibility: no PolicyEngine = identical to old behavior
10 */
11
12import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
13import { mkdtempSync, rmSync } from "node:fs";
14import { tmpdir } from "node:os";
15import { join } from "node:path";
16import Database from "better-sqlite3";
17
18import { IpfsService } from "../ipfs.js";
19import { RepoManager } from "../repo-manager.js";
20import type { Config } from "../config.js";
21import { PolicyEngine } from "../policy/engine.js";
22import { mutualAid, saas, configArchive } from "../policy/presets.js";
23import type { Policy, PolicySet } from "../policy/types.js";
24import {
25 DEFAULT_REPLICATION,
26 DEFAULT_SYNC,
27 DEFAULT_RETENTION,
28} from "../policy/types.js";
29import { ReplicationManager } from "./replication-manager.js";
30import { DidResolver } from "../did-resolver.js";
31import { InMemoryDidCache } from "../did-cache.js";
32
33// ============================================
34// Helpers
35// ============================================
36
37function testConfig(dataDir: string, replicateDids: string[] = []): Config {
38 return {
39 DID: "did:plc:test123",
40 HANDLE: "test.example.com",
41 PDS_HOSTNAME: "test.example.com",
42 AUTH_TOKEN: "test-auth-token",
43 SIGNING_KEY:
44 "0000000000000000000000000000000000000000000000000000000000000001",
45 SIGNING_KEY_PUBLIC: "zQ3shP2mWsZYWgvZM9GJ3EvMfRXQJwuTh6BdXLvJB9gFhT3Lr",
46 JWT_SECRET: "test-jwt-secret",
47 PASSWORD_HASH: "$2a$10$test",
48 DATA_DIR: dataDir,
49 PORT: 3000,
50 IPFS_ENABLED: true,
51 IPFS_NETWORKING: false,
52 REPLICATE_DIDS: replicateDids,
53 FIREHOSE_URL: "wss://localhost/xrpc/com.atproto.sync.subscribeRepos",
54 FIREHOSE_ENABLED: false,
55 RATE_LIMIT_ENABLED: false,
56 RATE_LIMIT_READ_PER_MIN: 300,
57 RATE_LIMIT_SYNC_PER_MIN: 30,
58 RATE_LIMIT_SESSION_PER_MIN: 10,
59 RATE_LIMIT_WRITE_PER_MIN: 200,
60 RATE_LIMIT_CHALLENGE_PER_MIN: 20,
61 RATE_LIMIT_MAX_CONNECTIONS: 100,
62 RATE_LIMIT_FIREHOSE_PER_IP: 3,
63 OAUTH_ENABLED: false, PUBLIC_URL: "http://localhost:3000",
64 };
65}
66
67function makePolicy(overrides: Partial<Policy> & { id: string }): Policy {
68 return {
69 name: overrides.id,
70 target: { type: "all" },
71 replication: { ...DEFAULT_REPLICATION },
72 sync: { ...DEFAULT_SYNC },
73 retention: { ...DEFAULT_RETENTION },
74 priority: 50,
75 enabled: true,
76 ...overrides,
77 };
78}
79
80function makePolicySet(policies: Policy[]): PolicySet {
81 return { version: 1, policies };
82}
83
84// ============================================
85// DID list merging
86// ============================================
87
88describe("PolicyEngine + ReplicationManager: DID list merging", () => {
89 let tmpDir: string;
90 let db: InstanceType<typeof Database>;
91 let ipfsService: IpfsService;
92 let repoManager: RepoManager;
93 let didResolver: DidResolver;
94
95 beforeEach(async () => {
96 tmpDir = mkdtempSync(join(tmpdir(), "policy-integration-test-"));
97 db = new Database(join(tmpDir, "test.db"));
98 ipfsService = new IpfsService({
99 db,
100 networking: false,
101 });
102 await ipfsService.start();
103
104 const config = testConfig(tmpDir, []);
105 repoManager = new RepoManager(db, config);
106 repoManager.init(undefined, ipfsService, ipfsService);
107
108 didResolver = new DidResolver({
109 didCache: new InMemoryDidCache(),
110 });
111
112 // Ensure replication tables exist (needed for getReplicateDids which queries admin_tracked_dids)
113 const { SyncStorage } = await import("./sync-storage.js");
114 new SyncStorage(db).initSchema();
115 });
116
117 afterEach(async () => {
118 if (ipfsService.isRunning()) await ipfsService.stop();
119 db.close();
120 rmSync(tmpDir, { recursive: true, force: true });
121 });
122
123 it("without PolicyEngine, returns only config DIDs", () => {
124 const config = testConfig(tmpDir, ["did:plc:a", "did:plc:b"]);
125 const rm = new ReplicationManager(
126 db,
127 config,
128 repoManager,
129 ipfsService,
130 ipfsService,
131 didResolver,
132 );
133 expect(rm.getReplicateDids().sort()).toEqual([
134 "did:plc:a",
135 "did:plc:b",
136 ]);
137 });
138
139 it("with PolicyEngine, merges config and policy explicit DIDs", () => {
140 const config = testConfig(tmpDir, ["did:plc:config1"]);
141 const engine = new PolicyEngine();
142 // Simulate migration: config DID becomes a config: policy
143 engine.addPolicy(configArchive("did:plc:config1"));
144 engine.addPolicy(makePolicy({
145 id: "p1",
146 target: { type: "list", dids: ["did:plc:policy1", "did:plc:policy2"] },
147 }));
148
149 const rm = new ReplicationManager(
150 db,
151 config,
152 repoManager,
153 ipfsService,
154 ipfsService,
155 didResolver,
156 undefined,
157 undefined,
158 engine,
159 );
160
161 const dids = rm.getReplicateDids().sort();
162 expect(dids).toEqual([
163 "did:plc:config1",
164 "did:plc:policy1",
165 "did:plc:policy2",
166 ]);
167 });
168
169 it("deduplicates DIDs present in both config policy and other policy", () => {
170 const config = testConfig(tmpDir, ["did:plc:shared", "did:plc:config-only"]);
171 const engine = new PolicyEngine();
172 engine.addPolicy(configArchive("did:plc:shared"));
173 engine.addPolicy(configArchive("did:plc:config-only"));
174 engine.addPolicy(makePolicy({
175 id: "p1",
176 target: { type: "list", dids: ["did:plc:shared", "did:plc:policy-only"] },
177 }));
178
179 const rm = new ReplicationManager(
180 db,
181 config,
182 repoManager,
183 ipfsService,
184 ipfsService,
185 didResolver,
186 undefined,
187 undefined,
188 engine,
189 );
190
191 const dids = rm.getReplicateDids().sort();
192 expect(dids).toEqual([
193 "did:plc:config-only",
194 "did:plc:policy-only",
195 "did:plc:shared",
196 ]);
197 });
198
199 it("config: policies are always included via PolicyEngine", () => {
200 const config = testConfig(tmpDir, ["did:plc:config-only"]);
201 const engine = new PolicyEngine();
202 engine.addPolicy(configArchive("did:plc:config-only"));
203 engine.addPolicy(makePolicy({
204 id: "p1",
205 target: { type: "list", dids: ["did:plc:policy-only"] },
206 }));
207
208 const rm = new ReplicationManager(
209 db,
210 config,
211 repoManager,
212 ipfsService,
213 ipfsService,
214 didResolver,
215 undefined,
216 undefined,
217 engine,
218 );
219
220 const dids = rm.getReplicateDids();
221 expect(dids).toContain("did:plc:config-only");
222 expect(dids).toContain("did:plc:policy-only");
223 });
224
225 it("disabled policy DIDs are not included (unless in config)", () => {
226 const config = testConfig(tmpDir, []);
227 const engine = new PolicyEngine(
228 makePolicySet([
229 makePolicy({
230 id: "disabled",
231 target: { type: "list", dids: ["did:plc:disabled"] },
232 enabled: false,
233 }),
234 makePolicy({
235 id: "enabled",
236 target: { type: "list", dids: ["did:plc:enabled"] },
237 enabled: true,
238 }),
239 ]),
240 );
241
242 const rm = new ReplicationManager(
243 db,
244 config,
245 repoManager,
246 ipfsService,
247 ipfsService,
248 didResolver,
249 undefined,
250 undefined,
251 engine,
252 );
253
254 const dids = rm.getReplicateDids();
255 expect(dids).toContain("did:plc:enabled");
256 expect(dids).not.toContain("did:plc:disabled");
257 });
258
259 it("getPolicyEngine returns the engine when set", () => {
260 const config = testConfig(tmpDir, []);
261 const engine = new PolicyEngine();
262
263 const rm = new ReplicationManager(
264 db,
265 config,
266 repoManager,
267 ipfsService,
268 ipfsService,
269 didResolver,
270 undefined,
271 undefined,
272 engine,
273 );
274
275 expect(rm.getPolicyEngine()).toBe(engine);
276 });
277
278 it("getPolicyEngine returns null when not set", () => {
279 const config = testConfig(tmpDir, []);
280 const rm = new ReplicationManager(
281 db,
282 config,
283 repoManager,
284 ipfsService,
285 ipfsService,
286 didResolver,
287 );
288
289 expect(rm.getPolicyEngine()).toBeNull();
290 });
291});
292
293// ============================================
294// Per-DID sync intervals
295// ============================================
296
297describe("PolicyEngine + ReplicationManager: per-DID sync intervals", () => {
298 let tmpDir: string;
299 let db: InstanceType<typeof Database>;
300 let ipfsService: IpfsService;
301 let repoManager: RepoManager;
302 let didResolver: DidResolver;
303
304 beforeEach(async () => {
305 tmpDir = mkdtempSync(join(tmpdir(), "policy-interval-test-"));
306 db = new Database(join(tmpDir, "test.db"));
307 ipfsService = new IpfsService({
308 db,
309 networking: false,
310 });
311 await ipfsService.start();
312
313 const config = testConfig(tmpDir, []);
314 repoManager = new RepoManager(db, config);
315 repoManager.init(undefined, ipfsService, ipfsService);
316
317 didResolver = new DidResolver({
318 didCache: new InMemoryDidCache(),
319 });
320 });
321
322 afterEach(async () => {
323 if (ipfsService.isRunning()) await ipfsService.stop();
324 db.close();
325 rmSync(tmpDir, { recursive: true, force: true });
326 });
327
328 it("getEffectiveSyncIntervalMs returns default without policy engine", () => {
329 const config = testConfig(tmpDir, ["did:plc:a"]);
330 const rm = new ReplicationManager(
331 db,
332 config,
333 repoManager,
334 ipfsService,
335 ipfsService,
336 didResolver,
337 );
338
339 // Default is 5 minutes
340 expect(rm.getEffectiveSyncIntervalMs("did:plc:a")).toBe(5 * 60 * 1000);
341 });
342
343 it("getEffectiveSyncIntervalMs returns policy interval when engine is set", () => {
344 const config = testConfig(tmpDir, []);
345 const engine = new PolicyEngine(
346 makePolicySet([
347 makePolicy({
348 id: "fast",
349 target: { type: "list", dids: ["did:plc:fast"] },
350 sync: { intervalSec: 60 },
351 }),
352 makePolicy({
353 id: "slow",
354 target: { type: "list", dids: ["did:plc:slow"] },
355 sync: { intervalSec: 600 },
356 }),
357 ]),
358 );
359
360 const rm = new ReplicationManager(
361 db,
362 config,
363 repoManager,
364 ipfsService,
365 ipfsService,
366 didResolver,
367 undefined,
368 undefined,
369 engine,
370 );
371
372 expect(rm.getEffectiveSyncIntervalMs("did:plc:fast")).toBe(60 * 1000);
373 expect(rm.getEffectiveSyncIntervalMs("did:plc:slow")).toBe(600 * 1000);
374 });
375
376 it("getEffectiveSyncIntervalMs returns default for DIDs without matching policy", () => {
377 const config = testConfig(tmpDir, ["did:plc:no-policy"]);
378 const engine = new PolicyEngine(
379 makePolicySet([
380 makePolicy({
381 id: "targeted",
382 target: { type: "list", dids: ["did:plc:targeted"] },
383 sync: { intervalSec: 60 },
384 }),
385 ]),
386 );
387
388 const rm = new ReplicationManager(
389 db,
390 config,
391 repoManager,
392 ipfsService,
393 ipfsService,
394 didResolver,
395 undefined,
396 undefined,
397 engine,
398 );
399
400 // did:plc:no-policy has no matching policy, so gets default
401 expect(rm.getEffectiveSyncIntervalMs("did:plc:no-policy")).toBe(
402 5 * 60 * 1000,
403 );
404 });
405
406 it("merged policy intervals take the minimum (most frequent)", () => {
407 const config = testConfig(tmpDir, []);
408 const engine = new PolicyEngine(
409 makePolicySet([
410 makePolicy({
411 id: "slow",
412 target: { type: "all" },
413 sync: { intervalSec: 600 },
414 }),
415 makePolicy({
416 id: "fast",
417 target: { type: "list", dids: ["did:plc:fast"] },
418 sync: { intervalSec: 30 },
419 }),
420 ]),
421 );
422
423 const rm = new ReplicationManager(
424 db,
425 config,
426 repoManager,
427 ipfsService,
428 ipfsService,
429 didResolver,
430 undefined,
431 undefined,
432 engine,
433 );
434
435 // did:plc:fast matches both policies; minimum wins
436 expect(rm.getEffectiveSyncIntervalMs("did:plc:fast")).toBe(30 * 1000);
437 });
438});
439
440// ============================================
441// Priority ordering
442// ============================================
443
444describe("PolicyEngine + ReplicationManager: priority ordering", () => {
445 let tmpDir: string;
446 let db: InstanceType<typeof Database>;
447 let ipfsService: IpfsService;
448 let repoManager: RepoManager;
449 let didResolver: DidResolver;
450
451 beforeEach(async () => {
452 tmpDir = mkdtempSync(join(tmpdir(), "policy-priority-test-"));
453 db = new Database(join(tmpDir, "test.db"));
454 ipfsService = new IpfsService({
455 db,
456 networking: false,
457 });
458 await ipfsService.start();
459
460 const config = testConfig(tmpDir, []);
461 repoManager = new RepoManager(db, config);
462 repoManager.init(undefined, ipfsService, ipfsService);
463
464 didResolver = new DidResolver({
465 didCache: new InMemoryDidCache(),
466 });
467 });
468
469 afterEach(async () => {
470 if (ipfsService.isRunning()) await ipfsService.stop();
471 db.close();
472 rmSync(tmpDir, { recursive: true, force: true });
473 });
474
475 it("syncAll processes higher-priority DIDs first", async () => {
476 const config = testConfig(tmpDir, []);
477 const engine = new PolicyEngine(
478 makePolicySet([
479 makePolicy({
480 id: "low-pri",
481 target: { type: "list", dids: ["did:plc:low"] },
482 priority: 10,
483 }),
484 makePolicy({
485 id: "high-pri",
486 target: { type: "list", dids: ["did:plc:high"] },
487 priority: 90,
488 }),
489 makePolicy({
490 id: "mid-pri",
491 target: { type: "list", dids: ["did:plc:mid"] },
492 priority: 50,
493 }),
494 ]),
495 );
496
497 const rm = new ReplicationManager(
498 db,
499 config,
500 repoManager,
501 ipfsService,
502 ipfsService,
503 didResolver,
504 undefined,
505 undefined,
506 engine,
507 );
508
509 // Track the order in which syncDid is called
510 const syncOrder: string[] = [];
511 const origSyncDid = rm.syncDid.bind(rm);
512 rm.syncDid = async (did: string) => {
513 syncOrder.push(did);
514 // Don't actually sync (would fail without real PDS)
515 };
516
517 rm.getSyncStorage().initSchema();
518 await rm.syncAll();
519
520 expect(syncOrder).toEqual([
521 "did:plc:high",
522 "did:plc:mid",
523 "did:plc:low",
524 ]);
525 });
526
527 it("without policy engine, DIDs are synced in config order", async () => {
528 const config = testConfig(tmpDir, [
529 "did:plc:first",
530 "did:plc:second",
531 "did:plc:third",
532 ]);
533
534 const rm = new ReplicationManager(
535 db,
536 config,
537 repoManager,
538 ipfsService,
539 ipfsService,
540 didResolver,
541 );
542
543 const syncOrder: string[] = [];
544 rm.syncDid = async (did: string) => {
545 syncOrder.push(did);
546 };
547
548 rm.getSyncStorage().initSchema();
549 await rm.syncAll();
550
551 expect(syncOrder).toEqual([
552 "did:plc:first",
553 "did:plc:second",
554 "did:plc:third",
555 ]);
556 });
557});
558
559// ============================================
560// shouldReplicate filtering
561// ============================================
562
563describe("PolicyEngine + ReplicationManager: shouldReplicate filtering", () => {
564 let tmpDir: string;
565 let db: InstanceType<typeof Database>;
566 let ipfsService: IpfsService;
567 let repoManager: RepoManager;
568 let didResolver: DidResolver;
569
570 beforeEach(async () => {
571 tmpDir = mkdtempSync(join(tmpdir(), "policy-filter-test-"));
572 db = new Database(join(tmpDir, "test.db"));
573 ipfsService = new IpfsService({
574 db,
575 networking: false,
576 });
577 await ipfsService.start();
578
579 const config = testConfig(tmpDir, []);
580 repoManager = new RepoManager(db, config);
581 repoManager.init(undefined, ipfsService, ipfsService);
582
583 didResolver = new DidResolver({
584 didCache: new InMemoryDidCache(),
585 });
586 });
587
588 afterEach(async () => {
589 if (ipfsService.isRunning()) await ipfsService.stop();
590 db.close();
591 rmSync(tmpDir, { recursive: true, force: true });
592 });
593
594 it("policy-only DIDs with disabled policy are excluded from sync", async () => {
595 const config = testConfig(tmpDir, []);
596 const engine = new PolicyEngine(
597 makePolicySet([
598 makePolicy({
599 id: "enabled",
600 target: { type: "list", dids: ["did:plc:enabled"] },
601 enabled: true,
602 }),
603 // This disabled policy won't add did:plc:disabled to explicit DIDs
604 makePolicy({
605 id: "disabled",
606 target: { type: "list", dids: ["did:plc:disabled"] },
607 enabled: false,
608 }),
609 ]),
610 );
611
612 const rm = new ReplicationManager(
613 db,
614 config,
615 repoManager,
616 ipfsService,
617 ipfsService,
618 didResolver,
619 undefined,
620 undefined,
621 engine,
622 );
623
624 const syncedDids: string[] = [];
625 rm.syncDid = async (did: string) => {
626 syncedDids.push(did);
627 };
628
629 rm.getSyncStorage().initSchema();
630 await rm.syncAll();
631
632 expect(syncedDids).toContain("did:plc:enabled");
633 expect(syncedDids).not.toContain("did:plc:disabled");
634 });
635
636 it("per-DID interval skips DIDs not yet due", async () => {
637 const config = testConfig(tmpDir, []);
638 const engine = new PolicyEngine(
639 makePolicySet([
640 makePolicy({
641 id: "fast",
642 target: { type: "list", dids: ["did:plc:fast"] },
643 sync: { intervalSec: 60 },
644 }),
645 makePolicy({
646 id: "slow",
647 target: { type: "list", dids: ["did:plc:slow"] },
648 sync: { intervalSec: 3600 },
649 }),
650 ]),
651 );
652
653 const rm = new ReplicationManager(
654 db,
655 config,
656 repoManager,
657 ipfsService,
658 ipfsService,
659 didResolver,
660 undefined,
661 undefined,
662 engine,
663 );
664
665 const syncedDids: string[] = [];
666 rm.syncDid = async (did: string) => {
667 syncedDids.push(did);
668 };
669
670 rm.getSyncStorage().initSchema();
671
672 // First sync: both should sync (never synced before)
673 await rm.syncAll();
674 expect(syncedDids).toContain("did:plc:fast");
675 expect(syncedDids).toContain("did:plc:slow");
676
677 // Reset tracking
678 syncedDids.length = 0;
679
680 // Second sync immediately: neither should sync (not enough time passed)
681 await rm.syncAll();
682 expect(syncedDids).toEqual([]);
683 });
684});
685
686// ============================================
687// Preset integration
688// ============================================
689
690describe("PolicyEngine + ReplicationManager: presets", () => {
691 let tmpDir: string;
692 let db: InstanceType<typeof Database>;
693 let ipfsService: IpfsService;
694 let repoManager: RepoManager;
695 let didResolver: DidResolver;
696
697 beforeEach(async () => {
698 tmpDir = mkdtempSync(join(tmpdir(), "policy-preset-test-"));
699 db = new Database(join(tmpDir, "test.db"));
700 ipfsService = new IpfsService({
701 db,
702 networking: false,
703 });
704 await ipfsService.start();
705
706 const config = testConfig(tmpDir, []);
707 repoManager = new RepoManager(db, config);
708 repoManager.init(undefined, ipfsService, ipfsService);
709
710 didResolver = new DidResolver({
711 didCache: new InMemoryDidCache(),
712 });
713
714 const { SyncStorage } = await import("./sync-storage.js");
715 new SyncStorage(db).initSchema();
716 });
717
718 afterEach(async () => {
719 if (ipfsService.isRunning()) await ipfsService.stop();
720 db.close();
721 rmSync(tmpDir, { recursive: true, force: true });
722 });
723
724 it("mutualAid preset drives DID list and sync interval", () => {
725 const config = testConfig(tmpDir, []);
726 const engine = new PolicyEngine();
727 engine.addPolicy(
728 mutualAid({
729 peerDids: ["did:plc:alice", "did:plc:bob", "did:plc:carol"],
730 intervalSec: 600,
731 }),
732 );
733
734 const rm = new ReplicationManager(
735 db,
736 config,
737 repoManager,
738 ipfsService,
739 ipfsService,
740 didResolver,
741 undefined,
742 undefined,
743 engine,
744 );
745
746 const dids = rm.getReplicateDids().sort();
747 expect(dids).toEqual([
748 "did:plc:alice",
749 "did:plc:bob",
750 "did:plc:carol",
751 ]);
752
753 for (const did of dids) {
754 expect(rm.getEffectiveSyncIntervalMs(did)).toBe(600 * 1000);
755 }
756 });
757
758 it("saas preset gets higher priority than mutualAid", async () => {
759 const config = testConfig(tmpDir, []);
760 const engine = new PolicyEngine();
761 engine.addPolicy(
762 mutualAid({
763 id: "aid",
764 peerDids: ["did:plc:peer1", "did:plc:peer2"],
765 }),
766 );
767 engine.addPolicy(
768 saas({
769 id: "sla",
770 accountDids: ["did:plc:customer"],
771 }),
772 );
773
774 const rm = new ReplicationManager(
775 db,
776 config,
777 repoManager,
778 ipfsService,
779 ipfsService,
780 didResolver,
781 undefined,
782 undefined,
783 engine,
784 );
785
786 // SaaS has priority 80, mutualAid has priority 50
787 const syncOrder: string[] = [];
788 rm.syncDid = async (did: string) => {
789 syncOrder.push(did);
790 };
791
792 rm.getSyncStorage().initSchema();
793 await rm.syncAll();
794
795 // Customer (priority 80) should be synced before peers (priority 50)
796 const customerIdx = syncOrder.indexOf("did:plc:customer");
797 const peer1Idx = syncOrder.indexOf("did:plc:peer1");
798 const peer2Idx = syncOrder.indexOf("did:plc:peer2");
799
800 expect(customerIdx).toBeLessThan(peer1Idx);
801 expect(customerIdx).toBeLessThan(peer2Idx);
802 });
803
804 it("saas preset uses shorter sync interval than mutualAid", () => {
805 const config = testConfig(tmpDir, []);
806 const engine = new PolicyEngine();
807 engine.addPolicy(
808 mutualAid({
809 id: "aid",
810 peerDids: ["did:plc:peer1"],
811 }),
812 );
813 engine.addPolicy(
814 saas({
815 id: "sla",
816 accountDids: ["did:plc:customer"],
817 }),
818 );
819
820 const rm = new ReplicationManager(
821 db,
822 config,
823 repoManager,
824 ipfsService,
825 ipfsService,
826 didResolver,
827 undefined,
828 undefined,
829 engine,
830 );
831
832 // SaaS: 60s, mutualAid: 600s
833 expect(rm.getEffectiveSyncIntervalMs("did:plc:customer")).toBe(
834 60 * 1000,
835 );
836 expect(rm.getEffectiveSyncIntervalMs("did:plc:peer1")).toBe(
837 600 * 1000,
838 );
839 });
840});
841
842// ============================================
843// Backward compatibility
844// ============================================
845
846describe("PolicyEngine + ReplicationManager: backward compatibility", () => {
847 let tmpDir: string;
848 let db: InstanceType<typeof Database>;
849 let ipfsService: IpfsService;
850 let repoManager: RepoManager;
851 let didResolver: DidResolver;
852
853 beforeEach(async () => {
854 tmpDir = mkdtempSync(join(tmpdir(), "policy-compat-test-"));
855 db = new Database(join(tmpDir, "test.db"));
856 ipfsService = new IpfsService({
857 db,
858 networking: false,
859 });
860 await ipfsService.start();
861
862 const config = testConfig(tmpDir, []);
863 repoManager = new RepoManager(db, config);
864 repoManager.init(undefined, ipfsService, ipfsService);
865
866 didResolver = new DidResolver({
867 didCache: new InMemoryDidCache(),
868 });
869
870 const { SyncStorage } = await import("./sync-storage.js");
871 new SyncStorage(db).initSchema();
872 });
873
874 afterEach(async () => {
875 if (ipfsService.isRunning()) await ipfsService.stop();
876 db.close();
877 rmSync(tmpDir, { recursive: true, force: true });
878 });
879
880 it("without PolicyEngine, syncAll syncs all config DIDs every tick", async () => {
881 const config = testConfig(tmpDir, ["did:plc:a", "did:plc:b"]);
882 const rm = new ReplicationManager(
883 db,
884 config,
885 repoManager,
886 ipfsService,
887 ipfsService,
888 didResolver,
889 );
890
891 const syncedDids: string[] = [];
892 rm.syncDid = async (did: string) => {
893 syncedDids.push(did);
894 };
895
896 rm.getSyncStorage().initSchema();
897
898 // First sync
899 await rm.syncAll();
900 expect(syncedDids.sort()).toEqual(["did:plc:a", "did:plc:b"]);
901
902 // Second sync (should still sync all, no per-DID interval tracking)
903 syncedDids.length = 0;
904 await rm.syncAll();
905 expect(syncedDids.sort()).toEqual(["did:plc:a", "did:plc:b"]);
906 });
907
908 it("constructor works without policyEngine parameter", () => {
909 const config = testConfig(tmpDir, ["did:plc:a"]);
910 const rm = new ReplicationManager(
911 db,
912 config,
913 repoManager,
914 ipfsService,
915 ipfsService,
916 didResolver,
917 );
918
919 expect(rm.getPolicyEngine()).toBeNull();
920 expect(rm.getReplicateDids()).toEqual(["did:plc:a"]);
921 });
922});