import test from 'node:test'; import assert from 'node:assert/strict'; import { buildIntentRequestOutcomeNotification, buildLiquidityActionNotification, buildQuoteOutcomeNotification, createNotificationDispatcher, hasProvenSettlement, observedAtOrAfter, } from '../src/core/notification-layer.mjs'; const BTC = { assetId: 'nep141:btc.omft.near', symbol: 'BTC', decimals: 8 }; const EURE = { assetId: 'nep141:eure.omft.near', symbol: 'EURe', decimals: 18 }; const config = { tradingBtc: BTC, tradingEure: EURE, assetRegistry: new Map([ [BTC.assetId, BTC], [EURE.assetId, EURE], ]), }; test('liquidity notifications only emit credited deposits and completed withdrawals', () => { assert.equal(buildLiquidityActionNotification({ config, event: { payload: { action_type: 'deposit_status_observed', status: 'PENDING', chain: 'eth:100', asset_id: EURE.assetId, details: { amount: '5000000000000000000', tx_hash: 'deposit-tx-1' }, }, }, }), null); const deposit = buildLiquidityActionNotification({ config, event: { payload: { action_type: 'deposit_status_observed', status: 'CREDITED', chain: 'eth:100', asset_id: EURE.assetId, details: { amount: '5000000000000000000', tx_hash: 'deposit-tx-1' }, }, }, }); assert.equal(deposit.notification_type, 'deposit_credited'); assert.match(deposit.message, /Deposit credited: 5 EURe/); assert.match(deposit.message, /tx=deposit-tx-1/); assert.equal(buildLiquidityActionNotification({ config, event: { payload: { action_type: 'withdrawal_status_changed', status: 'SUBMITTED', chain: 'btc:mainnet', asset_id: BTC.assetId, details: { amount: '100000', withdrawal_hash: 'withdrawal-1' }, }, }, }), null); const withdrawal = buildLiquidityActionNotification({ config, event: { payload: { action_type: 'withdrawal_status_changed', status: 'COMPLETED', chain: 'btc:mainnet', asset_id: BTC.assetId, details: { amount: '100000', withdrawal_hash: 'withdrawal-1', transfer_tx_hash: 'btc-tx-1' }, }, }, }); assert.equal(withdrawal.notification_type, 'withdrawal_completed'); assert.match(withdrawal.message, /Withdrawal completed: 0.001 BTC/); assert.match(withdrawal.message, /transfer=btc-tx-1/); }); test('trade notifications require completed outcome with durable settlement evidence', () => { const submitted = { quote_id: 'quote-submitted', outcome_status: 'submitted', attribution_status: 'unattributed', attributed_inventory_delta: null, }; assert.equal(hasProvenSettlement(submitted), false); assert.equal(buildQuoteOutcomeNotification({ record: submitted, config }), null); const completedWithoutDelta = { quote_id: 'quote-fake-completed', outcome_status: 'completed', attribution_status: 'unattributed', attributed_inventory_delta: null, }; assert.equal(hasProvenSettlement(completedWithoutDelta), false); assert.equal(buildQuoteOutcomeNotification({ record: completedWithoutDelta, config }), null); const completed = { quote_id: 'quote-real', decision_id: 'decision-real', command_id: 'command-real', gross_edge_pct: '0.49', outcome_status: 'completed', attribution_status: 'heuristic_match', attributed_inventory_delta: { inventory_id: 'inventory-after', delta_units: { [BTC.assetId]: '-12345', [EURE.assetId]: '76500000000000000000', }, }, }; const notification = buildQuoteOutcomeNotification({ record: completed, config }); assert.equal(notification.notification_type, 'trade_completed'); assert.match(notification.message, /Quote trade completed: quote-real/); assert.match(notification.message, /-0.00012345 BTC/); assert.match(notification.message, /\+76.5 EURe/); }); test('own request notifications do not emit from relay acceptance alone', () => { const accepted = { request_id: 'request-accepted', outcome_status: 'awaiting_settlement', submission_status: 'accepted_by_relay', relay_status: 'SETTLED', attribution_status: 'unattributed', attributed_inventory_delta: null, }; assert.equal(buildIntentRequestOutcomeNotification({ record: accepted, config }), null); const completed = { request_id: 'request-completed', idempotency_key: 'idem-1', intent_hash: 'intent-hash-1', outcome_status: 'completed', attribution_status: 'linked_settlement', attributed_inventory_delta: { inventory_id: 'inventory-after-request', delta_units: { [EURE.assetId]: '-5000000000000000000', [BTC.assetId]: '8000', }, }, }; const notification = buildIntentRequestOutcomeNotification({ record: completed, config }); assert.equal(notification.notification_type, 'trade_completed'); assert.match(notification.message, /Own request completed: request-completed/); assert.match(notification.message, /-5 EURe/); assert.match(notification.message, /\+0.00008 BTC/); }); test('notification dispatcher publishes each durable notification key once', async () => { const calls = []; const claimed = new Set(); const finished = []; const dispatcher = createNotificationDispatcher({ client: { isConfigured: () => true, async publish(notification) { calls.push(notification); return { ok: true, status: 200, response: '{"id":"msg-1"}' }; }, }, store: { async claim(notification) { if (claimed.has(notification.notification_key)) return false; claimed.add(notification.notification_key); return true; }, async finish(notification, result) { finished.push({ notification, result }); }, }, }); const notification = { notification_key: 'deposit_credited:eth:100:eure:tx-1:5000', notification_type: 'deposit_credited', source_kind: 'liquidity_action', source_id: 'tx-1', title: 'Deposit credited', message: 'Deposit credited: 5 EURe', priority: 'default', tags: ['unrip', 'deposit'], }; assert.equal((await dispatcher.publishOnce(notification)).delivered, true); assert.deepEqual(await dispatcher.publishOnce(notification), { ok: true, skipped: true, reason: 'notification_already_delivered', }); assert.equal(calls.length, 1); assert.equal(finished.length, 1); }); test('outcome recency filter keeps startup refresh from sending historical notifications', () => { assert.equal(observedAtOrAfter('2026-04-16T10:00:00.000Z', '2026-04-16T09:59:59.000Z'), true); assert.equal(observedAtOrAfter('2026-04-16T09:00:00.000Z', '2026-04-16T09:59:59.000Z'), false); });