178 lines
5.7 KiB
TypeScript
178 lines
5.7 KiB
TypeScript
|
|
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();
|