OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room commits / 2917f55

test: gate the projectRepoCore:false serve path
atproto-store's derive-vs-project proof covers the DERIVATION (the derived CAR is byte-identical to the projected one). Nothing covered what serveMembrane itself does when the flag is off, which is the half with the operational risk: serve-membrane.js warns that flipping it is "a COORDINATED change with the consumer", because a peer announcing repoKey:null is not fronted by a reader that requires it. Asserts the three things a flip depends on: 1. no membrane-repo core is opened (the storage win is real) 2. the announce carries repoKey:null — the field EXISTS so a consumer can branch on it, rather than being omitted or throwing 3. the default (omitted) still projects, so nothing moves until a DID is explicitly flipped Plus sync() safety: the sign loop calls it every ~2s, so dereferencing the null repo would crash-loop the daemon within seconds of a flip. Motivation, measured on tabertron 2026-08-05: the membrane-repo Hyperbee holds a second copy of every record that already exists as a content-addressed file — 78 MB of RocksDB against 2.3 MB of record files. Confirmed by grepping one post's text across every store: it appears in atproto-repo/ (canonical), membrane/db (this projection), and favela-web's corestore (its replicated firehose, which is protocol-inherent). Notably hpp's own store/ (157 MB) holds ZERO copies — PointerSubstrate is doing exactly what it claims. Consumer side verified separately, not by this test: atproto-did-peer 0.2.10 makes repoKey optional in _openCores/onAnnounce/DidPeerRouter and `get holds()` keys off the FIREHOSE (!!this.seq). All deployed consumers on both hosts are on 0.2.10, and the PeerFront references left in the bridge's live files are comments, not usage. 57/57 tests, 214/214 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2917f55605e39dfe83c572be6229dda8e9229d3f
by plankatron <fleeky@gmail.com> · 2026-08-05T16:46:15.000Z

Changed files (2) +85 -0

test/all.js +1 -0
……@@ -5,6 +5,7 @@
55import '../lib/bare-globals.js'
66import './repo-base.test.js'
77import './membrane-stale-gate.test.mjs'
8import './project-repo-core-off.test.mjs'
89import './membrane-projection-drift.test.mjs'
910import './lease.test.js'
1011import './lease-handoff.test.js'
test/project-repo-core-off.test.mjs +84 -0
……@@ -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
24import '../lib/bare-globals.js'
25import test from 'brittle'
26import b4a from 'b4a'
27import { serveMembrane } from '../lib/serve-membrane.js'
28import { openCorestore } from 'atproto-store/lib/corestore.js'
29import fs from 'bare-fs'
30
31const DID = 'did:plc:projectrepocoreoff00000'
32const 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.
35function fakeSwarm () {
36 return {
37 on () {}, off () {},
38 keyPair: { publicKey: b4a.alloc(32, 7) },
39 join () { return { flushed: async () => {} } }
40 }
41}
42
43function fakeDevice (log = []) {
44 return {
45 _log: log,
46 repo: { listRecords: async () => [] },
47 async firehoseLog () { return this._log }
48 }
49}
50
51test('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
62test('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
72test('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})