aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/ark-reconcile-race.test.js22
1 files changed, 22 insertions, 0 deletions
diff --git a/tools/ark-reconcile-race.test.js b/tools/ark-reconcile-race.test.js
new file mode 100644
index 0000000..165eb58
--- /dev/null
+++ b/tools/ark-reconcile-race.test.js
@@ -0,0 +1,22 @@
+// 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
+});