SPB Git

Repository Public

hardening-e.test.ts 5.6 KB 63 lines typescript

1// V8 hardening (E): the start-up summary of optional capabilities (variable NAMES only, never a value) and the heartbeat of2// the agent engines in loopStats. Pure / in-memory: no database.3import { describe, expect, it } from 'vitest';4import { capabilities, capabilitiesLine } from '../src/capabilities.ts';5import { engineLoop, enginePassFailure, loopStats } from '../src/loop-stats.ts';67const SECRET = 's3cr3t-value-that-must-never-be-printed-0123456789';8describe('optional capabilities', () => {9  it('says which are on and names the missing variable for the others — and never prints a value', () => {10    const none = capabilities({});11    const by = Object.fromEntries(none.map((c) => [c.id, c]));12    expect(by.deriv_live_mainnet).toMatchObject({ on: false, missing: ['SPW_DERIV_LIVE_MAINNET'] });13    expect(by.deriv_mandate_mainnet).toMatchObject({ on: false, missing: ['SPW_DERIV_LIVE_MAINNET', 'SPW_DERIV_MANDATE_MAINNET'] });14    expect(by.deriv_gmx_live).toMatchObject({ on: false, missing: ['SPW_DERIV_LIVE_MAINNET', 'SPW_DERIV_GMX_LIVE'] });15    expect(by.research_runner).toMatchObject({ on: false, missing: ['SPW_RESEARCH_RUNNER_SECRET'] });16    expect(by.research_readonly_db).toMatchObject({ on: false, missing: ['RESEARCH_DATABASE_URL'] });17    expect(by.broadcast!.on).toBe(true); expect(by.workers!.on).toBe(true); expect(by.strategy_sandbox_enforced!.on).toBe(true);18    const line = capabilitiesLine(none);19    expect(line).toMatch(/^capabilities: deriv_live_mainnet=OFF\(missing SPW_DERIV_LIVE_MAINNET\) deriv_mandate_mainnet=OFF\(missing SPW_DERIV_LIVE_MAINNET\+SPW_DERIV_MANDATE_MAINNET\) deriv_gmx_live=OFF\(missing SPW_DERIV_LIVE_MAINNET\+SPW_DERIV_GMX_LIVE\) research_runner=OFF\(missing SPW_RESEARCH_RUNNER_SECRET\) research_readonly_db=OFF\(missing RESEARCH_DATABASE_URL\) broadcast=on workers=on strategy_sandbox_enforced=on/);20    expect(line.split('\n')).toHaveLength(1);2122    const env = { SPW_DERIV_LIVE_MAINNET: 'on', SPW_DERIV_MANDATE_MAINNET: 'on', SPW_DERIV_GMX_LIVE: 'on', SPW_RESEARCH_RUNNER_SECRET: SECRET, RESEARCH_DATABASE_URL: `postgres://reader:${SECRET}@db/sp_wallet`, SPW_BROADCAST: 'off', SPW_LAB_SANDBOX: 'off', SPW_WEB_DIR: '/srv/web' };23    const all = capabilities(env), text = JSON.stringify(all) + capabilitiesLine(all);24    expect(all.filter((c) => c.on).map((c) => c.id)).toEqual(['deriv_live_mainnet', 'deriv_mandate_mainnet', 'deriv_gmx_live', 'research_runner', 'research_readonly_db', 'workers', 'web_app']);25    expect(text).not.toContain(SECRET); expect(text).not.toContain('reader'); expect(text).not.toContain('/srv/web');26    expect(capabilitiesLine(all)).toMatch(/broadcast=OFF\(set off by SPW_BROADCAST\).*strategy_sandbox_enforced=OFF\(set off by SPW_LAB_SANDBOX\)/);27  });28  it('a runner secret that is too short is off, and the reason says so without the secret', () => {29    const c = capabilities({ SPW_RESEARCH_RUNNER_SECRET: 'short-secret' }).find((x) => x.id === 'research_runner')!;30    expect(c.on).toBe(false); expect(c.why).toMatch(/shorter than 32/); expect(c.why).not.toContain('short-secret');31    // GMX needs BOTH server locks (main's gmx-exec reads the same two variables)32    expect(capabilities({ SPW_DERIV_GMX_LIVE: 'on' }).find((x) => x.id === 'deriv_gmx_live')).toMatchObject({ on: false, missing: ['SPW_DERIV_LIVE_MAINNET'] });33    expect(capabilities({ SPW_DERIV_LIVE_MAINNET: 'on' }).find((x) => x.id === 'deriv_gmx_live')).toMatchObject({ on: false, missing: ['SPW_DERIV_GMX_LIVE'] });34    expect(capabilities({ SPW_DERIV_LIVE_MAINNET: 'true' }).find((x) => x.id === 'deriv_live_mainnet')!.on).toBe(false);   // only "on" turns it on, as in the code that reads it35  });36});3738describe('agent engines — heartbeat', () => {39  it('a pass fails when it throws or when every agent ended in an error; one healthy agent is enough', () => {40    expect(enginePassFailure({})).toBeNull();41    expect(enginePassFailure({ a: 'held', b: 'error: x' })).toBeNull();42    expect(enginePassFailure({ a: 'error: signal runner could not start', b: 'backing off after an error' })).toBe('error: signal runner could not start');43    expect(enginePassFailure({ a: 'backing off after an error' })).toBe('backing off after an error');44  });45  it('engineLoop registers in loopStats, counts failures in a row, resets on success, and unregisters on stop', async () => {46    let n = 0;47    const stop = engineLoop('test-engine', 10, async () => { n += 1; if (n === 2) throw new Error('boom'); return n <= 4 ? { a: 'error: cannot reach the signal runner' } : { a: 'no order: targets reached' }; });48    const st = loopStats.get('test-engine')!;49    expect(st).toMatchObject({ name: 'test-engine', everyMs: 10, runs: 0, failures: 0, lastOkAt: null });50    for (let i = 0; i < 200 && st.failures < 4; i++) await new Promise((r) => setTimeout(r, 5));51    expect(st.failures).toBe(4); expect(st.lastOkAt).toBeNull(); expect(st.lastError).toMatch(/cannot reach the signal runner|boom/);52    for (let i = 0; i < 200 && st.failures !== 0; i++) await new Promise((r) => setTimeout(r, 5));53    expect(st.failures).toBe(0); expect(st.lastOkAt).not.toBeNull(); expect(st.runs).toBeGreaterThanOrEqual(5);54    stop();55    expect(loopStats.has('test-engine')).toBe(false);56  });57  it('the real engines use it: both names are registered by their starters', async () => {58    const src = (f: string) => import('node:fs').then((fs) => fs.readFileSync(new URL(f, import.meta.url), 'utf8'));59    expect(await src('../src/agents/live.ts')).toMatch(/engineLoop\('agent-engine', everyMs, \(\) => enginePass\(m\)\)/);60    expect(await src('../src/agents/perp-live.ts')).toMatch(/engineLoop\('perp-agent-engine', everyMs, \(\) => perpEnginePass\(\)\)/);61  });62});63
Files main
Menu

Theme