atproto user agency toolkit for individuals and groups
at main 922 lines 22 kB view raw
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});