Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,405 @@
|
||||
// Generated, do not edit. sha256:3566aeffe3b8fd67fb12a75b9b7a38c50a27b00abed96a2cec53436ed08cde28
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { schijnwaarde, isSchijnwaarde, createAliasRegistry, ALIAS_SOORTEN } from '../../server/ai/aliases.js';
|
||||
import { bucket, bucketLabel, shareBucket, shareLabel } from '../../server/ai/buckets.js';
|
||||
import { createTranslator } from '../../server/ai/translate.js';
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
|
||||
const TEST_GEHEIM = 'test-geheim-voor-de-suite-0123456789abcdef';
|
||||
|
||||
describe('schijnwaarden: de onmogelijke letter', () => {
|
||||
it('maakt een adres dat geen geldig bech32 kan zijn', () => {
|
||||
const a = schijnwaarde('address', 0);
|
||||
assert.ok(a.startsWith('bc1q'), a);
|
||||
assert.equal(a.length, 42);
|
||||
assert.ok(a.slice(4).includes('o'), `geen onmogelijke letter in ${a}`);
|
||||
});
|
||||
|
||||
it('maakt een xpub die geen geldig base58 kan zijn', () => {
|
||||
const x = schijnwaarde('xpub', 0);
|
||||
assert.ok(x.startsWith('xpub'), x);
|
||||
assert.equal(x.length, 111);
|
||||
assert.ok(x.slice(4).includes('0'), `geen onmogelijke letter in ${x}`);
|
||||
});
|
||||
|
||||
it('maakt een txid die geen geldige hex kan zijn', () => {
|
||||
const t = schijnwaarde('txid', 0);
|
||||
assert.equal(t.length, 64);
|
||||
assert.ok(t.includes('z'), `geen onmogelijke letter in ${t}`);
|
||||
});
|
||||
|
||||
it('herkent zijn eigen maaksel, ook uit een andere sessie', () => {
|
||||
for (const soort of ALIAS_SOORTEN) {
|
||||
assert.equal(isSchijnwaarde(schijnwaarde(soort, 7)), true, soort);
|
||||
}
|
||||
});
|
||||
|
||||
it('houdt een waarde die alleen uit het echte alfabet bestaat voor echt', () => {
|
||||
const echtVormig = `bc1q${'qpzry9x8gf2tvdw0s3jn54khce6mua7l'.repeat(2).slice(0, 38)}`;
|
||||
assert.equal(echtVormig.length, 42);
|
||||
assert.equal(isSchijnwaarde(echtVormig), false);
|
||||
});
|
||||
|
||||
it('geeft dezelfde invoer dezelfde schijnwaarde, en verschillende invoer een andere', () => {
|
||||
const r = createAliasRegistry({ secret: TEST_GEHEIM });
|
||||
const een = r.alias('address', 'bc1qecht1');
|
||||
assert.equal(r.alias('address', 'bc1qecht1'), een);
|
||||
assert.notEqual(r.alias('address', 'bc1qecht2'), een);
|
||||
assert.equal(r.resolve(een), 'bc1qecht1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles: stabiel over sessies, want anders wijzen ze stil naar de verkeerde rij', () => {
|
||||
const versDeSessie = () => createTranslator({ secret: TEST_GEHEIM });
|
||||
|
||||
it('geeft dezelfde rij in twee losse sessies dezelfde handle', () => {
|
||||
const een = versDeSessie().toAi({ id: 42 }, { entity: 'account' }).id;
|
||||
const twee = versDeSessie().toAi({ id: 42 }, { entity: 'account' }).id;
|
||||
assert.equal(een, twee);
|
||||
});
|
||||
|
||||
it('geeft twee rijen een andere handle, ook als ze in omgekeerde volgorde langskomen', () => {
|
||||
const a = versDeSessie();
|
||||
const b = versDeSessie();
|
||||
const a7 = a.toAi({ id: 7 }, { entity: 'account' }).id;
|
||||
const a9 = a.toAi({ id: 9 }, { entity: 'account' }).id;
|
||||
const b9 = b.toAi({ id: 9 }, { entity: 'account' }).id;
|
||||
const b7 = b.toAi({ id: 7 }, { entity: 'account' }).id;
|
||||
assert.equal(a7, b7);
|
||||
assert.equal(a9, b9);
|
||||
assert.notEqual(a7, a9);
|
||||
});
|
||||
|
||||
it('geeft een ANDER geheim andere handles', () => {
|
||||
const anders = createTranslator({ secret: 'een-heel-ander-geheim-0123456789abcdef' });
|
||||
assert.notEqual(
|
||||
versDeSessie().toAi({ id: 42 }, { entity: 'account' }).id,
|
||||
anders.toAi({ id: 42 }, { entity: 'account' }).id);
|
||||
});
|
||||
|
||||
it('geeft dezelfde waarde onder twee SOORTEN een andere handle', () => {
|
||||
const t = versDeSessie();
|
||||
const rekening = t.toAi({ id: 3 }, { entity: 'account' }).id;
|
||||
const asset = t.toAi({ id: 3 }, { entity: 'asset' }).id;
|
||||
assert.notEqual(rekening.split(':')[1], asset.split(':')[1]);
|
||||
});
|
||||
|
||||
it('weigert te bestaan zonder geheim', () => {
|
||||
assert.throws(() => createAliasRegistry(), /secret/i);
|
||||
assert.throws(() => createAliasRegistry({ secret: 'kort' }), /secret/i);
|
||||
});
|
||||
|
||||
it('maakt ook een schijnwaarde stabiel over sessies', () => {
|
||||
const een = versDeSessie().toAi({ address: 'bc1qecht' }, { entity: 'walletAddress' }).address;
|
||||
const twee = versDeSessie().toAi({ address: 'bc1qecht' }, { entity: 'walletAddress' }).address;
|
||||
assert.equal(een, twee);
|
||||
assert.ok(isSchijnwaarde(een), een);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emmers', () => {
|
||||
it('legt 12.347 in "10000 tot 15000" op de grove ladder', () => {
|
||||
assert.equal(bucketLabel(12347, 'COARSE'), '10000 tot 15000');
|
||||
});
|
||||
|
||||
it('is op elke schaal even nauwkeurig', () => {
|
||||
assert.equal(bucketLabel(123.47, 'COARSE'), '100 tot 150');
|
||||
assert.equal(bucketLabel(1234700, 'COARSE'), '1000000 tot 1500000');
|
||||
});
|
||||
|
||||
it('is fijner op de fijne ladder', () => {
|
||||
assert.equal(bucketLabel(12347, 'FINE'), '10000 tot 12500');
|
||||
});
|
||||
|
||||
it('laat nul met rust, want nul is geen geheim', () => {
|
||||
assert.deepEqual(bucket(0), { from: 0, to: 0, exact: true });
|
||||
assert.equal(bucketLabel(0), '0');
|
||||
});
|
||||
|
||||
it('houdt het teken van een negatief bedrag', () => {
|
||||
const b = bucket(-12347, 'COARSE');
|
||||
assert.ok(b.from < 0 && b.to < 0 && b.from < b.to, JSON.stringify(b));
|
||||
});
|
||||
|
||||
it('valt niet om op een exacte macht van tien', () => {
|
||||
assert.equal(bucketLabel(10000, 'COARSE'), '10000 tot 15000');
|
||||
assert.equal(bucketLabel(100000, 'COARSE'), '100000 tot 150000');
|
||||
});
|
||||
|
||||
it('geeft niets terug voor iets dat geen getal is', () => {
|
||||
assert.equal(bucket(NaN), null);
|
||||
assert.equal(bucket('12347'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emmers: aandelen', () => {
|
||||
it('legt een aandeel in een marge van vijf punt', () => {
|
||||
assert.equal(shareLabel(42.7), '40 tot 45%');
|
||||
assert.equal(shareLabel(45), '45 tot 50%');
|
||||
assert.equal(shareLabel(99.9), '95 tot 100%');
|
||||
});
|
||||
|
||||
it('is fijner in de staart, want daar zijn de verschillen klein', () => {
|
||||
assert.equal(shareLabel(2), '2 tot 4%');
|
||||
assert.equal(shareLabel(7.4), '6 tot 8%');
|
||||
assert.equal(shareLabel(11), '10 tot 15%');
|
||||
});
|
||||
|
||||
it('zegt van een piepklein aandeel dat het onder de ondergrens ligt', () => {
|
||||
assert.equal(shareLabel(0.4), 'minder dan 2%');
|
||||
assert.equal(shareBucket(0.4).below, true);
|
||||
assert.equal(shareBucket(3).below, false);
|
||||
});
|
||||
|
||||
it('houdt 0% en 100% exact, want dat zijn feiten en geen marges', () => {
|
||||
assert.equal(shareLabel(0), '0%');
|
||||
assert.equal(shareLabel(100), '100%');
|
||||
});
|
||||
|
||||
it('geeft niets terug voor iets dat geen aandeel is', () => {
|
||||
assert.equal(shareBucket(NaN), null);
|
||||
assert.equal(shareBucket(-3), null);
|
||||
assert.equal(shareBucket('42'), null);
|
||||
});
|
||||
|
||||
it('heeft een ladder die oploopt en op 100 eindigt', () => {
|
||||
const g = CONFIG.AI.SHARE_BUCKETS;
|
||||
assert.equal(g[g.length - 1], 100);
|
||||
for (let i = 1; i < g.length; i++) assert.ok(g[i] > g[i - 1], `stap ${i} loopt niet op`);
|
||||
assert.ok(g[0] > 0, 'de ondergrens hoort boven nul te liggen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('emmers: de bodem, want een relatieve ladder heeft een absolute grens', () => {
|
||||
const BODEM = 3000;
|
||||
|
||||
it('geeft onder de bodem een bovengrens in plaats van een bereik', () => {
|
||||
assert.equal(bucketLabel(1200, 'COARSE'), `minder dan ${BODEM}`);
|
||||
assert.equal(bucketLabel(42, 'COARSE'), `minder dan ${BODEM}`);
|
||||
assert.equal(bucketLabel(2, 'COARSE'), `minder dan ${BODEM}`);
|
||||
});
|
||||
|
||||
it('laat de bodem los zodra er genoeg waarden in de emmer passen', () => {
|
||||
assert.equal(bucketLabel(BODEM, 'COARSE'), '3000 tot 5000');
|
||||
assert.equal(bucketLabel(BODEM - 1, 'COARSE'), `minder dan ${BODEM}`);
|
||||
});
|
||||
|
||||
it('geldt ALLEEN voor gehele waarden', () => {
|
||||
assert.equal(bucketLabel(123.47, 'COARSE'), '100 tot 150');
|
||||
assert.equal(bucketLabel(1.6, 'COARSE'), '1.5 tot 2');
|
||||
});
|
||||
|
||||
it('geldt NIET voor de fijne ladder', () => {
|
||||
assert.equal(bucketLabel(1200, 'FINE'), '1000 tot 1250');
|
||||
assert.equal(bucketLabel(25, 'FINE'), '25 tot 31.5');
|
||||
});
|
||||
|
||||
it('houdt het teken van een negatieve waarde onder de bodem', () => {
|
||||
assert.equal(bucketLabel(-42, 'COARSE'), `meer dan -${BODEM}`);
|
||||
const b = bucket(-42, 'COARSE');
|
||||
assert.ok(b.from < 0 && b.to === 0, JSON.stringify(b));
|
||||
});
|
||||
|
||||
it('laat nul nog steeds met rust', () => {
|
||||
assert.equal(bucketLabel(0, 'COARSE'), '0');
|
||||
});
|
||||
|
||||
it('schuift mee als de ladder fijner wordt', () => {
|
||||
const smalst = Math.min(...CONFIG.AI.BUCKET_LADDERS.COARSE.map((v, i, a) =>
|
||||
((i + 1 < a.length ? a[i + 1] : 10) - v) / v));
|
||||
assert.equal(BODEM, CONFIG.AI.BUCKET_MIN_VALUES.COARSE / smalst);
|
||||
});
|
||||
});
|
||||
|
||||
describe('het choke point, heen', () => {
|
||||
const rij = () => ({
|
||||
id: 4, accountId: 9, address: 'bc1qechtadresvandegebruiker0000000000000',
|
||||
txid: 'a'.repeat(64), valueSats: 123456789, fiatPrice: 54321.5, direction: 'in',
|
||||
note: 'geheime notitie', blockHeight: 800000, geenBekendVeld: 'zou niet mee mogen',
|
||||
});
|
||||
|
||||
it('laat alleen gedeclareerde velden door', () => {
|
||||
const uit = createTranslator({ secret: TEST_GEHEIM }).toAi(rij(), { entity: 'walletTransaction' });
|
||||
assert.equal(Object.hasOwn(uit, 'geenBekendVeld'), false);
|
||||
assert.equal(Object.hasOwn(uit, 'blockHeight'), false, 'keten-metadata zonder alias-vorm gaat niet mee');
|
||||
});
|
||||
|
||||
it('vervangt keten-data door een schijnwaarde van dezelfde soort', () => {
|
||||
const uit = createTranslator({ secret: TEST_GEHEIM }).toAi(rij(), { entity: 'walletTransaction' });
|
||||
assert.ok(uit.address.startsWith('bc1q'), uit.address);
|
||||
assert.notEqual(uit.address, rij().address);
|
||||
assert.equal(isSchijnwaarde(uit.address), true);
|
||||
assert.equal(uit.txid.length, 64);
|
||||
assert.equal(isSchijnwaarde(uit.txid), true);
|
||||
});
|
||||
|
||||
it('emmert hoeveelheden en laat koersen exact', () => {
|
||||
const uit = createTranslator({ secret: TEST_GEHEIM }).toAi(rij(), { entity: 'walletTransaction' });
|
||||
assert.equal(uit.valueSats, '100000000 tot 150000000');
|
||||
assert.equal(uit.fiatPrice, 54321.5);
|
||||
assert.equal(uit.direction, 'in');
|
||||
});
|
||||
|
||||
it('vertelt welke velden er doorgekomen zijn, zonder de waarden', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
t.toAi(rij(), { entity: 'walletTransaction' });
|
||||
const velden = t.fieldsShared();
|
||||
assert.ok(velden.includes('valueSats'));
|
||||
assert.equal(velden.includes('geenBekendVeld'), false);
|
||||
assert.equal(velden.includes('blockHeight'), false);
|
||||
});
|
||||
|
||||
it('deelt Preference.value alleen mét de tabel-hint', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const zonder = t.toAi({ scope: 'app', key: 'currency', value: 'eur' });
|
||||
assert.equal(Object.hasOwn(zonder, 'value'), false, 'zonder hint blijft `value` binnen');
|
||||
|
||||
const met = t.toAi({ scope: 'app', key: 'currency', value: 'eur' }, { table: 'Preference' });
|
||||
assert.equal(met.value, 'eur');
|
||||
assert.equal(met.scope, 'app');
|
||||
});
|
||||
|
||||
it('houdt de xpub binnen, ook met een tabel-hint erbij', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const uit = t.toAi({ id: 3, value: 'xpub6CUGRUo...' }, { table: 'WalletSource' });
|
||||
assert.equal(Object.hasOwn(uit, 'value'), false);
|
||||
assert.ok(!JSON.stringify(uit).includes('xpub'));
|
||||
});
|
||||
|
||||
it('schrijft de GEKWALIFICEERDE naam in het auditlog zodra de uitzondering is gebruikt', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
t.toAi({ scope: 'app', key: 'currency', value: 'eur' }, { table: 'Preference' });
|
||||
const velden = t.fieldsShared();
|
||||
assert.ok(velden.includes('Preference.value'));
|
||||
assert.equal(velden.includes('value'), false);
|
||||
assert.ok(velden.includes('scope'));
|
||||
assert.equal(velden.includes('Preference.scope'), false);
|
||||
});
|
||||
|
||||
it('erft de tabel niet naar een genest object', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const uit = t.toAi({ scope: 'app', source: { value: 'xpub6CUGRUo...' } }, { table: 'Preference' });
|
||||
assert.equal(uit.source?.value, undefined);
|
||||
});
|
||||
|
||||
it('werkt op diepte, niet alleen op de bovenste laag', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const uit = t.toAi({ id: 1, accounts: [{ id: 2, address: 'bc1qdiep', note: 'diep' }] });
|
||||
assert.equal(Object.hasOwn(uit, 'accounts'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('het choke point, terug', () => {
|
||||
it('vertaalt terug in lopende tekst, niet alleen in velden', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const uit = t.toAi({ id: 7, name: 'Bitvavo' }, { entity: 'account' });
|
||||
const antwoord = `Ik zie dat ${uit.id} (${uit.name}) een tekort heeft.`;
|
||||
const { text } = t.fromAi(antwoord);
|
||||
assert.equal(text, 'Ik zie dat 7 (Bitvavo) een tekort heeft.');
|
||||
});
|
||||
|
||||
it('kan geen handle verwarren met een andere, want ze zijn allemaal even lang', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const tokens = [];
|
||||
for (const naam of ['EEN', 'TWEE', 'DRIE', 'VIER', 'VIJF', 'ZES', 'ZEVEN', 'ACHT', 'NEGEN', 'TIEN']) {
|
||||
tokens.push(t.toAi({ id: naam }, { entity: 'account' }).id);
|
||||
}
|
||||
const lengtes = new Set(tokens.map(x => x.length));
|
||||
assert.equal(lengtes.size, 1, `handles zijn niet even lang: ${[...lengtes].join(', ')}`);
|
||||
assert.equal(new Set(tokens).size, tokens.length, 'twee entiteiten kregen dezelfde handle');
|
||||
for (const a of tokens) {
|
||||
for (const b of tokens) {
|
||||
if (a !== b) assert.equal(b.startsWith(a), false, `${a} is een prefix van ${b}`);
|
||||
}
|
||||
}
|
||||
const { text } = t.fromAi(`${tokens[9]} en ${tokens[0]}`);
|
||||
assert.equal(text, 'TIEN en EEN');
|
||||
});
|
||||
|
||||
it('laat een schijnwaarde die hij niet kent STAAN, en meldt hem', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const vreemd = schijnwaarde('address', 999);
|
||||
const { text, unresolved } = t.fromAi(`Kijk naar ${vreemd} eens.`);
|
||||
assert.ok(text.includes(vreemd), 'de onbekende schijnwaarde hoort te blijven staan');
|
||||
assert.deepEqual(unresolved, [vreemd]);
|
||||
});
|
||||
|
||||
it('meldt niets bij een tekst zonder schijnwaarden', () => {
|
||||
const t = createTranslator({ secret: TEST_GEHEIM });
|
||||
const { text, unresolved } = t.fromAi('Er is niets bijzonders gevonden.');
|
||||
assert.equal(text, 'Er is niets bijzonders gevonden.');
|
||||
assert.deepEqual(unresolved, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onbewerkt delen: de vertaalslag uit (ai.allow_raw)', () => {
|
||||
const RIJ = Object.freeze({
|
||||
id: 7,
|
||||
name: 'Spaarrekening bij de bank',
|
||||
amount: 12345.678,
|
||||
fiatFee: 4321,
|
||||
fiatPrice: 98765,
|
||||
address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
|
||||
occurredAt: '2026-01-02T03:04:05Z',
|
||||
});
|
||||
|
||||
const vertaald = () => createTranslator({ secret: TEST_GEHEIM });
|
||||
const onbewerkt = () => createTranslator({ secret: TEST_GEHEIM, precision: { rawValues: true } });
|
||||
|
||||
it('geeft bedragen als getal in plaats van als emmer', () => {
|
||||
const normaal = vertaald().toAi(RIJ, { entity: 'account' });
|
||||
const ruw = onbewerkt().toAi(RIJ, { entity: 'account' });
|
||||
|
||||
assert.equal(typeof normaal.amount, 'string', 'voorwaarde: normaal is het een emmer-label');
|
||||
assert.equal(typeof normaal.fiatFee, 'string');
|
||||
assert.equal(ruw.amount, 12345.678);
|
||||
assert.equal(ruw.fiatFee, 4321);
|
||||
assert.equal(ruw.fiatPrice, 98765, 'een koers ging altijd al ongefilterd mee');
|
||||
});
|
||||
|
||||
it('geeft namen en id\'s zoals ze zijn, in plaats van als handle', () => {
|
||||
const normaal = vertaald().toAi(RIJ, { entity: 'account' });
|
||||
const ruw = onbewerkt().toAi(RIJ, { entity: 'account' });
|
||||
|
||||
assert.match(normaal.name, /^text:[0-9a-f]{6}$/, 'voorwaarde: normaal is het een handle');
|
||||
assert.equal(ruw.name, 'Spaarrekening bij de bank');
|
||||
assert.equal(ruw.id, 7);
|
||||
});
|
||||
|
||||
it('geeft een adres echt mee in plaats van als schijnwaarde', () => {
|
||||
const ruw = onbewerkt().toAi(RIJ, { entity: 'account' });
|
||||
|
||||
assert.equal(ruw.address, RIJ.address);
|
||||
assert.equal(isSchijnwaarde(ruw.address), false, 'er hoort geen onmogelijke letter in te zitten');
|
||||
});
|
||||
|
||||
it('HOUDT DE ALLOWLIST OVEREIND: een ongedeclareerd veld gaat ook onbewerkt niet mee', () => {
|
||||
const ruw = onbewerkt().toAi({ ...RIJ, ietsOnbekends: 'geheim' }, { entity: 'account' });
|
||||
|
||||
assert.equal('ietsOnbekends' in ruw, false, 'de allowlist is een ander mechanisme en blijft gelden');
|
||||
assert.equal(ruw.name, 'Spaarrekening bij de bank', 'voorwaarde: de rest gaat wél mee');
|
||||
});
|
||||
|
||||
it('blijft melden wat er gedeeld is, want het auditlog moet ook hier kloppen', () => {
|
||||
const t = onbewerkt();
|
||||
t.beginCall();
|
||||
t.toAi(RIJ, { entity: 'account' });
|
||||
|
||||
const gedeeld = t.fieldsShared();
|
||||
assert.ok(gedeeld.includes('amount'), 'een gedeeld bedrag hoort in het logboek te staan');
|
||||
assert.ok(gedeeld.includes('name'));
|
||||
assert.ok(gedeeld.includes('address'));
|
||||
});
|
||||
|
||||
it('laat vrije tekst met rust bij het terugvertalen, want er is niets vervangen', () => {
|
||||
const t = onbewerkt();
|
||||
t.toAi(RIJ, { entity: 'account' });
|
||||
|
||||
const uit = t.fromAi('Het saldo van Spaarrekening bij de bank is 12345.678.');
|
||||
|
||||
assert.equal(uit.text, 'Het saldo van Spaarrekening bij de bank is 12345.678.');
|
||||
assert.deepEqual(uit.unresolved, []);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user