Files
2026-10-29 11:57:29 +01:00

257 lines
11 KiB
JavaScript

// 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/);
});
});