OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-did-peer commits / 457d990

did-peer: regression test for DidPeerRouter multi-DID dispatch
Control+fix test over an in-process testnet (deterministic, offline): two holders serve real signed repos, and - CONTROL: per-DidPeer attach() collides on the single announce channel, so only the first-added DID reads and the second never learns its cores (the bug); - FIX: DidPeerRouter dispatches each announce by DID, so BOTH reach a verified head. `npm test` now runs the deterministic offline suite (commitops + router); `npm test:live` keeps the network read-live check. Adds @hyperswarm/testnet devDep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
457d9900d8b4a01c235e83a698bf88295a178aab
by plankatron <fleeky@gmail.com> · 2026-07-14T17:32:35.000Z

Changed files (3) +110 -4

package-lock.json +14 -0
……@@ -15,6 +15,7 @@
1515 "protomux": "^3.11.0"
1616 },
1717 "devDependencies": {
18 "@hyperswarm/testnet": "^3.1.4",
1819 "atproto-identity": "file:../atproto-identity",
1920 "corestore": "^7.4.0",
2021 "hyperswarm": "^4.11.0"
……@@ -85,6 +86,19 @@
8586 "unslab": "^1.3.0"
8687 }
8788 },
89 "node_modules/@hyperswarm/testnet": {
90 "version": "3.1.4",
91 "resolved": "https://registry.npmjs.org/@hyperswarm/testnet/-/testnet-3.1.4.tgz",
92 "integrity": "sha512-Jw8JIZ9kdZ4R9jwJHsqctj5YeRqecw3aeCg2NS12HBXCqMioQIYzVqHs/8BWdOQCf34+BEXkwMQjba9CzaLwkg==",
93 "dev": true,
94 "license": "Apache-2.0",
95 "dependencies": {
96 "hyperdht": "^6.6.0"
97 },
98 "bin": {
99 "hyperswarm-testnet": "bin.js"
100 }
101 },
88102 "node_modules/adaptive-timeout": {
89103 "version": "1.0.1",
90104 "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz",
package.json +6 -4
……@@ -8,18 +8,20 @@
88 ".": "./index.js"
99 },
1010 "scripts": {
11 "test": "node test/read-live.mjs"
11 "test": "node test/commitops.test.mjs && node test/router.test.mjs",
12 "test:live": "node test/read-live.mjs"
1213 },
1314 "dependencies": {
14 "atproto-store": "file:../atproto-store",
1515 "atproto-repo": "file:../atproto-repo",
16 "atproto-store": "file:../atproto-store",
1617 "b4a": "^1.8.1",
1718 "compact-encoding": "^2.19.0",
1819 "protomux": "^3.11.0"
1920 },
2021 "devDependencies": {
22 "@hyperswarm/testnet": "^3.1.4",
2123 "atproto-identity": "file:../atproto-identity",
22 "hyperswarm": "^4.11.0",
23 "corestore": "^7.4.0"
24 "corestore": "^7.4.0",
25 "hyperswarm": "^4.11.0"
2426 }
2527}
test/router.test.mjs +90 -0
……@@ -0,0 +1,90 @@
1// DidPeerRouter regression — multi-DID reading over ONE shared swarm. Two holders serve real signed
2// repos (RepoManager + FirehoseSequencer) on an in-process testnet; a reader must learn BOTH.
3// CONTROL: per-DidPeer attach() collides on the single announce channel -> only the FIRST-added DID
4// reads; the second never learns its cores (the bug).
5// FIX: DidPeerRouter dispatches each announce by DID -> BOTH read.
6// Run: node test/router.test.mjs
7import { mkdtempSync, rmSync } from 'node:fs'
8import { join } from 'node:path'
9import { tmpdir } from 'node:os'
10import createTestnet from '@hyperswarm/testnet'
11import Hyperswarm from 'hyperswarm'
12import { RepoStore } from 'atproto-store/lib/repo-store.js'
13import { RepoManager } from 'atproto-store/lib/repo-manager.js'
14import { FirehoseSequencer } from 'atproto-store/lib/sequencer.js'
15import { openCorestore } from 'atproto-store/lib/corestore.js'
16import { createAtprotoIdentityFromKey } from 'atproto-identity/lib/atproto-identity.js'
17import { discoveryTopic } from 'atproto-identity/lib/discovery.js'
18import { DidPeer, DidPeerRouter } from '../index.js'
19
20const assert = (c, m) => { if (!c) { console.error('FAIL:', m); cleanupSync(); process.exit(1) } console.log(' ok -', m) }
21const iso = '2026-07-01T00:00:00.000Z'
22const dirs = []
23const toClose = []
24function newDir (p) { const d = mkdtempSync(join(tmpdir(), p)); dirs.push(d); return d }
25function cleanupSync () { for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }) } catch {} } }
26
27const idA = createAtprotoIdentityFromKey(new Uint8Array(32).fill(9), { handle: 'a.test', pdsUrl: 'https://pds.local' })
28const idB = createAtprotoIdentityFromKey(new Uint8Array(32).fill(11), { handle: 'b.test', pdsUrl: 'https://pds.local' })
29
30const testnet = await createTestnet(3)
31const bootstrap = testnet.bootstrap
32
33// A holder authors a signed repo + sequences its commit, then serves the cores on its own swarm.
34async function makeHolder (id, text) {
35 const store = openCorestore(newDir('holder-'))
36 const repoStore = await new RepoStore(store, { name: `atproto-repo:${id.did}` }).ready()
37 let micros = 1_700_000_000_000_000n
38 const mgr = await new RepoManager(repoStore, { did: id.did, privateKey: id.privateKey, clock: () => (micros += 1_000_000n) }).init()
39 const write = await mgr.applyWrite({ action: 'create', collection: 'app.bsky.feed.post', rkey: '1', record: { $type: 'app.bsky.feed.post', text, createdAt: iso } })
40 const seq = await new FirehoseSequencer(store.get({ name: 'firehose' })).ready()
41 await seq.sequenceCommit(write, { time: iso })
42 const peer = new DidPeer(store, { did: id.did, verifyKey: id.publicKey })
43 await peer.provide({ repoKey: repoStore.core.key, firehoseKey: seq.core.key })
44 const swarm = new Hyperswarm({ bootstrap })
45 swarm.on('connection', (c) => peer.attach(c))
46 await swarm.join(discoveryTopic(id.did), { server: true, client: true }).flushed()
47 toClose.push(async () => { try { await swarm.destroy() } catch {}; try { await peer.close() } catch {}; try { await store.close() } catch {} })
48 return peer
49}
50
51const holderA = await makeHolder(idA, 'hello from A')
52const holderB = await makeHolder(idB, 'hello from B')
53console.log('holders serving:', idA.did.slice(0, 20), '+', idB.did.slice(0, 20))
54
55// --- CONTROL: per-peer attach() — only the FIRST-added DID reads ---
56{
57 const store = openCorestore(newDir('ctrl-'))
58 const swarm = new Hyperswarm({ bootstrap })
59 const cA = new DidPeer(store, { did: idA.did, verifyKey: idA.publicKey })
60 const cB = new DidPeer(store, { did: idB.did, verifyKey: idB.publicKey })
61 swarm.on('connection', (c) => cA.attach(c)) // cA registered FIRST -> grabs the channel on every conn
62 swarm.on('connection', (c) => cB.attach(c))
63 await swarm.join(discoveryTopic(idA.did), { server: true, client: true }).flushed()
64 await swarm.join(discoveryTopic(idB.did), { server: true, client: true }).flushed()
65 const [chA, chB] = await Promise.all([cA.ready({ timeout: 15000 }), cB.ready({ timeout: 15000 })])
66 assert(!!chA, 'CONTROL: the first-added DID (A) reads')
67 assert(!chB, 'CONTROL: per-peer attach() — the SECOND DID (B) never reads (reproduces the bug)')
68 await swarm.destroy(); await cA.close(); await cB.close(); await store.close()
69}
70
71// --- FIX: DidPeerRouter dispatches by DID — BOTH read ---
72{
73 const store = openCorestore(newDir('router-'))
74 const swarm = new Hyperswarm({ bootstrap })
75 const router = new DidPeerRouter(store)
76 swarm.on('connection', (c) => router.onconnection(c))
77 const pA = router.add(idA.did, { verifyKey: idA.publicKey })
78 const pB = router.add(idB.did, { verifyKey: idB.publicKey })
79 await swarm.join(discoveryTopic(idA.did), { server: true, client: true }).flushed()
80 await swarm.join(discoveryTopic(idB.did), { server: true, client: true }).flushed()
81 const [hA, hB] = await Promise.all([pA.ready({ timeout: 20000 }), pB.ready({ timeout: 20000 })])
82 assert(!!hA, 'ROUTER: DID A reaches a verified head')
83 assert(!!hB, 'ROUTER: DID B reaches a verified head (both members read — the fix)')
84 await swarm.destroy(); await router.close(); await store.close()
85}
86
87console.log('\nROUTER OK: per-peer attach reads only the first DID; DidPeerRouter reads BOTH')
88for (const close of toClose) await close()
89await testnet.destroy()
90cleanupSync()