unrip/test/notification-layer.test.mjs
philipp 4a8766d4ae
All checks were successful
deploy / deploy (push) Successful in 1m1s
Publish startup settlement ntfy notifications
Proof: Targeted notification/history-writer tests pass; full npm test passes with 285 tests; operator dashboard bundle builds.

Assumptions: Startup outcome refresh may recompute settlements whose inventory movement occurred before process start, and notification_deliveries prevents duplicate ntfy posts for already delivered keys.

Still fake: Venue-native terminal fill ids and fee-complete realized PnL remain unavailable; ntfy remains an external utility service outside the unrip app deployment ownership.
2026-06-12 15:14:14 +02:00

225 lines
7.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildIntentRequestOutcomeNotification,
buildLiquidityActionNotification,
buildQuoteOutcomeNotification,
createNotificationDispatcher,
hasProvenSettlement,
outcomeNotificationObservedAtOrAfter,
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('observed timestamp recency filter rejects stale outcome observations', () => {
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);
});
test('outcome notification recency accepts settlements computed after their observed movement', () => {
assert.equal(outcomeNotificationObservedAtOrAfter({
outcome_observed_at: '2026-06-12T13:02:32.053Z',
computed_at: '2026-06-12T13:08:42.800Z',
}, '2026-06-12T13:08:10.000Z'), true);
assert.equal(outcomeNotificationObservedAtOrAfter({
payload: {
outcome_observed_at: '2026-06-12T13:02:32.053Z',
computed_at: '2026-06-12T13:08:42.800Z',
},
}, '2026-06-12T13:08:10.000Z'), true);
assert.equal(outcomeNotificationObservedAtOrAfter({
outcome_observed_at: '2026-06-12T13:02:32.053Z',
computed_at: '2026-06-12T13:07:42.800Z',
}, '2026-06-12T13:08:10.000Z'), false);
});