// bun test tools/ark-reconcile-race.test.js — a reconcile pass must not hand a // coin back to spendable after a payment took it mid-pass (double-tapped zap). import { test, expect, mock } from 'bun:test'; import * as proto from '../src/ark/proto.js'; let release; const gate = new Promise((r) => { release = r; }); mock.module('../src/ark/proto.js', () => ({ ...proto, getVtxoStatus: async () => { await gate; return proto.VTXO_STATE_SPENDABLE; } })); const { ArkManager } = await import('../src/ark/manager.js'); test('a coin taken mid-reconcile stays taken', async () => { const m = Object.create(ArkManager.prototype); m.state = { vtxos: [{ id: 'a', state: 'spendable', amountSat: 85, bytes: 'x' }, { id: 'b', state: 'pending', amountSat: 5, bytes: 'x' }], actions: [], scheduled: [] }; m.arkUrl = 'x'; m._decoded = () => ({ point: { raw: new Uint8Array(36) } }); m._keyForVtxo = () => ({ privkey: new Uint8Array(32) }); m._save = () => {}; m._movement = () => {}; const p = m.reconcile(); await new Promise((r) => setTimeout(r, 10)); m.state.vtxos[0].state = 'pending'; // the zap takes coin a m.state.actions.push({ type: 'ln-pay', step: 'created', parts: [{ inputId: 'a' }] }); release(); await p; expect(m.state.vtxos[0].state).toBe('pending'); expect(m.state.vtxos[1].state).toBe('spendable'); // a real orphan is still freed });