Proof: npm test passed 159/159; focused node --test for service-snapshot-summary and ops-sentinel static coverage passed; failed rollout showed ops-sentinel could not import src/core/service-snapshot-summary.mjs and this commit adds the module plus regression coverage. Assumptions: Forgejo main push remains the deployment path; no manual kubectl reconciliation is needed because the failed rollout will be superseded by the repo workflow. Still fake: no live funds movement; production rollout evidence still depends on the follow-up Forgejo deployment completing.
This commit is contained in:
parent
2ffa4b17f1
commit
af87a08a19
3 changed files with 214 additions and 0 deletions
150
src/core/service-snapshot-summary.mjs
Normal file
150
src/core/service-snapshot-summary.mjs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
const COMMON_STATE_FIELDS = [
|
||||||
|
'paused',
|
||||||
|
'armed',
|
||||||
|
'last_error',
|
||||||
|
'last_event_at',
|
||||||
|
'last_runtime_eval_at',
|
||||||
|
'last_published_at',
|
||||||
|
'last_publish_error',
|
||||||
|
'publish_count',
|
||||||
|
'last_sync_at',
|
||||||
|
'last_write_at',
|
||||||
|
'last_metrics_at',
|
||||||
|
'last_quote_outcomes_at',
|
||||||
|
'database_connectivity',
|
||||||
|
'source_error_count',
|
||||||
|
'last_source_error_at',
|
||||||
|
'last_bootstrap_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function summarizeServiceSnapshotForSentinel(snapshot = {}) {
|
||||||
|
return {
|
||||||
|
service: snapshot.service,
|
||||||
|
label: snapshot.label,
|
||||||
|
base_url: snapshot.base_url,
|
||||||
|
reachable: snapshot.reachable,
|
||||||
|
state: summarizeServiceState(snapshot.service, snapshot.state),
|
||||||
|
health: snapshot.health || null,
|
||||||
|
error: snapshot.error || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeServiceState(service, state) {
|
||||||
|
if (!state || typeof state !== 'object') return state || null;
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case 'near-intents-ingest':
|
||||||
|
return pickFields(state, ['service', 'namespace', 'pair_filter', 'ingest']);
|
||||||
|
case 'market-reference-ingest':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'paused',
|
||||||
|
'kraken',
|
||||||
|
'coingecko',
|
||||||
|
'last_published_at',
|
||||||
|
'last_publish_error',
|
||||||
|
'publish_count',
|
||||||
|
'error_count',
|
||||||
|
'active_pair',
|
||||||
|
]);
|
||||||
|
case 'inventory-sync':
|
||||||
|
return {
|
||||||
|
...pickFields(state, ['service', 'namespace', 'account_id', 'paused', 'last_sync_at', 'last_error', 'publish_count']),
|
||||||
|
last_snapshot: summarizeInventorySnapshot(state.last_snapshot),
|
||||||
|
};
|
||||||
|
case 'liquidity-manager':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'account_id',
|
||||||
|
'paused',
|
||||||
|
'funding_observer_paused',
|
||||||
|
'withdrawals_frozen',
|
||||||
|
'deposit_addresses',
|
||||||
|
'withdrawal_defaults',
|
||||||
|
'latest_funding_observation_at',
|
||||||
|
'last_refresh_at',
|
||||||
|
'last_error',
|
||||||
|
]);
|
||||||
|
case 'history-writer':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'database_connectivity',
|
||||||
|
'last_write_at',
|
||||||
|
'last_alert_write_at',
|
||||||
|
'last_funding_observation_write_at',
|
||||||
|
'last_environment_status_write_at',
|
||||||
|
'last_environment_status_seen_at',
|
||||||
|
'last_environment_status_duplicate_at',
|
||||||
|
'last_metrics_at',
|
||||||
|
'last_quote_outcomes_at',
|
||||||
|
'latest_portfolio_metrics',
|
||||||
|
'latest_quote_outcomes',
|
||||||
|
'offsets',
|
||||||
|
'metrics_error',
|
||||||
|
'quote_outcomes_error',
|
||||||
|
]);
|
||||||
|
case 'strategy-engine':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'paused',
|
||||||
|
'armed',
|
||||||
|
'last_error',
|
||||||
|
'latest_decision',
|
||||||
|
'latest_inventory_event',
|
||||||
|
'decision_count',
|
||||||
|
'command_count',
|
||||||
|
'reject_count',
|
||||||
|
'block_count',
|
||||||
|
]);
|
||||||
|
case 'trade-executor':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'paused',
|
||||||
|
'armed',
|
||||||
|
'last_error',
|
||||||
|
'relay',
|
||||||
|
'last_result',
|
||||||
|
'last_quote_status',
|
||||||
|
'submitted_count',
|
||||||
|
'failed_count',
|
||||||
|
'blocked_count',
|
||||||
|
]);
|
||||||
|
case 'operator-dashboard':
|
||||||
|
return pickFields(state, [
|
||||||
|
'service',
|
||||||
|
'namespace',
|
||||||
|
'source_error_count',
|
||||||
|
'last_source_error_at',
|
||||||
|
'last_bootstrap_at',
|
||||||
|
'source_errors',
|
||||||
|
]);
|
||||||
|
default:
|
||||||
|
return pickFields(state, COMMON_STATE_FIELDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeInventorySnapshot(snapshot) {
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') return snapshot || null;
|
||||||
|
return pickFields(snapshot, [
|
||||||
|
'inventory_id',
|
||||||
|
'account_id',
|
||||||
|
'synced_at',
|
||||||
|
'spendable',
|
||||||
|
'pending_inbound',
|
||||||
|
'pending_outbound',
|
||||||
|
'reconciliation_status',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFields(source, fields) {
|
||||||
|
const picked = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
if (source[field] !== undefined) picked[field] = source[field];
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
@ -20,3 +20,12 @@ test('ops sentinel polls official NEAR status and publishes environment status w
|
||||||
assert.match(source, /normalized\.status_fingerprint === state\.last_environment_status_fingerprint/);
|
assert.match(source, /normalized\.status_fingerprint === state\.last_environment_status_fingerprint/);
|
||||||
assert.match(source, /assertEnvironmentStatusEvent/);
|
assert.match(source, /assertEnvironmentStatusEvent/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ops sentinel exposes trimmed service snapshots and computed runtime alerts', () => {
|
||||||
|
assert.match(source, /summarizeServiceSnapshotForSentinel/);
|
||||||
|
assert.match(source, /serviceSnapshots\.map\(summarizeServiceSnapshotForSentinel\)/);
|
||||||
|
assert.match(source, /latest_runtime_alerts: state\.latest_runtime_alerts/);
|
||||||
|
assert.match(source, /active_alerts: state\.latest_runtime_alerts/);
|
||||||
|
assert.match(source, /activeAlerts: desiredRuntimeAlerts/);
|
||||||
|
assert.match(source, /state\.latest_runtime_alerts = desiredRuntimeAlerts/);
|
||||||
|
});
|
||||||
|
|
|
||||||
55
test/service-snapshot-summary.test.mjs
Normal file
55
test/service-snapshot-summary.test.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
summarizeServiceSnapshotForSentinel,
|
||||||
|
summarizeServiceState,
|
||||||
|
} from '../src/core/service-snapshot-summary.mjs';
|
||||||
|
|
||||||
|
test('sentinel snapshot summary keeps trade-executor health fields and drops retained command maps', () => {
|
||||||
|
const summary = summarizeServiceSnapshotForSentinel({
|
||||||
|
service: 'trade-executor',
|
||||||
|
label: 'Trade Executor',
|
||||||
|
base_url: 'http://trade-executor',
|
||||||
|
reachable: true,
|
||||||
|
health: { ok: true },
|
||||||
|
state: {
|
||||||
|
service: 'trade-executor',
|
||||||
|
namespace: 'unrip',
|
||||||
|
paused: false,
|
||||||
|
armed: true,
|
||||||
|
relay: { connected: true, last_message_at: '2026-05-06T15:00:00.000Z' },
|
||||||
|
last_result: { status: 'submitted' },
|
||||||
|
submitted_count: 100,
|
||||||
|
processed_idempotency_keys: Object.fromEntries(Array.from({ length: 100 }, (_, index) => [`quote:${index}`, true])),
|
||||||
|
submitted_commands: {
|
||||||
|
'cmd-1': { quote_id: 'quote-1', raw_response: { large: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(summary.state.armed, true);
|
||||||
|
assert.deepEqual(summary.state.relay, { connected: true, last_message_at: '2026-05-06T15:00:00.000Z' });
|
||||||
|
assert.equal(summary.state.processed_idempotency_keys, undefined);
|
||||||
|
assert.equal(summary.state.submitted_commands, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sentinel snapshot summary keeps near-intents ingest truth fields', () => {
|
||||||
|
const state = summarizeServiceState('near-intents-ingest', {
|
||||||
|
service: 'near-intents-ingest',
|
||||||
|
namespace: 'unrip',
|
||||||
|
pair_filter: { pair: 'btc->eure' },
|
||||||
|
ingest: {
|
||||||
|
connected: true,
|
||||||
|
last_message_at: '2026-05-06T15:00:00.000Z',
|
||||||
|
last_matching_quote_at: '2026-05-06T09:56:42.742Z',
|
||||||
|
last_published_at: '2026-05-06T09:56:42.744Z',
|
||||||
|
filtered_count: 10,
|
||||||
|
},
|
||||||
|
bulky_unused_field: Object.fromEntries(Array.from({ length: 100 }, (_, index) => [index, index])),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(state.ingest.connected, true);
|
||||||
|
assert.equal(state.ingest.last_matching_quote_at, '2026-05-06T09:56:42.742Z');
|
||||||
|
assert.equal(state.bulky_unused_field, undefined);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue