/** * Integration suite for `HybridDiscoveryManager`. * * Per the spec (Phase 6 of the proposal): exercises the orchestrator * against real `DiscoveryStrategy` implementations (the in-package * `TestStrategy` real fixture under `tests/setup/test-strategy.ts`), * NOT mocks. NO `vi.mock` / `jest.mock` / `__mocks__` anywhere in this * suite. The five shipped Wave-2 sibling discovery packages are * dependency-graph imported via `dep-graph-siblings.test.ts` (separate * file) to prove they are reachable as optional peer-deps — but the * orchestrator-level behaviour assertions in THIS file run against the * faster, deterministic `TestStrategy` so the suite is hermetic. * * Scenarios covered: * 1. Register all five Wave-2 sibling IDs + verify peer dedup with * `PeerSource` correctness. * 2. Degrade one strategy and verify fallback to next-healthiest. * 3. Lifecycle (start all → stop → restart) idempotency. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import { HybridDiscoveryManager } from "../src/index.js"; import { makePeer, TestStrategy } from "./setup/test-strategy.js"; describe("HybridDiscoveryManager — integration against real strategies", () => { it("registers all five Wave-2 sibling ids and dedupes with PeerSource provenance", { timeout: 10_000, }, async () => { // Shared peer "alice" is observed by 3 strategies; "bob" by 2; // "charlie" by 1; each strategy also contributes a unique peer. const sharedAlice = makePeer("alice", "shared"); const sharedBob = makePeer("bob", "shared"); const peerJs = new TestStrategy({ name: "peerjs", peers: [sharedAlice, makePeer("uniq-peerjs", "peerjs")], }); const trystero = new TestStrategy({ name: "trystero", peers: [ sharedAlice, sharedBob, makePeer("uniq-trystero", "trystero"), ], }); const crdt = new TestStrategy({ name: "crdt", peers: [sharedAlice, makePeer("uniq-crdt", "crdt")], }); const webtorrent = new TestStrategy({ name: "webtorrent", peers: [sharedBob, makePeer("uniq-webtorrent", "webtorrent")], }); const gundb = new TestStrategy({ name: "gundb", peers: [ makePeer("charlie", "gundb"), makePeer("uniq-gundb", "gundb"), ], }); const m = new HybridDiscoveryManager({ strategy: "hybrid", strategies: [ { id: "peerjs", priority: "primary", strategy: peerJs }, { id: "trystero", priority: "primary", strategy: trystero }, { id: "crdt", priority: "fallback", strategy: crdt }, { id: "webtorrent", priority: "fallback", strategy: webtorrent, }, { id: "gundb", priority: "backup", strategy: gundb }, ], }); await m.start(); const peers = await m.discover(2000); // Unique peer ids: alice, bob, charlie, uniq-peerjs, uniq-trystero, // uniq-crdt, uniq-webtorrent, uniq-gundb = 8. expect(peers).toHaveLength(8); const status = m.getStatus(); // PeerSource provenance: alice → 3 strategies; bob → 2; charlie → 1 // Cross-check via the per-strategy peer counts. expect(status.peersByStrategy.peerjs).toBe(2); expect(status.peersByStrategy.trystero).toBe(3); expect(status.peersByStrategy.crdt).toBe(2); expect(status.peersByStrategy.webtorrent).toBe(2); expect(status.peersByStrategy.gundb).toBe(2); expect(status.totalPeers).toBe(8); expect(status.activeStrategies).toEqual([ "peerjs", "trystero", "crdt", "webtorrent", "gundb", ]); await m.stop(); }); it("degrades one strategy and falls back to the next healthiest", { timeout: 10_000, }, async () => { const primary = new TestStrategy({ name: "peerjs", peers: [makePeer("a", "peerjs")], }); // Initially-failing factory that the fallback path will retry. let factoryAttempts = 0; const m = new HybridDiscoveryManager({ strategy: "hybrid", autoFallback: true, strategies: [ { id: "peerjs", priority: "primary", strategy: primary }, { id: "trystero", priority: "fallback", factory: async () => { factoryAttempts += 1; if (factoryAttempts === 1) { // Simulate "signaling broken" on first attempt; // the orchestrator records UNHEALTHY and treats // it as dormant. NO mocks. throw new Error("signaling unreachable"); } return new TestStrategy({ name: "trystero", peers: [makePeer("b", "trystero")], }); }, }, ], }); await m.start(); // Engineered degradation: cut the primary's signal. NO mocks. primary.setAvailable(false); // Drive the health-check directly so the suite stays under 10 s. await ( m as unknown as { strategyManager: { runHealthCheck(): Promise }; } ).strategyManager.runHealthCheck(); const status = m.getStatus(); const trysteroHealth = status.strategies.find( (s) => s.id === "trystero" ); expect(trysteroHealth?.active).toBe(true); expect(factoryAttempts).toBe(2); await m.stop(); }); it("lifecycle: start → stop → restart is idempotent + safe", { timeout: 10_000, }, async () => { const s1 = new TestStrategy({ name: "peerjs", peers: [makePeer("a", "peerjs")], }); const s2 = new TestStrategy({ name: "trystero", peers: [makePeer("b", "trystero")], }); const m = new HybridDiscoveryManager({ strategy: "hybrid", strategies: [ { id: "peerjs", priority: "primary", strategy: s1 }, { id: "trystero", priority: "fallback", strategy: s2 }, ], }); await m.start(); let peers = await m.discover(1000); expect(peers).toHaveLength(2); await m.stop(); expect(await m.isAvailable()).toBe(false); // Restart — verify everything is functional again. await m.start(); peers = await m.discover(1000); expect(peers).toHaveLength(2); expect(s1.startedCount).toBe(2); expect(s2.startedCount).toBe(2); await m.stop(); }); it("shipped Wave-2 sibling packages are importable as optional peer-deps", { timeout: 10_000, }, async () => { // Pure dep-graph reachability check — verifies that the five // sibling packages are present in the workspace and that the // hybrid orchestrator can dynamically load them via the factory // path. We import the package's class symbols and assert they // exist; we do NOT instantiate (each has transport-specific // construction requirements that are exercised in the sibling's // own test suite). const peerjs = await import("@suaveplan/network-discovery-peerjs"); const crdt = await import("@suaveplan/network-discovery-crdt"); const trystero = await import("@suaveplan/network-discovery-trystero"); const webtorrent = await import( "@suaveplan/network-discovery-webtorrent" ); const gundb = await import("@suaveplan/network-discovery-gundb"); expect(peerjs.PeerJSDiscovery).toBeDefined(); expect(crdt.CRDTDiscovery).toBeDefined(); expect(trystero.TrysteroDiscovery).toBeDefined(); expect(webtorrent.WebTorrentDiscovery).toBeDefined(); expect(gundb.GunDBDiscovery).toBeDefined(); }); });