import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as path from 'path'; import { RustBridge } from '../ts/classes.rustbridge.js'; import type { ICommandDefinition } from '../ts/interfaces/index.js'; const testDir = path.resolve(path.dirname(new URL(import.meta.url).pathname)); const mockBinaryPath = path.join(testDir, 'helpers/mock-rust-binary.mjs'); // Define the command types for our mock binary type TMockCommands = { echo: { params: Record; result: Record }; largeEcho: { params: Record; result: Record }; error: { params: {}; result: never }; emitEvent: { params: { eventName: string; eventData: any }; result: null }; slow: { params: {}; result: { delayed: boolean } }; exit: { params: {}; result: null }; streamEcho: { params: { count: number }; chunk: { index: number; value: string }; result: { totalChunks: number } }; streamError: { params: {}; chunk: { index: number; value: string }; result: never }; streamEmpty: { params: {}; chunk: never; result: { totalChunks: number } }; }; tap.test('should spawn and receive ready event', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); const result = await bridge.spawn(); expect(result).toBeTrue(); expect(bridge.running).toBeTrue(); bridge.kill(); expect(bridge.running).toBeFalse(); }); tap.test('should send command and receive response', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); const result = await bridge.sendCommand('echo', { hello: 'world', num: 42 }); expect(result).toEqual({ hello: 'world', num: 42 }); bridge.kill(); }); tap.test('should handle error responses', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); let threw = false; try { await bridge.sendCommand('error', {}); } catch (err: any) { threw = true; expect(err.message).toInclude('Test error message'); } expect(threw).toBeTrue(); bridge.kill(); }); tap.test('should receive custom events from the binary', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); const eventPromise = new Promise((resolve) => { bridge.once('management:testEvent', (data) => { resolve(data); }); }); await bridge.sendCommand('emitEvent', { eventName: 'testEvent', eventData: { key: 'value' }, }); const eventData = await eventPromise; expect(eventData).toEqual({ key: 'value' }); bridge.kill(); }); tap.test('should handle delayed responses', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 5000, }); await bridge.spawn(); const result = await bridge.sendCommand('slow', {}); expect(result).toEqual({ delayed: true }); bridge.kill(); }); tap.test('should handle multiple concurrent commands', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); const results = await Promise.all([ bridge.sendCommand('echo', { id: 1 }), bridge.sendCommand('echo', { id: 2 }), bridge.sendCommand('echo', { id: 3 }), ]); expect(results[0]).toEqual({ id: 1 }); expect(results[1]).toEqual({ id: 2 }); expect(results[2]).toEqual({ id: 3 }); bridge.kill(); }); tap.test('should throw when sending command while not running', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], }); let threw = false; try { await bridge.sendCommand('echo', {}); } catch (err: any) { threw = true; expect(err.message).toInclude('not running'); } expect(threw).toBeTrue(); }); tap.test('should return false when binary not found', async () => { const bridge = new RustBridge({ binaryName: 'nonexistent-binary-xyz', searchSystemPath: false, }); const result = await bridge.spawn(); expect(result).toBeFalse(); expect(bridge.running).toBeFalse(); }); tap.test('should emit exit event when process exits', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); const exitPromise = new Promise((resolve) => { bridge.once('exit', (code) => { resolve(code); }); }); // Tell mock binary to exit await bridge.sendCommand('exit', {}); const exitCode = await exitPromise; expect(exitCode).toEqual(0); expect(bridge.running).toBeFalse(); }); tap.test('should handle 1MB payload round-trip', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 30000, }); await bridge.spawn(); // Create a ~1MB payload const largeString = 'x'.repeat(1024 * 1024); const result = await bridge.sendCommand('largeEcho', { data: largeString }); expect(result.data).toEqual(largeString); expect(result.data.length).toEqual(1024 * 1024); bridge.kill(); }); tap.test('should handle 10MB payload round-trip', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 60000, }); await bridge.spawn(); // Create a ~10MB payload const largeString = 'y'.repeat(10 * 1024 * 1024); const result = await bridge.sendCommand('largeEcho', { data: largeString }); expect(result.data).toEqual(largeString); expect(result.data.length).toEqual(10 * 1024 * 1024); bridge.kill(); }); tap.test('should reject outbound messages exceeding maxPayloadSize', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, maxPayloadSize: 1000, }); await bridge.spawn(); let threw = false; try { await bridge.sendCommand('largeEcho', { data: 'z'.repeat(2000) }); } catch (err: any) { threw = true; expect(err.message).toInclude('maxPayloadSize'); } expect(threw).toBeTrue(); bridge.kill(); }); tap.test('should handle multiple large concurrent commands', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 30000, }); await bridge.spawn(); const size = 500 * 1024; // 500KB each const results = await Promise.all([ bridge.sendCommand('largeEcho', { data: 'a'.repeat(size), id: 1 }), bridge.sendCommand('largeEcho', { data: 'b'.repeat(size), id: 2 }), bridge.sendCommand('largeEcho', { data: 'c'.repeat(size), id: 3 }), ]); expect(results[0].data.length).toEqual(size); expect(results[0].data[0]).toEqual('a'); expect(results[1].data.length).toEqual(size); expect(results[1].data[0]).toEqual('b'); expect(results[2].data.length).toEqual(size); expect(results[2].data[0]).toEqual('c'); bridge.kill(); }); // === Streaming tests === tap.test('streaming: should receive chunks via for-await-of and final result', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 10000, }); await bridge.spawn(); const stream = bridge.sendCommandStreaming('streamEcho', { count: 5 }); const chunks: Array<{ index: number; value: string }> = []; for await (const chunk of stream) { chunks.push(chunk); } expect(chunks.length).toEqual(5); for (let i = 0; i < 5; i++) { expect(chunks[i].index).toEqual(i); expect(chunks[i].value).toEqual(`chunk_${i}`); } const result = await stream.result; expect(result.totalChunks).toEqual(5); bridge.kill(); }); tap.test('streaming: should handle zero chunks (immediate result)', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, }); await bridge.spawn(); const stream = bridge.sendCommandStreaming('streamEmpty', {}); const chunks: any[] = []; for await (const chunk of stream) { chunks.push(chunk); } expect(chunks.length).toEqual(0); const result = await stream.result; expect(result.totalChunks).toEqual(0); bridge.kill(); }); tap.test('streaming: should propagate error to iterator and .result', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 10000, }); await bridge.spawn(); const stream = bridge.sendCommandStreaming('streamError', {}); const chunks: any[] = []; let iteratorError: Error | null = null; try { for await (const chunk of stream) { chunks.push(chunk); } } catch (err: any) { iteratorError = err; } // Should have received at least one chunk before error expect(chunks.length).toEqual(1); expect(chunks[0].value).toEqual('before_error'); // Iterator should have thrown expect(iteratorError).toBeTruthy(); expect(iteratorError!.message).toInclude('Stream error after chunk'); // .result should also reject let resultError: Error | null = null; try { await stream.result; } catch (err: any) { resultError = err; } expect(resultError).toBeTruthy(); expect(resultError!.message).toInclude('Stream error after chunk'); bridge.kill(); }); tap.test('streaming: should fail when bridge is not running', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], }); const stream = bridge.sendCommandStreaming('streamEcho', { count: 3 }); let resultError: Error | null = null; try { await stream.result; } catch (err: any) { resultError = err; } expect(resultError).toBeTruthy(); expect(resultError!.message).toInclude('not running'); }); tap.test('streaming: should fail when killed mid-stream', async () => { const bridge = new RustBridge({ binaryName: 'node', binaryPath: 'node', cliArgs: [mockBinaryPath], readyTimeoutMs: 5000, requestTimeoutMs: 30000, }); await bridge.spawn(); // Request many chunks so we can kill mid-stream const stream = bridge.sendCommandStreaming('streamEcho', { count: 100 }); const chunks: any[] = []; let iteratorError: Error | null = null; // Kill after a short delay setTimeout(() => { bridge.kill(); }, 50); try { for await (const chunk of stream) { chunks.push(chunk); } } catch (err: any) { iteratorError = err; } // Should have gotten some chunks but not all expect(iteratorError).toBeTruthy(); expect(iteratorError!.message).toInclude('killed'); }); export default tap.start();