// DELETE-THE-TIP WEDGE (repro + fix) — a SOLE writer that deletes its most-recent-committed record trips
// the forward-only sign gate forever: the record's CREATE is still in a #commit frame (netKeys has it),
// but the writer intentionally deleted it (view/curKeys lacks it), so the gate reads "view behind net head,
// missing <key>" and SKIPS every sign → no delete commit is ever produced → the chain wedges. Reproduced
// live on wj62/stortron with app.bsky.feed.post/healverify1 (docs/HANDOFF §28). The fix must let a
// LEGITIMATE delete through while still blocking a genuinely-behind view (test 2 of the sibling suite).
//
// Run: bare test/delete-tip-wedge.repro.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import b4a from 'b4a'
import { SwarmDevice } from '../lib/swarm-device.js'
const DID = 'did:plc:deletetip'
const KEY = (() => { const k = b4a.alloc(32); k[31] = 9; return k })()
const post = (text) => ({ $type: 'app.bsky.feed.post', text, createdAt: '2026-07-21T00:00:00.000Z' })
// The same in-memory KV the forward-only-sign suite uses (a converged, single-writer StateBase). del()
// hard-removes the key (matching StateBaseV3._applyDel), while the __commit__ frames persist.
class MemKV {
constructor () { this.m = new Map() }
async set (k, v) { this.m.set(k, v) }
async get (k) { return this.m.has(k) ? this.m.get(k) : null }
async getNow (k) { return this.get(k) }
async del (k) { this.m.delete(k) }
async list (prefix = '') { const o = {}; for (const [k, v] of this.m) if (k.startsWith(prefix)) o[k] = v; return o }
async listNow (prefix = '') { return this.list(prefix) }
}
test('delete-the-tip: a sole writer signs the delete forward (does NOT wedge)', async (t) => {
const sb = new MemKV()
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
await A.putRecord('app.bsky.feed.post', 'r2', post('two'))
await A.putRecord('app.bsky.feed.post', 'healverify1', post('tip to delete'))
const h1 = await A.sign({ force: true })
t.ok(h1 && h1.rev, 'created 3 records + signed the network head (rev ' + (h1 && h1.rev) + ')')
// Delete the MOST-RECENT-committed record (the tip). The view now lists {r1,r2}; the healverify1 CREATE
// is still in the #commit frame at h1.rev, so netKeys still contains it.
await A.deleteRecord('app.bsky.feed.post', 'healverify1')
// The SAME sole writer signs again. This must produce a DELETE commit (rev > h1.rev), not skip forever.
const h2 = await A.sign()
t.ok(h2 && h2.rev, 'sign produced a forward commit for the delete (did NOT wedge on the forward-only gate)')
t.ok(h2 && h2.rev > h1.rev, `delete commit rev advanced past the create (${h1 && h1.rev} -> ${h2 && h2.rev})`)
// The record is gone from the view, and a re-sign is a clean no-op (chain settled, not re-wedged).
const gone = (await A.repo.listRecords()).find((r) => r.rkey === 'healverify1')
t.absent(gone, 'healverify1 is deleted from the repo view')
const h3 = await A.sign()
t.is(h3, null, 're-sign after the delete is a no-op (chain settled — not re-wedged, not a phantom commit)')
})
test('delete-the-tip AFTER RESTART: the delete still finalizes with a proper delete frame (stortron state)', async (t) => {
const sb = new MemKV()
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true })
await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
await A.putRecord('app.bsky.feed.post', 'r2', post('two'))
await A.putRecord('app.bsky.feed.post', 'healverify1', post('tip'))
const h1 = await A.sign({ force: true })
await A.deleteRecord('app.bsky.feed.post', 'healverify1') // delete happens...
// ...then the process RESTARTS before the delete is ever signed (exactly stortron: it restarted while
// wedged). A fresh device shares the same converged StateBase; restore() re-seeds head + _lastRecords.
const B = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'B', firehose: true })
const h2 = await B.sign()
t.ok(h2 && h2.rev > h1.rev, `post-restart sign advanced past the wedge (${h1 && h1.rev} -> ${h2 && h2.rev})`)
// The firehose MUST carry a delete op for healverify1 — else the relay never learns of the delete and a
// later post's prev-link dangles. A 0-op "phantom" advance (no delete frame) would be a broken firehose.
const frames = Object.values(await sb.listNow('__commit__/')).sort((a, b) => (a.rev < b.rev ? -1 : 1))
const top = frames[frames.length - 1]
const delOp = (top.ops || []).find((o) => o.action === 'delete' && o.path === 'app.bsky.feed.post/healverify1')
t.ok(delOp, 'the newest #commit frame carries a delete op for healverify1 (relay learns of the delete)')
t.ok(top.rev > h1.rev && top.prevData, 'the delete frame chains forward from the create head (prev-link intact)')
})
// A genuinely BEHIND view (pointer linearized, bytes not yet fetched) must STILL skip — the delete-fix must
// not weaken the anti-content-regression guard. Fake store: pointer present in the StateBase, ensureLocal
// returns null (bytes unreplicated), so the record is absent from the resolved view but PRESENT in rawKeys.
test('behind (pointer present, bytes unreplicated): forward-only gate STILL skips (no content regression)', async (t) => {
const sb = new MemKV()
const bytes = new Map() // oid -> record; ensureLocal misses for an unreplicated oid
const store = {
async put (collection, rkey, record) { const oid = collection + '/' + rkey; bytes.set(oid, record); return { oid, size: 1 } },
async ensureLocal (collection, rkey, oid) { return bytes.has(oid) ? bytes.get(oid) : null }, // null = bytes not fetched
async del (collection, rkey) { bytes.delete(collection + '/' + rkey) },
async records () { return [] }
}
const A = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'A', firehose: true, recordStore: store })
await A.putRecord('app.bsky.feed.post', 'r1', post('one'))
await A.putRecord('app.bsky.feed.post', 'r2', post('two'))
const h1 = await A.sign({ force: true })
// A third record's POINTER converges (writer added it) but its BYTES are not fetched here → the pointer is
// in the StateBase (rawKeys) but the resolved view lists only {r1,r2}. Frame it into the net head.
await sb.set('app.bsky.feed.post/r3', { $oid: 'app.bsky.feed.post/r3', size: 1 }) // pointer present, no bytes
const real = Object.values(await sb.listNow('__commit__/')).pop()
await sb.set('__commit__/' + (real.rev + 'z'), { ...real, rev: real.rev + 'z', ops: [{ action: 'create', path: 'app.bsky.feed.post/r3', cid: null, prev: null }], seq: (real.seq || 1) + 1 })
const B = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'B', firehose: true, recordStore: store })
const hB = await B.sign()
t.is(hB, null, 'pointer present + bytes unreplicated → gate SKIPS (would-be delete of a live r3 is blocked)')
})