// Step 2a MIGRATION + ANTI-WIPE proof (the wj62-wipe hazard, reproduced + healed offline).
//
// The hazard: a repo written by the previous FILES-ONLY backend (OverlayRepoSubstrate) has record
// BYTES on disk but NO pointers in the shared autobase. Booting the new PointerSubstrate code over it,
// listRecords() reads the (empty) pointer set while restore() loads a NON-EMPTY head from the firehose
// → the first sign() materializes an empty repo → emits a delete-every-record #commit → wipes the
// account off bsky. This proves both defenses:
// (1) sign()'s ANTI-WIPE GUARD refuses to sign when the pointer set is empty but record FILES exist,
// so a not-yet-migrated boot never wipes.
// (2) migratePointers() backfills a {$oid,size} pointer per file → the record set converges → the
// next sign() is a byte-identical no-op (head preserved), and a subsequent post emits ONE create
// (no re-authoring of the migrated records — restore() re-baselines the diff).
// Plus: a GENUINE full-delete (no files left) is NOT blocked — the guard is precise, not count-only.
import '../lib/bare-globals.js' // web-global polyfills for Bare — MUST load first
import test from 'brittle'
import { mkdtempSync, rmSync } from 'bare-fs'
import { join } from 'bare-path'
import os from 'bare-os'
import b4a from 'b4a'
import { SwarmDevice } from '../lib/swarm-device.js'
import { OverlayRepoStore } from 'atproto-store/lib/overlay-repo-store.js'
const DID = 'did:plc:migtest'
const KEY = (() => { const k = b4a.alloc(32); k[31] = 9; return k })()
const tmp = (t) => { const d = mkdtempSync(join(os.tmpdir(), 'sd-mig-')); t.teardown(() => rmSync(d, { recursive: true, force: true })); return d }
const post = (text, at) => ({ $type: 'app.bsky.feed.post', text, createdAt: at })
// StateBase-shaped KV with a writable `base` (migratePointers gates on sb.base.writable).
class MigKV {
constructor () { this.m = new Map(); this.base = { writable: true } }
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('2a migration: not-yet-migrated boot does NOT wipe (anti-wipe guard), then backfill heals it', async (t) => {
const sb = new MigKV()
const store = await new OverlayRepoStore(tmp(t)).ready()
// ── Phase A: a files-only repo's END state — record BYTES on disk + a signed firehose head, but the
// pointer set is ABSENT (as if written by OverlayRepoSubstrate). Establish it via one device, then
// strip the pointers to simulate the pre-migration on-disk reality.
const d1 = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'creator', firehose: true, recordStore: store })
await d1.putRecord('app.bsky.feed.post', '1', post('one', '2026-01-01T00:00:00.000Z'))
await d1.putRecord('app.bsky.feed.post', '2', post('two', '2026-01-02T00:00:00.000Z'))
const signed = await d1.sign({ force: true })
t.ok(signed && signed.rev, 'phase A: creator signed a non-empty commit')
const headDataCid = signed.dataCid.toString()
t.is((await d1.firehoseLog()).length, 1, 'phase A: firehose has one #commit')
// strip the pointers → files remain on disk, autobase record set now empty (the files-only reality)
await sb.del('app.bsky.feed.post/1')
await sb.del('app.bsky.feed.post/2')
t.is(Object.keys(await sb.list('app.bsky.feed.post/')).length, 0, 'phase A: pointer set stripped (files-only state)')
t.is((await store.records()).length, 2, 'phase A: the 2 record FILES are still on disk')
// ── Phase B: boot the NEW PointerSubstrate code over that state (fresh device object = a restart).
const d2 = new SwarmDevice(sb, { did: DID, privateKey: KEY, self: 'creator', firehose: true, recordStore: store })
t.is((await d2.repo.listRecords()).length, 0, 'phase B: listRecords reads ZERO (the wipe hazard is live)')
// sign() WITHOUT force — restore() loads the non-empty head, listRecords() reads empty → the guard
// MUST fire and refuse to sign (no delete-all commit).
const wipeAttempt = await d2.sign()
t.is(wipeAttempt, null, 'phase B: anti-wipe guard refused the sign (returned null)')
t.is((await d2.firehoseLog()).length, 1, 'phase B: NO new #commit — the account was NOT wiped')
t.is(d2.head && d2.head.dataCid.toString(), headDataCid, 'phase B: head still points at the non-empty repo')
// migrate: backfill the pointers, then re-baseline like the boot path (restore()).
const mig = await d2.migratePointers()
t.alike(mig, { written: 2, skipped: 0, total: 2 }, 'phase B: migratePointers backfilled both records')
await d2.restore() // boot path calls this once existing>0 — re-seeds head + the diff baseline
t.is((await d2.repo.listRecords()).length, 2, 'phase B: record set converged (2) after backfill')
t.alike((await d2.repo.getRecord('app.bsky.feed.post', '1')), post('one', '2026-01-01T00:00:00.000Z'), 'byte-exact record resolves')
// the next sign is a byte-identical no-op — head preserved, no spurious #commit.
const noop = await d2.sign()
t.is(noop, null, 'phase B: post-migration sign is a no-op (unchanged data root)')
t.is((await d2.firehoseLog()).length, 1, 'phase B: still one #commit (no re-author)')
// ── Phase C: a NEW post after migration emits exactly ONE create — the migrated records are the
// diff baseline (restore re-based them), so they are NOT re-authored.
await d2.putRecord('app.bsky.feed.post', '3', post('three', '2026-01-03T00:00:00.000Z'))
const h3 = await d2.sign()
t.ok(h3 && h3.rev, 'phase C: new post signed a fresh commit')
const log = await d2.firehoseLog()
t.is(log.length, 2, 'phase C: exactly one new #commit for the new post')
const frame = log[log.length - 1]
t.is(frame.ops.length, 1, 'phase C: the new #commit has exactly ONE op (no re-authoring of migrated records)')
t.alike(frame.ops[0], { action: 'create', path: 'app.bsky.feed.post/3', cid: frame.ops[0].cid, prev: null }, 'phase C: the single op is the new record create')
await store.close()
})
test('anti-wipe guard is PRECISE: a genuine full-delete (no files left) is NOT blocked', async (t) => {
const store = await new OverlayRepoStore(tmp(t)).ready()
const d = new SwarmDevice(new MigKV(), { did: DID, privateKey: KEY, self: 'creator', firehose: true, recordStore: store })
await d.putRecord('app.bsky.feed.post', '1', post('only', '2026-01-01T00:00:00.000Z'))
await d.sign({ force: true })
t.is((await d.firehoseLog()).length, 1, 'one record signed')
// delete it through the repo (removes BOTH the pointer AND the file) — a legitimate empty repo.
await d.deleteRecord('app.bsky.feed.post', '1')
t.is((await store.records()).length, 0, 'no record files remain (genuine empty)')
const h = await d.sign() // head != null, records 0, files 0 → guard does NOT fire → emit the delete
t.ok(h && h.rev, 'sign proceeded (genuine empty is allowed)')
const frame = (await d.firehoseLog())[1]
t.is(frame.ops.length, 1, 'the delete op was emitted')
t.is(frame.ops[0].action, 'delete', 'it is a delete (relay reflects the intentional removal)')
await store.close()
})