// Generated, do not edit. sha256:e96cfe6ba81097a2ecb4810acd03529886647af210d2bac0f57c242345f7c633 import { describe, it, beforeEach, mock } from 'node:test'; import assert from 'node:assert/strict'; import { createDbHarness } from '../helpers/db-node.mjs'; const harness = createDbHarness({ seedPreferences: true }); mock.module('../../server/db.js', { namedExports: harness.exports }); const { createAiProposal, listPendingAiProposals, listAiProposals, getAiProposal, acceptAiProposal, rejectAiProposal } = await import('../../server/data/ai-proposals.js'); const { createAccount } = await import('../../server/data/accounts.js'); const { listEvents } = await import('../../server/data/events.js'); const { createAsset } = await import('../../server/data/assets.js'); const { listPriceHistory } = await import('../../server/data/price-history.js'); const { takeChanges } = await import('../../server/data/_events.js'); const id = r => (typeof r === 'object' && r !== null ? (r.id ?? r) : r); let btc, acc; function manualProvider() { harness.exports.run(`INSERT INTO Provider (key, name, searchName, baseUrl, authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt) VALUES ('manual','Handmatig','handmatig','','none',0,1,1,1,'2026-01-01T00:00:00Z')`); } const voorstel = (extra = {}) => createAiProposal({ kind: 'price_point', assetId: btc, fiatPrice: 50000, observedAt: '2026-03-01T00:00:00Z', note: 'koers volgens de muntwinkel', ...extra, }); beforeEach(async () => { harness.recreate(); takeChanges(); btc = id(await createAsset({ symbol: 'BTC', name: 'Bitcoin', kind: 'crypto', unit: 'BTC', subunit: 'sat', decimals: 8, displayUnit: 'unit' })); acc = id(await createAccount({ name: 'Kluis', kind: 'vault', isOwnCustody: 1, scanEnabled: 0 })); takeChanges(); }); describe('ai-proposals: aanmaken en opsommen', () => { it('maakt een voorstel dat openstaat en de asset erbij noemt', async () => { const pid = await voorstel(); const rij = getAiProposal(pid); assert.equal(rij.status, 'pending'); assert.equal(rij.asset_symbol, 'BTC'); assert.equal(rij.asset_name, 'Bitcoin'); assert.equal(rij.resolvedAt, null); }); it('weigert een onbekende soort met een leesbare fout', async () => { await assert.rejects(() => createAiProposal({ kind: 'iets_anders' }), /kind/); }); it('somt alleen openstaande voorstellen op', async () => { const blijft = await voorstel(); const weg = await voorstel({ fiatPrice: 60000 }); await rejectAiProposal(weg, 'niet nodig'); assert.deepEqual(listPendingAiProposals().map(r => r.id), [blijft]); }); }); describe('ai-proposals: accepteren voert écht uit', () => { it('schrijft het koerspunt via de datalaag naar PriceHistory', async () => { manualProvider(); const pid = await voorstel(); await acceptAiProposal(pid); const punten = listPriceHistory(btc); assert.equal(punten.length, 1, 'het punt hoort in PriceHistory te staan'); assert.equal(punten[0].fiatPrice, 50000); assert.equal(punten[0].observedAt, '2026-03-01T00:00:00Z', 'dagresolutie, dus middernacht'); assert.equal(punten[0].resolution, 'day'); assert.equal(getAiProposal(pid).status, 'accepted'); assert.ok(getAiProposal(pid).resolvedAt, 'resolvedAt hoort gevuld te zijn'); }); it('kan twee keer toegepast worden zonder dubbel punt, want de upsert is idempotent', async () => { manualProvider(); const eerste = await voorstel(); await acceptAiProposal(eerste); const tweede = await voorstel(); await acceptAiProposal(tweede); assert.equal(listPriceHistory(btc).length, 1, 'zelfde asset, zelfde dag, zelfde bron: één rij'); }); }); describe('ai-proposals: de niet-gelukkige paden', () => { it('weigert een tweede keer accepteren', async () => { manualProvider(); const pid = await voorstel(); await acceptAiProposal(pid); await assert.rejects(() => acceptAiProposal(pid), (e) => e.key === 'aiProposals.handled_accepted'); }); it('weigert accepteren van een afgewezen voorstel', async () => { const pid = await voorstel(); await rejectAiProposal(pid); await assert.rejects(() => acceptAiProposal(pid), (e) => e.key === 'aiProposals.handled_rejected'); assert.equal(listPriceHistory(btc).length, 0, 'er hoort niets geschreven te zijn'); }); it('weigert een voorstel dat niet bestaat', async () => { await assert.rejects(() => acceptAiProposal(999999), (e) => e.key === 'aiProposals.unknownProposal'); await assert.rejects(() => rejectAiProposal(999999), (e) => e.key === 'aiProposals.unknownProposal'); }); it('verdwijnt met zijn asset, en accepteren gooit daarna', async () => { const pid = await voorstel(); harness.exports.run(`DELETE FROM Asset WHERE id = ?`, [btc]); assert.equal(getAiProposal(pid), null); await assert.rejects(() => acceptAiProposal(pid), (e) => e.key === 'aiProposals.unknownProposal'); }); it('laat het voorstel STAAN als de uitvoering afketst, met de reden erbij', async () => { const pid = await voorstel(); await assert.rejects(() => acceptAiProposal(pid), /manual/); const rij = getAiProposal(pid); assert.equal(rij.status, 'pending', 'een afgeketste uitvoering hoort het voorstel niet af te handelen'); assert.match(rij.detail, /manual/, 'de reden hoort na te lezen te zijn'); assert.deepEqual(listPendingAiProposals().map(r => r.id), [pid], 'en hij staat nog op de lijst'); }); it('wist de reden zodra het alsnog lukt', async () => { const pid = await voorstel(); await assert.rejects(() => acceptAiProposal(pid)); manualProvider(); await acceptAiProposal(pid); assert.equal(getAiProposal(pid).detail, null); assert.equal(getAiProposal(pid).status, 'accepted'); }); it('bewaart een afgewezen voorstel in plaats van het te verwijderen', async () => { const pid = await voorstel(); await rejectAiProposal(pid, 'die koers klopt niet'); const rij = getAiProposal(pid); assert.equal(rij.status, 'rejected'); assert.equal(rij.detail, 'die koers klopt niet'); assert.ok(rij.resolvedAt); }); }); describe('het verandersignaal, zodat de kaart zichzelf bijwerkt (27-08-2026)', () => { it('zet een signaal bij het aanmaken', async () => { await voorstel(); assert.deepEqual(takeChanges(), ['ai-proposals-changed']); }); it('zet een signaal bij het afwijzen, voor een tweede tabblad en voor de bolletjes', async () => { const pid = await voorstel(); takeChanges(); await rejectAiProposal(pid, 'niet nodig'); assert.deepEqual(takeChanges(), ['ai-proposals-changed']); }); it('zet een signaal bij het goedkeuren', async () => { manualProvider(); const pid = await voorstel(); takeChanges(); await acceptAiProposal(pid); assert.ok(takeChanges().includes('ai-proposals-changed')); }); it('zet OOK een signaal als de uitvoering afketst', async () => { const pid = await voorstel(); takeChanges(); await assert.rejects(() => acceptAiProposal(pid)); assert.deepEqual(takeChanges(), ['ai-proposals-changed']); }); it('zegt niets bij een leesactie', async () => { await voorstel(); takeChanges(); listPendingAiProposals(); getAiProposal(1); assert.deepEqual(takeChanges(), []); }); }); describe('listAiProposals: ook wat al afgehandeld is (27-08-2026)', () => { it('geeft afgewezen en openstaande voorstellen, nieuwste eerst', async () => { const eerste = await voorstel(); const tweede = await voorstel({ fiatPrice: 60000 }); await rejectAiProposal(eerste, 'niet nodig'); const rijen = listAiProposals(); assert.deepEqual(rijen.map(r => r.id), [tweede, eerste], 'nieuwste eerst'); assert.equal(rijen.find(r => r.id === eerste).status, 'rejected', 'een afgehandeld voorstel hoort erin te zitten'); }); it('houdt zich aan de grens en negeert een onzinnige', async () => { for (let i = 0; i < 4; i++) await voorstel({ fiatPrice: 50000 + i }); assert.equal(listAiProposals(2).length, 2); assert.equal(listAiProposals(0).length, 4); assert.equal(listAiProposals('twee').length, 4); }); it('geeft NIETS als de koppeling uit staat, net als de pending-lijst', async () => { await voorstel(); harness.exports.run(`UPDATE Preference SET value = '0' WHERE scope = 'ai' AND key = 'allow_read'`); harness.exports.run(`UPDATE Preference SET value = '0' WHERE scope = 'ai' AND key = 'enabled'`); assert.deepEqual(listAiProposals(), []); }); }); describe('ai-proposals: kosten', () => { const boeking = (extra = {}) => createAiProposal({ kind: 'event', assetId: btc, eventType: 'buy', amount: 100000, fiatPrice: 50000, observedAt: '2026-03-01T10:00:00Z', toAccountId: acc, ...extra, }); it('bewaart de kosten op het voorstel', async () => { const pid = await boeking({ fiatFee: 4.5 }); assert.equal(getAiProposal(pid).fiatFee, 4.5); }); it('laat ze weg als ze er niet zijn, en zet geen nul', async () => { assert.equal(getAiProposal(await boeking()).fiatFee, null); }); it('ZET ZE OOK ÉCHT OP DE BOEKING, en dat is de hele reden dat dit veld bestaat', async () => { const pid = await boeking({ fiatFee: 4.5 }); await acceptAiProposal(pid); const events = listEvents(); assert.equal(events.length, 1); assert.equal(events[0].fiatFee, 4.5, 'zonder deze regel is de kostprijs precies de fee te laag'); assert.equal(events[0].fiatPrice, 50000, 'en de koers hoort onaangeroerd te blijven'); }); it('weigert kosten op een soort die ze niet kent', async () => { await assert.rejects( () => createAiProposal({ kind: 'price_point', assetId: btc, fiatPrice: 50000, observedAt: '2026-03-01T00:00:00Z', fiatFee: 1 }), /Fees belong to an event/); }); it('weigert een negatieve fee, want dat is de DDL', async () => { await assert.rejects(() => boeking({ fiatFee: -1 })); }); const verkoop = (extra = {}) => boeking({ eventType: 'sell', toAccountId: null, fromAccountId: acc, ...extra }); it('zet een fee in eenheden op de verkoop die hij goedkeurt', async () => { const pid = await verkoop({ unitFee: 150 }); assert.equal(getAiProposal(pid).unitFee, 150); await acceptAiProposal(pid); assert.equal(listEvents()[0].unitFee, 150, 'zonder deze regel staat het grootboek de fee te hoog'); }); it('weigert een fee in eenheden waar het grootboek hem niet leest', async () => { await assert.rejects(() => boeking({ unitFee: 150 }), /fee in units belongs to a sell or a transfer/); await assert.rejects( () => createAiProposal({ kind: 'price_point', assetId: btc, fiatPrice: 50000, observedAt: '2026-03-01T00:00:00Z', unitFee: 1 }), /Fees belong to an event/); }); });