import { tap, expect } from '@git.zone/tstest/tapbundle'; import { RustSecurityBridge, BridgeState } from '../ts/security/classes.rustsecuritybridge.js'; import type { IBridgeResilienceConfig } from '../ts/security/classes.rustsecuritybridge.js'; // Use fast backoff settings for testing const TEST_CONFIG: Partial = { maxRestartAttempts: 3, healthCheckIntervalMs: 60_000, // long interval so health checks don't interfere restartBackoffBaseMs: 100, restartBackoffMaxMs: 500, healthCheckTimeoutMs: 2_000, }; tap.test('Resilience - should start in Idle state', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); const bridge = RustSecurityBridge.getInstance(); expect(bridge.state).toEqual(BridgeState.Idle); }); tap.test('Resilience - state transitions: Idle -> Starting -> Running', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); const bridge = RustSecurityBridge.getInstance(); const transitions: Array<{ oldState: string; newState: string }> = []; bridge.on('stateChange', (evt: { oldState: string; newState: string }) => { transitions.push(evt); }); const ok = await bridge.start(); if (!ok) { console.log('WARNING: Rust binary not available — skipping resilience start tests'); return; } // We should have seen Idle -> Starting -> Running expect(transitions.length).toBeGreaterThanOrEqual(2); expect(transitions[0].oldState).toEqual(BridgeState.Idle); expect(transitions[0].newState).toEqual(BridgeState.Starting); expect(transitions[1].oldState).toEqual(BridgeState.Starting); expect(transitions[1].newState).toEqual(BridgeState.Running); expect(bridge.state).toEqual(BridgeState.Running); }); tap.test('Resilience - deliberate stop transitions to Stopped', async () => { const bridge = RustSecurityBridge.getInstance(); if (!bridge.running) { console.log('SKIP: bridge not running'); return; } const transitions: Array<{ oldState: string; newState: string }> = []; bridge.on('stateChange', (evt: { oldState: string; newState: string }) => { transitions.push(evt); }); await bridge.stop(); expect(bridge.state).toEqual(BridgeState.Stopped); // Deliberate stop should NOT trigger restart // Wait a bit to ensure no restart happens await new Promise(resolve => setTimeout(resolve, 300)); expect(bridge.state).toEqual(BridgeState.Stopped); bridge.removeAllListeners('stateChange'); }); tap.test('Resilience - commands throw descriptive errors when not running', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); const bridge = RustSecurityBridge.getInstance(); // Idle state try { await bridge.ping(); expect(true).toBeFalse(); // Should not reach } catch (err) { expect((err as Error).message).toInclude('not been started'); } // Stopped state const ok = await bridge.start(); if (ok) { await bridge.stop(); try { await bridge.ping(); expect(true).toBeFalse(); } catch (err) { expect((err as Error).message).toInclude('stopped'); } } }); tap.test('Resilience - restart after stop and fresh start', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); const bridge = RustSecurityBridge.getInstance(); const ok = await bridge.start(); if (!ok) { console.log('SKIP: Rust binary not available'); return; } expect(bridge.state).toEqual(BridgeState.Running); // Stop await bridge.stop(); expect(bridge.state).toEqual(BridgeState.Stopped); // Start again const ok2 = await bridge.start(); expect(ok2).toBeTrue(); expect(bridge.state).toEqual(BridgeState.Running); // Commands should work const pong = await bridge.ping(); expect(pong).toBeTrue(); await bridge.stop(); }); tap.test('Resilience - stateChange events emitted correctly', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); const bridge = RustSecurityBridge.getInstance(); const events: Array<{ oldState: string; newState: string }> = []; bridge.on('stateChange', (evt: { oldState: string; newState: string }) => { events.push(evt); }); const ok = await bridge.start(); if (!ok) { console.log('SKIP: Rust binary not available'); return; } await bridge.stop(); // Verify the full lifecycle: Idle->Starting->Running->Stopped const stateSequence = events.map(e => e.newState); expect(stateSequence).toContain(BridgeState.Starting); expect(stateSequence).toContain(BridgeState.Running); expect(stateSequence).toContain(BridgeState.Stopped); bridge.removeAllListeners('stateChange'); }); tap.test('Resilience - configure sets resilience parameters', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure({ maxRestartAttempts: 10, healthCheckIntervalMs: 60_000, }); // Just verify no errors — config is private, but we can verify // by the behavior in other tests const bridge = RustSecurityBridge.getInstance(); expect(bridge).toBeTruthy(); }); tap.test('Resilience - resetInstance creates fresh singleton', async () => { RustSecurityBridge.resetInstance(); const bridge1 = RustSecurityBridge.getInstance(); RustSecurityBridge.resetInstance(); const bridge2 = RustSecurityBridge.getInstance(); // They should be different instances (we can't compare directly since // resetInstance nulls the static, and getInstance creates new) expect(bridge2.state).toEqual(BridgeState.Idle); }); tap.test('Resilience - cleanup', async () => { RustSecurityBridge.resetInstance(); RustSecurityBridge.configure(TEST_CONFIG); }); export default tap.start();