// REGRESSION: a DIRECT notary write must FINALIZE under the event-driven sign loop (no manual nudging).
//
// Bug (wj62 2026-07-21): an `apply-write` issued directly on the notary added the record but the signed
// head never advanced — get-repo-car failed (head-behind-records) and the post never reached the bridge/
// bsky. Co-owner writes (favela -> tabertron -> stortron) finalized fine. Root cause: sb.watch -> tick ->
// sign() is only re-fired from the autobase APPLY path (state-base.js _apply) and on presence changes;
// the autobase's own 'update' event (which fires when the BACKGROUND apply of THIS node's optimistic
// write completes) was NOT wired to the watcher fan-out. So a solo notary write had no event to trigger
// its own sign. Fix: state-base.js `_openBase` now does `base.on('update', () => this._fireWatchers())`.
//
// This test drives NO manual base.update() and calls sign() NEVER — purely event-driven, exactly like the
// production daemon — so it fails without the fix and passes with it.
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import b4a from 'b4a'
import createTestnet from '@hyperswarm/testnet'
import { bootMultiDevice, provisionDevice } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'
const M = { root: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', dev: 'legal winner thank year wave sausage worth useful legal winner thank yellow' }
const tmp = () => { const d = '/tmp/hpp-finlag-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: new Date().toISOString() } })
const has = (st, rk) => st.records.some(r => r.rkey === rk)
async function bundle (rootMnem, devMnem, handle) {
const dk = await deviceKeetKeyPair(devMnem)
const b = await provisionDevice({ rootMnemonic: rootMnem, devicePublicKey: dk.publicKey, handle, pdsUrl: 'https://pds.local' })
return { entry: { deviceMnemonic: devMnem, did: b.did, signingKey: b.signingKey, deviceProof: b.deviceProof, rootIdentityKey: b.rootIdentityKey, handle }, keetPub: b4a.toString(dk.publicKey, 'hex'), did: b.did }
}
test('a direct notary write FINALIZES event-driven (no manual base.update / no manual sign)', { timeout: 180000 }, async (t) => {
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const s1 = tmp()
const D = await bundle(M.root, M.dev, 'wj.local')
D.entry.members = [{ key: D.keetPub, role: 1 }]
const n1 = await bootMultiDevice({ storage: s1, dids: [D.entry], swarmOpts, signIntervalMs: 1000 })
const node = n1.byDid.get(D.did)
for (let i = 0; i < 60 && !node.sb.base.writable; i++) await sleep(500)
t.ok(node.sb.base.writable, 'notary is writable')
let base = null
for (let i = 0; i < 60; i++) { const s = await node.device.state(); if (s.head) { base = s.head.rev; break } await sleep(500) }
t.ok(base, 'notary authored an initial head=' + base)
// Purely event-driven: apply the write, then only READ (state()/firehoseLog are non-blocking). Never
// drive base.update(), never call sign(). This is exactly how the prod daemon behaves.
await node.device.applyWrite(post('r-direct', 'direct notary write — event-driven only'))
const t0 = Date.now()
let ms = null
for (let i = 0; i < 40; i++) { // up to ~20s
const s = await node.device.state()
if (s.head && s.head.rev > base && has(s, 'r-direct')) { ms = Date.now() - t0; break }
await sleep(500)
}
t.comment('event-driven finalize latency: ' + (ms !== null ? ms + 'ms' : 'DID NOT FINALIZE'))
t.ok(ms !== null, 'the direct notary write finalized event-driven (head advanced past ' + base + ')')
const fin = await node.device.state()
t.is(node.sb.base.writable, true, 'still writable')
t.ok(has(fin, 'r-direct'), 'the post is in the finalized record set')
await n1.close()
await testnet.destroy()
})
// CO-OWNED path (the prod topology: stortron notary + tabertron co-owner). Admission uses manual
// base.update() drives (SETUP only). The finalization measurements below drive NOTHING — purely
// event-driven — so they exercise the fix in the real 2-writer/1-indexer shape.
const DEV2 = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote'
test('co-owned: notary OWN write AND co-owner write both finalize event-driven', { timeout: 300000 }, async (t) => {
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const sCre = tmp(); const sPeer = tmp()
const d1 = await bundle(M.root, M.dev, 'wj.local')
const d2 = await bundle(M.root, DEV2, 'wj.local')
const CRE = d1.keetPub < d2.keetPub ? d1 : d2
const PEER = d1.keetPub < d2.keetPub ? d2 : d1
const DID = CRE.did
const members = [{ key: d1.keetPub, role: 1 }, { key: d2.keetPub, role: 1 }]
CRE.entry.members = members; PEER.entry.members = members
const nCre = await bootMultiDevice({ storage: sCre, dids: [CRE.entry], swarmOpts, signIntervalMs: 1000 })
const nPeer = await bootMultiDevice({ storage: sPeer, dids: [PEER.entry], swarmOpts, signIntervalMs: 1000 })
t.teardown(async () => { try { await nCre.close() } catch {} ; try { await nPeer.close() } catch {} ; await testnet.destroy() })
const cre = () => nCre.byDid.get(DID)
const peer = () => nPeer.byDid.get(DID)
// SETUP (drives allowed): admit the peer as a co-owner writer + converge.
let admitted = false
for (let i = 0; i < 100 && !admitted; i++) { await cre().sb.base.update().catch(() => {}); await peer().sb.base.update().catch(() => {}); try { await peer().device.applyWrite(post('r-peer-setup', 'peer setup')); admitted = true } catch { await sleep(750) } }
t.ok(admitted, 'peer admitted as a co-owner writer')
for (let i = 0; i < 40; i++) { await cre().sb.base.update().catch(() => {}); await peer().sb.base.update().catch(() => {}); await sleep(300) }
// ── THE FIX under co-ownership: notary's OWN write, PURELY event-driven (NO manual drives) ──
const b1 = (await cre().device.state()).head.rev
await cre().device.applyWrite(post('r-notary', 'direct notary write, co-owned'))
let notaryMs = null; const tN = Date.now()
for (let i = 0; i < 50; i++) { const s = await cre().device.state(); if (s.head.rev > b1 && has(s, 'r-notary')) { notaryMs = Date.now() - tN; break } await sleep(500) }
t.comment('NOTARY own-write finalize (event-driven): ' + (notaryMs !== null ? notaryMs + 'ms' : 'NO'))
t.ok(notaryMs !== null, 'notary OWN write finalizes event-driven under co-ownership (the fix)')
// ── co-owner write finalizes at the notary, PURELY event-driven ──
const b2 = (await cre().device.state()).head.rev
await peer().device.applyWrite(post('r-peer2', 'co-owner write'))
let peerMs = null; const tP = Date.now()
for (let i = 0; i < 50; i++) { const s = await cre().device.state(); if (has(s, 'r-peer2') && s.head.rev > b2) { peerMs = Date.now() - tP; break } await sleep(500) }
t.comment('CO-OWNER write finalize at notary (event-driven): ' + (peerMs !== null ? peerMs + 'ms' : 'NO'))
t.ok(peerMs !== null, 'co-owner write finalizes at the notary event-driven')
// ── both writes converge to the peer's view ──
let converged = false
for (let i = 0; i < 60 && !converged; i++) { const ps = await peer().device.state(); if (has(ps, 'r-notary') && has(ps, 'r-peer2')) { converged = true; break } await sleep(500) }
t.ok(converged, 'peer converged BOTH the notary write and its own write')
})