Files
smartmta/test/test.rustsecuritybridge.resilience.node.ts

178 lines
5.7 KiB
TypeScript
Raw Normal View History

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<IBridgeResilienceConfig> = {
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();