// Crash during append — SIGKILL a publish in flight, reopen, and check the tree
// and the pointer records still agree. The tagged-record scheme exists so the
// pointer commits in the same CoreTX as the tree node; this is what tests it.
//
// node test/crash-append.mjs <workDir> <bigFile> [trials]
// node test/crash-append.mjs --child <virtual|stock> <dir> <file> (internal)
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const fs = require('fs')
const { spawn } = require('child_process')
const Corestore = require('corestore')
const Hyperdrive = require('hyperdrive')
const { ContentStore, build } = require('../lib/storage.cjs')
const { chunk } = await import('../../hyper-overlay/lib/chunker.js')
const NAME = '/app.bin'
const PRIMARY = Buffer.alloc(32, 7)
function indexContent (file) {
const buf = fs.readFileSync(file)
const content = new ContentStore()
const chunks = chunk(buf)
for (const c of chunks) content.add(c.hash, file, c.offset, c.length)
return { buf, chunks, content }
}
function openStore (dir, mode, content) {
if (mode === 'stock') return new Corestore(dir, { primaryKey: PRIMARY, unsafe: true })
const Storage = build(content)
return new Corestore(new Storage(dir), { primaryKey: PRIMARY, unsafe: true })
}
function write (ws, buf) {
return new Promise((resolve) => {
if (ws.write(buf)) return resolve()
ws.once('drain', resolve)
})
}
// ── child: publish, and get killed partway through ──────────────────────────
if (process.argv[2] === '--child') {
const [, , , mode, dir, file] = process.argv
const { chunks, content } = indexContent(file)
const store = openStore(dir, mode, content)
const drive = new Hyperdrive(store)
await drive.ready()
const ws = drive.createWriteStream(NAME, { dedup: true })
process.stdout.write('READY\n')
for (const c of chunks) await write(ws, c.data)
await new Promise((resolve, reject) => {
ws.once('error', reject)
ws.once('close', resolve)
ws.end()
})
await drive.close()
await store.close()
process.stdout.write('DONE\n')
process.exit(0)
}
// ── parent ──────────────────────────────────────────────────────────────────
const ROOT = process.argv[2]
const FILE = process.argv[3]
const TRIALS = Number(process.argv[4] || 4)
const { buf, content } = indexContent(FILE)
function crashRun (dir, mode, killAfterMs) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [process.argv[1], '--child', mode, dir, FILE], {
stdio: ['ignore', 'pipe', 'ignore']
})
let killed = false
let finished = false
child.stdout.on('data', (d) => {
if (d.toString().includes('READY')) {
setTimeout(() => {
if (!finished) {
child.kill('SIGKILL')
killed = true
}
}, killAfterMs)
}
if (d.toString().includes('DONE')) finished = true
})
child.on('exit', (code, signal) => resolve({ killed, finished, code, signal }))
})
}
// Reopen and look for the one state that would prove the scheme broken:
// the tree claims a block, but its bytes cannot be produced.
async function audit (dir, mode) {
const store = openStore(dir, mode, content)
const drive = new Hyperdrive(store)
const out = { opened: false, blobsLength: 0, claimedButMissing: 0, entry: null, bytesOk: null }
try {
await drive.ready()
await drive.getBlobs()
out.opened = true
const core = drive.blobs.core
out.blobsLength = core.length
for (let i = 0; i < core.length; i++) {
if (!(await core.has(i))) continue
const block = await core.get(i, { wait: false })
if (block === null) out.claimedButMissing++
}
const entry = await drive.entry(NAME)
out.entry = entry === null ? 'none' : 'present'
if (entry !== null) {
const got = await drive.get(NAME, { timeout: 5000 }).catch(() => null)
out.bytesOk = got !== null && got.equals(buf)
}
} catch (err) {
out.error = err.code || err.message
}
await drive.close().catch(() => {})
await store.close().catch(() => {})
return out
}
const results = []
for (let t = 0; t < TRIALS; t++) {
for (const mode of ['virtual', 'stock']) {
const dir = `${ROOT}/${mode}-${t}`
const killAfterMs = 150 + t * 400
const run = await crashRun(dir, mode, killAfterMs)
const state = await audit(dir, mode)
results.push({ trial: t, mode, killAfterMs, run, state })
console.log(JSON.stringify({ trial: t, mode, killAfterMs, ...run, ...state }))
}
}
content.close()
const broken = results.filter((r) => r.state.claimedButMissing > 0 || r.state.bytesOk === false)
console.log(
JSON.stringify({
trials: results.length,
killedMidPublish: results.filter((r) => r.run.killed).length,
allReopened: results.every((r) => r.state.opened),
inconsistent: broken.length,
verdict: broken.length === 0 ? 'PASS' : 'FAIL'
})
)