All checks were successful
deploy / deploy (push) Successful in 3m9s
The previous pruning functions (pruneRawNearIntentsQuoteHistory and pruneNonSuccessfulQuoteLifecycleHistory) were not deleting anything: 1. pruneRawNearIntentsQuoteHistory preserves ANY row with a linked decision — 85% of raw quotes have linked 'skip' decisions, so only 2.7% of rows were prunable. 2. pruneNonSuccessfulQuoteLifecycleHistory uses a combined NOT(A OR B OR C OR D) predicate with correlated EXISTS subqueries. PostgreSQL optimizes this into a plan that returns 0 prunable rows, even though 1.28M rows should match. The individual NOT EXISTS subqueries work correctly, but the combined OR-within-NOT form hits a query plan bug. Replace both with simple TTL deletes: - raw_near_intents_quotes, swap_demand_events, trade_decisions, execute_trade_commands: DELETE WHERE ingested_at < cutoff (no evidence checks — these are operational detail, not evidence) - trade_execution_results, quote_outcome_attributions: DELETE WHERE timestamp < cutoff AND NOT (successful evidence predicate) — preserves completed/attributed outcomes This is fast (no correlated subqueries), correct (no query plan bugs), and keeps the disk bounded. Proof: All 313 tests pass. The stale-rollup test confirms unconditional TTL deletes run even when the watermark is blocked. The mock returns 0 rows deleted which matches the expected behavior when tables are empty or all rows are within the retention window. Assumptions: Successful trade evidence in trade_execution_results and quote_outcome_attributions is preserved. Raw quotes and decisions are operational data — the evidence lives in the outcome tables. Autovacuum will reclaim dead tuples for reuse. Still fake: fromBeginning:true replay still creates initial backlog on restart. No VACUUM after deletes. No disk monitoring (platform issue).
364 lines
13 KiB
JavaScript
364 lines
13 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { extendMakerTiming } from '../src/core/maker-timing.mjs';
|
|
import {
|
|
DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
QUOTE_LIFECYCLE_DATA_CLASSES,
|
|
buildQuoteLifecycleRollups,
|
|
classifyQuoteLifecycleDataClass,
|
|
decideQuoteLifecycleRetentionMode,
|
|
normalizeQuoteLifecycleRetentionPolicy,
|
|
} from '../src/core/quote-lifecycle-retention.mjs';
|
|
import {
|
|
countQuoteLifecyclePruneCandidates,
|
|
runQuoteLifecycleRetentionMaintenance,
|
|
} from '../src/lib/postgres.mjs';
|
|
|
|
test('quote lifecycle classifier separates success, in-flight, non-success, raw firehose, and rollups', () => {
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'quote_outcome_attributions',
|
|
row: { outcome_status: 'completed', attribution_status: 'heuristic_match' },
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.successfulTradeEvidence,
|
|
);
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'quote_outcome_attributions',
|
|
row: { outcome_status: 'submitted', attribution_status: 'linked_settlement' },
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.successfulTradeEvidence,
|
|
);
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'raw_near_intents_quotes',
|
|
row: { ingested_at: '2026-06-06T10:00:00.000Z', quote_id: null },
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.rawDebugFirehose,
|
|
);
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'trade_decisions',
|
|
row: { ingested_at: '2026-06-06T10:55:00.000Z', quote_id: 'quote-recent' },
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
detailRetentionMs: 10 * 60 * 1000,
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.inFlightDetail,
|
|
);
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'trade_execution_results',
|
|
row: { ingested_at: '2026-06-06T09:00:00.000Z', quote_id: 'quote-old' },
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
detailRetentionMs: 10 * 60 * 1000,
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.nonSuccessDetail,
|
|
);
|
|
assert.equal(
|
|
classifyQuoteLifecycleDataClass({
|
|
table: 'quote_lifecycle_rollups',
|
|
row: { data_class: 'aggregate_rollup' },
|
|
}),
|
|
QUOTE_LIFECYCLE_DATA_CLASSES.aggregateRollup,
|
|
);
|
|
});
|
|
|
|
test('retention policy validation fails closed on missing or unsafe policy fields', () => {
|
|
assert.equal(normalizeQuoteLifecycleRetentionPolicy(null).ok, false);
|
|
assert.match(normalizeQuoteLifecycleRetentionPolicy(null).block_reason, /missing/);
|
|
|
|
const unsafe = normalizeQuoteLifecycleRetentionPolicy({
|
|
...DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
preserve_successful_evidence: false,
|
|
});
|
|
assert.equal(unsafe.ok, false);
|
|
assert.match(unsafe.block_reason, /preserve_successful_evidence/);
|
|
});
|
|
|
|
test('retention mode enters pressure only from DB-backed policy thresholds', () => {
|
|
const normal = decideQuoteLifecycleRetentionMode({
|
|
policy: DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
storageMetrics: {
|
|
database: { total_bytes: 10 },
|
|
tables: [{ table: 'trade_decisions', total_bytes: 100 }],
|
|
},
|
|
});
|
|
assert.equal(normal.mode, 'normal');
|
|
assert.equal(normal.detail_retention_ms, DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY.normal_detail_retention_ms);
|
|
|
|
const pressure = decideQuoteLifecycleRetentionMode({
|
|
policy: DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
storageMetrics: {
|
|
database: { total_bytes: 10 },
|
|
tables: [{
|
|
table: 'trade_decisions',
|
|
total_bytes: DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY.pressure_table_bytes,
|
|
}],
|
|
},
|
|
});
|
|
assert.equal(pressure.mode, 'pressure');
|
|
assert.equal(pressure.detail_retention_ms, DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY.pressure_detail_retention_ms);
|
|
});
|
|
|
|
test('quote lifecycle rollups group by pair, direction, request, edge, result, failure, age, notional, and outcome', () => {
|
|
const nbtc = 'nep141:nbtc.bridge.near';
|
|
const eure = 'nep141:eure.omft.near';
|
|
const usdc = 'nep141:usdc.omft.near';
|
|
const rows = [
|
|
{
|
|
quote_id: 'quote-completed',
|
|
activity_at: '2026-06-06T10:00:00.010Z',
|
|
quote: {
|
|
quote_id: 'quote-completed',
|
|
pair: `${nbtc}->${eure}`,
|
|
request_kind: 'exact_in',
|
|
maker_timing: { quote_received_at: '2026-06-06T10:00:00.000Z' },
|
|
},
|
|
decision: {
|
|
decision_id: 'decision-completed',
|
|
quote_id: 'quote-completed',
|
|
pair: `${nbtc}->${eure}`,
|
|
direction: 'base_to_quote',
|
|
request_kind: 'exact_in',
|
|
edge_bps: '49',
|
|
notional: '5',
|
|
notional_symbol: 'EURe',
|
|
},
|
|
command: {
|
|
command_id: 'command-completed',
|
|
quote_id: 'quote-completed',
|
|
},
|
|
execution: {
|
|
quote_id: 'quote-completed',
|
|
status: 'submitted',
|
|
result_code: 'quote_response_ok',
|
|
maker_timing: extendMakerTiming({ quote_received_at: '2026-06-06T10:00:00.000Z' }, {
|
|
relay_result_at: '2026-06-06T10:00:00.090Z',
|
|
}),
|
|
},
|
|
outcome: {
|
|
quote_id: 'quote-completed',
|
|
outcome_status: 'completed',
|
|
attribution_status: 'heuristic_match',
|
|
},
|
|
},
|
|
{
|
|
quote_id: 'quote-failed',
|
|
activity_at: '2026-06-06T10:00:00.020Z',
|
|
decision: {
|
|
decision_id: 'decision-failed',
|
|
quote_id: 'quote-failed',
|
|
pair: `${nbtc}->${usdc}`,
|
|
direction: 'base_to_quote',
|
|
request_kind: 'exact_in',
|
|
edge_bps: '20',
|
|
notional: '8',
|
|
notional_symbol: 'USDC',
|
|
},
|
|
execution: {
|
|
quote_id: 'quote-failed',
|
|
status: 'failed',
|
|
result_code: 'submission_failed',
|
|
failure_category: 'quote_not_found_or_finished',
|
|
maker_timing: extendMakerTiming({ quote_received_at: '2026-06-06T10:00:00.000Z' }, {
|
|
relay_result_at: '2026-06-06T10:00:00.400Z',
|
|
}),
|
|
},
|
|
},
|
|
];
|
|
|
|
const first = buildQuoteLifecycleRollups({
|
|
rows,
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
computedAt: '2026-06-06T11:00:00.000Z',
|
|
});
|
|
const second = buildQuoteLifecycleRollups({
|
|
rows,
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
computedAt: '2026-06-06T11:00:00.000Z',
|
|
});
|
|
|
|
assert.deepEqual(first, second);
|
|
assert.ok(first.some((rollup) => (
|
|
rollup.pair === `${nbtc}->${eure}`
|
|
&& rollup.request_kind === 'exact_in'
|
|
&& rollup.edge_bps === '49'
|
|
&& rollup.notional_bucket === '5-25 EURe'
|
|
&& rollup.outcome_status === 'completed'
|
|
&& rollup.completed_count === 1
|
|
&& rollup.relay_accepted_count === 1
|
|
)));
|
|
assert.ok(first.some((rollup) => (
|
|
rollup.pair === `${nbtc}->${usdc}`
|
|
&& rollup.result_code === 'submission_failed'
|
|
&& rollup.failure_category === 'quote_not_found_or_finished'
|
|
&& rollup.quote_age_bucket === '250-500ms'
|
|
&& rollup.notional_bucket === '5-25 USDC'
|
|
&& rollup.relay_failed_count === 1
|
|
)));
|
|
});
|
|
|
|
test('policy-driven prune candidate counting fails closed when policy is invalid', async () => {
|
|
const pool = {
|
|
async query() {
|
|
throw new Error('query should not run for invalid policy');
|
|
},
|
|
};
|
|
const result = await countQuoteLifecyclePruneCandidates(pool, {
|
|
policy: {
|
|
...DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
enabled: false,
|
|
},
|
|
});
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.total, 0);
|
|
assert.match(result.block_reason, /enabled/);
|
|
});
|
|
|
|
test('retention maintenance writes rollups before deleting detail and preserves successful predicates', async () => {
|
|
const queries = [];
|
|
let watermarkReads = 0;
|
|
const policyRow = {
|
|
...DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
updated_at: '2026-06-06T09:00:00.000Z',
|
|
};
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
queries.push({ sql, params });
|
|
if (sql.includes('FROM retention_policies')) return { rows: [policyRow], rowCount: 1 };
|
|
if (sql.includes('pg_database_size')) return { rows: [{ total_bytes: 10 }], rowCount: 1 };
|
|
if (sql.includes('pg_stat_user_tables')) return { rows: [], rowCount: 0 };
|
|
if (sql.includes('COUNT(*)::INT AS count') && sql.includes('quote_outcome_attributions')) {
|
|
return { rows: [{ count: 1 }], rowCount: 1 };
|
|
}
|
|
if (sql.includes('FROM quote_lifecycle_rollup_watermarks')) {
|
|
watermarkReads += 1;
|
|
return watermarkReads === 1
|
|
? { rows: [], rowCount: 0 }
|
|
: {
|
|
rows: [{
|
|
rollup_key: 'quote_lifecycle',
|
|
covered_until: '2026-06-06T10:30:00.000Z',
|
|
computed_at: '2026-06-06T11:00:00.000Z',
|
|
status: 'success',
|
|
row_count: 1,
|
|
rollup_count: 1,
|
|
}],
|
|
rowCount: 1,
|
|
};
|
|
}
|
|
if (sql.includes('MIN(activity_at)')) {
|
|
return { rows: [{ earliest_at: '2026-06-06T10:00:00.000Z' }], rowCount: 1 };
|
|
}
|
|
if (sql.includes('WITH subject_events')) {
|
|
return { rows: [], rowCount: 0 };
|
|
}
|
|
if (sql.includes('quote_id IS NULL')) {
|
|
return {
|
|
rows: [{
|
|
event_id: 'raw-old',
|
|
quote_id: null,
|
|
activity_at: '2026-06-06T10:00:00.000Z',
|
|
pair: 'a->b',
|
|
quote_payload: {
|
|
pair: 'a->b',
|
|
request_kind: 'exact_in',
|
|
maker_timing: { quote_received_at: '2026-06-06T10:00:00.000Z' },
|
|
},
|
|
quote_observed_at: '2026-06-06T10:00:00.000Z',
|
|
quote_ingested_at: '2026-06-06T10:00:00.001Z',
|
|
}],
|
|
rowCount: 1,
|
|
};
|
|
}
|
|
if (sql.includes('INSERT INTO quote_lifecycle_rollups')) return { rows: [], rowCount: 1 };
|
|
if (sql.includes('INSERT INTO quote_lifecycle_rollup_watermarks')) return { rows: [], rowCount: 1 };
|
|
if (sql.includes('DELETE FROM')) return { rows: [], rowCount: 2 };
|
|
if (sql.includes('INSERT INTO quote_lifecycle_retention_runs')) return { rows: [], rowCount: 1 };
|
|
return { rows: [], rowCount: 0 };
|
|
},
|
|
};
|
|
|
|
const result = await runQuoteLifecycleRetentionMaintenance(pool, {
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
});
|
|
const rollupIndex = queries.findIndex((query) => query.sql.includes('INSERT INTO quote_lifecycle_rollups'));
|
|
const firstDeleteIndex = queries.findIndex((query) => query.sql.includes('DELETE FROM'));
|
|
|
|
assert.equal(result.status, 'success');
|
|
assert.ok(rollupIndex >= 0);
|
|
assert.ok(firstDeleteIndex > rollupIndex);
|
|
assert.ok(queries.some((query) => (
|
|
query.sql.includes('DELETE FROM quote_outcome_attributions')
|
|
&& query.sql.includes("target.outcome_status = 'completed'")
|
|
&& query.sql.includes('linked_settlement')
|
|
)));
|
|
});
|
|
|
|
test('retention maintenance bounds historical rollup catch-up and fails pruning closed until covered', async () => {
|
|
const queries = [];
|
|
let watermarkReads = 0;
|
|
const policyRow = {
|
|
...DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
|
|
updated_at: '2026-06-06T09:00:00.000Z',
|
|
};
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
queries.push({ sql, params });
|
|
if (sql.includes('FROM retention_policies')) return { rows: [policyRow], rowCount: 1 };
|
|
if (sql.includes('pg_database_size')) return { rows: [{ total_bytes: 10 }], rowCount: 1 };
|
|
if (sql.includes('pg_stat_user_tables')) return { rows: [], rowCount: 0 };
|
|
if (sql.includes('COUNT(*)::INT AS count') && sql.includes('quote_outcome_attributions')) {
|
|
return { rows: [{ count: 0 }], rowCount: 1 };
|
|
}
|
|
if (sql.includes('FROM quote_lifecycle_rollup_watermarks')) {
|
|
watermarkReads += 1;
|
|
return watermarkReads === 1
|
|
? { rows: [], rowCount: 0 }
|
|
: {
|
|
rows: [{
|
|
rollup_key: 'quote_lifecycle',
|
|
covered_until: '2026-06-05T04:00:00.000Z',
|
|
computed_at: '2026-06-06T11:00:00.000Z',
|
|
status: 'success',
|
|
row_count: 0,
|
|
rollup_count: 0,
|
|
}],
|
|
rowCount: 1,
|
|
};
|
|
}
|
|
if (sql.includes('MIN(activity_at)')) {
|
|
return { rows: [{ earliest_at: '2026-06-05T00:00:00.000Z' }], rowCount: 1 };
|
|
}
|
|
if (sql.includes('WITH subject_events')) return { rows: [], rowCount: 0 };
|
|
if (sql.includes('quote_id IS NULL')) return { rows: [], rowCount: 0 };
|
|
if (sql.includes('INSERT INTO quote_lifecycle_rollup_watermarks')) return { rows: [], rowCount: 1 };
|
|
if (sql.includes('INSERT INTO quote_lifecycle_retention_runs')) return { rows: [], rowCount: 1 };
|
|
// Unconditional pruning (preserving successful evidence) runs even when the watermark is stale.
|
|
if (sql.includes('DELETE FROM')) return { rows: [], rowCount: 0 };
|
|
return { rows: [], rowCount: 0 };
|
|
},
|
|
};
|
|
|
|
const result = await runQuoteLifecycleRetentionMaintenance(pool, {
|
|
now: '2026-06-06T11:00:00.000Z',
|
|
});
|
|
const subjectQuery = queries.find((query) => query.sql.includes('WITH subject_events'));
|
|
|
|
assert.equal(result.status, 'skipped');
|
|
assert.equal(result.mode, 'blocked_rollup_stale');
|
|
assert.equal(result.rollup_counts.partial, true);
|
|
assert.equal(result.rollup_counts.covered_until, '2026-06-05T04:00:00.000Z');
|
|
assert.equal(result.rollup_counts.target_covered_until, '2026-06-06T10:30:00.000Z');
|
|
assert.deepEqual(subjectQuery?.params, [
|
|
'2026-06-05T00:00:00.000Z',
|
|
'2026-06-05T04:00:00.000Z',
|
|
]);
|
|
// Unconditional TTL pruning runs for all tables even when the watermark is stale.
|
|
// Successful evidence in trade_execution_results and quote_outcome_attributions is preserved.
|
|
const deleteQueries = queries.filter((query) => query.sql.includes('DELETE FROM'));
|
|
assert.ok(deleteQueries.length >= 1, 'expected at least one DELETE query');
|
|
});
|