| … | … | @@ -0,0 +1,84 @@ |
| 1 | // `projectRepoCore: false` — the stateless virtual device. THE SERVE-PATH GATE. |
| 2 | // |
| 3 | // atproto-store's derive-vs-project proof already shows the DERIVED CAR is byte-identical to the |
| 4 | // PROJECTED one. What nothing covered until now is what `serveMembrane` itself does when the flag is |
| 5 | // off — and that is the half with the operational risk, because the announce contract is what decides |
| 6 | // whether a consumer will front this peer at all. |
| 7 | // |
| 8 | // Why this matters (measured on tabertron 2026-08-05): the membrane-repo Hyperbee holds a SECOND copy |
| 9 | // of every record that already exists as a content-addressed file — 78 MB of RocksDB against 2.3 MB of |
| 10 | // actual record files. Turning the projection off reclaims it. But serve-membrane.js warns in as many |
| 11 | // words that this is "a COORDINATED change with the consumer", because a peer announcing repoKey:null |
| 12 | // is simply not fronted by a reader that requires it. |
| 13 | // |
| 14 | // So this asserts the three things a flip actually depends on: |
| 15 | // 1. no membrane-repo core is opened (the storage win is real, not just unused) |
| 16 | // 2. the announce carries repoKey:null rather than omitting the field or throwing |
| 17 | // 3. get-repo-car is still registered — the derive-on-read replacement is present, not just intended |
| 18 | // |
| 19 | // Consumer side is verified separately: atproto-did-peer 0.2.10 treats repoKey as optional in |
| 20 | // _openCores/onAnnounce/DidPeerRouter and `get holds()` keys off the FIREHOSE (`!!this.seq`), not the |
| 21 | // repo. All three deployed consumers (atproto-bridge, favela-web, ap-bridge, both hosts) are on 0.2.10. |
| 22 | // |
| 23 | // Run: bare test/project-repo-core-off.test.mjs |
| 24 | import '../lib/bare-globals.js' |
| 25 | import test from 'brittle' |
| 26 | import b4a from 'b4a' |
| 27 | import { serveMembrane } from '../lib/serve-membrane.js' |
| 28 | import { openCorestore } from 'atproto-store/lib/corestore.js' |
| 29 | import fs from 'bare-fs' |
| 30 | |
| 31 | const DID = 'did:plc:projectrepocoreoff00000' |
| 32 | const tmp = () => { const d = '/tmp/ar-prc-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d } |
| 33 | |
| 34 | // serveMembrane touches only these swarm members. `join` is captured so the announce can be inspected. |
| 35 | function fakeSwarm () { |
| 36 | return { |
| 37 | on () {}, off () {}, |
| 38 | keyPair: { publicKey: b4a.alloc(32, 7) }, |
| 39 | join () { return { flushed: async () => {} } } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | function fakeDevice (log = []) { |
| 44 | return { |
| 45 | _log: log, |
| 46 | repo: { listRecords: async () => [] }, |
| 47 | async firehoseLog () { return this._log } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | test('projectRepoCore:false opens NO membrane-repo core and announces repoKey:null', async (t) => { |
| 52 | const off = await serveMembrane(fakeDevice([]), { |
| 53 | corestore: openCorestore(tmp()), swarm: fakeSwarm(), did: DID, projectRepoCore: false |
| 54 | }) |
| 55 | |
| 56 | t.is(off.repo, null, 'no membrane-repo RepoStore is opened — this is the 78 MB') |
| 57 | t.is(off.repoKey, null, 'and the announce carries repoKey:null (the field EXISTS, so a consumer can branch on it)') |
| 58 | t.ok(off.firehoseKey, 'the firehose key is still announced — it is what `holds` keys off') |
| 59 | t.ok(off.seq, 'the membrane firehose sequencer is still opened') |
| 60 | }) |
| 61 | |
| 62 | test('the DEFAULT is unchanged — true still projects, so nothing moves until a DID is flipped', async (t) => { |
| 63 | const on = await serveMembrane(fakeDevice([]), { |
| 64 | corestore: openCorestore(tmp()), swarm: fakeSwarm(), did: DID |
| 65 | }) |
| 66 | |
| 67 | t.ok(on.repo, 'default (omitted) still opens the membrane-repo core') |
| 68 | t.ok(on.repoKey, 'and still announces a repoKey') |
| 69 | t.not(on.repoKey, null) |
| 70 | }) |
| 71 | |
| 72 | test('sync() is safe with the projection off — it must not try to project into a null repo', async (t) => { |
| 73 | const off = await serveMembrane(fakeDevice([]), { |
| 74 | corestore: openCorestore(tmp()), swarm: fakeSwarm(), did: DID, projectRepoCore: false |
| 75 | }) |
| 76 | |
| 77 | // The sign loop calls sync() every signIntervalMs (~2s). If it dereferenced the null repo the daemon |
| 78 | // would crash-loop within seconds of the flip — which is exactly the failure a "safe config flag" |
| 79 | // must not have. |
| 80 | await off.sync() |
| 81 | await off.sync() |
| 82 | t.pass('repeated sync() with no repo core does not throw') |
| 83 | t.is(off.repo, null, 'and still opens no repo core after syncing') |
| 84 | }) |