@push.rocks/smartrust 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ import { expect, tap } from '@git.zone/tstest/tapbundle';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import * as childProcess from 'child_process';
5
+ import * as os from 'os';
6
+ import { RustBridge } from '../ts/classes.rustbridge.js';
7
+ import type { ICommandDefinition } from '../ts/interfaces/index.js';
8
+
9
+ const testDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
10
+ const mockServerPath = path.join(testDir, 'helpers/mock-socket-server.mjs');
11
+
12
+ // Define the command types for our mock binary
13
+ type TMockCommands = {
14
+ echo: { params: Record<string, any>; result: Record<string, any> };
15
+ largeEcho: { params: Record<string, any>; result: Record<string, any> };
16
+ error: { params: {}; result: never };
17
+ emitEvent: { params: { eventName: string; eventData: any }; result: null };
18
+ slow: { params: {}; result: { delayed: boolean } };
19
+ exit: { params: {}; result: null };
20
+ streamEcho: { params: { count: number }; chunk: { index: number; value: string }; result: { totalChunks: number } };
21
+ streamError: { params: {}; chunk: { index: number; value: string }; result: never };
22
+ streamEmpty: { params: {}; chunk: never; result: { totalChunks: number } };
23
+ };
24
+
25
+ /**
26
+ * Start the mock socket server and return the socket path.
27
+ * Returns { proc, socketPath }.
28
+ */
29
+ async function startMockServer(testName: string): Promise<{ proc: childProcess.ChildProcess; socketPath: string }> {
30
+ const socketPath = path.join(os.tmpdir(), `smartrust-test-${Date.now()}-${testName}.sock`);
31
+
32
+ const proc = childProcess.spawn('node', [mockServerPath, socketPath], {
33
+ stdio: ['pipe', 'pipe', 'pipe'],
34
+ });
35
+
36
+ // Wait for the server to signal readiness via stdout
37
+ return new Promise((resolve, reject) => {
38
+ let stdoutData = '';
39
+ const timeout = setTimeout(() => {
40
+ proc.kill();
41
+ reject(new Error('Mock server did not start within 5s'));
42
+ }, 5000);
43
+
44
+ proc.stdout!.on('data', (data: Buffer) => {
45
+ stdoutData += data.toString();
46
+ const lines = stdoutData.split('\n');
47
+ for (const line of lines) {
48
+ if (line.trim()) {
49
+ try {
50
+ const parsed = JSON.parse(line.trim());
51
+ if (parsed.ready) {
52
+ clearTimeout(timeout);
53
+ resolve({ proc, socketPath: parsed.socketPath });
54
+ return;
55
+ }
56
+ } catch { /* not JSON yet */ }
57
+ }
58
+ }
59
+ });
60
+
61
+ proc.on('error', (err) => {
62
+ clearTimeout(timeout);
63
+ reject(err);
64
+ });
65
+
66
+ proc.on('exit', (code) => {
67
+ clearTimeout(timeout);
68
+ reject(new Error(`Mock server exited with code ${code}`));
69
+ });
70
+ });
71
+ }
72
+
73
+ function stopMockServer(proc: childProcess.ChildProcess, socketPath: string) {
74
+ try { proc.kill('SIGTERM'); } catch { /* ignore */ }
75
+ try { fs.unlinkSync(socketPath); } catch { /* ignore */ }
76
+ }
77
+
78
+ // === Socket Transport Tests via RustBridge.connect() ===
79
+
80
+ tap.test('socket: should connect and receive ready event', async () => {
81
+ const { proc, socketPath } = await startMockServer('connect-ready');
82
+ try {
83
+ const bridge = new RustBridge<TMockCommands>({
84
+ binaryName: 'mock-daemon',
85
+ readyTimeoutMs: 5000,
86
+ });
87
+
88
+ const result = await bridge.connect(socketPath);
89
+ expect(result).toBeTrue();
90
+ expect(bridge.running).toBeTrue();
91
+
92
+ bridge.kill();
93
+ expect(bridge.running).toBeFalse();
94
+ } finally {
95
+ stopMockServer(proc, socketPath);
96
+ }
97
+ });
98
+
99
+ tap.test('socket: should send command and receive response', async () => {
100
+ const { proc, socketPath } = await startMockServer('send-command');
101
+ try {
102
+ const bridge = new RustBridge<TMockCommands>({
103
+ binaryName: 'mock-daemon',
104
+ readyTimeoutMs: 5000,
105
+ });
106
+
107
+ await bridge.connect(socketPath);
108
+
109
+ const result = await bridge.sendCommand('echo', { hello: 'world', num: 42 });
110
+ expect(result).toEqual({ hello: 'world', num: 42 });
111
+
112
+ bridge.kill();
113
+ } finally {
114
+ stopMockServer(proc, socketPath);
115
+ }
116
+ });
117
+
118
+ tap.test('socket: should handle error responses', async () => {
119
+ const { proc, socketPath } = await startMockServer('error-response');
120
+ try {
121
+ const bridge = new RustBridge<TMockCommands>({
122
+ binaryName: 'mock-daemon',
123
+ readyTimeoutMs: 5000,
124
+ });
125
+
126
+ await bridge.connect(socketPath);
127
+
128
+ let threw = false;
129
+ try {
130
+ await bridge.sendCommand('error', {});
131
+ } catch (err: any) {
132
+ threw = true;
133
+ expect(err.message).toInclude('Test error message');
134
+ }
135
+ expect(threw).toBeTrue();
136
+
137
+ bridge.kill();
138
+ } finally {
139
+ stopMockServer(proc, socketPath);
140
+ }
141
+ });
142
+
143
+ tap.test('socket: should receive custom events', async () => {
144
+ const { proc, socketPath } = await startMockServer('custom-events');
145
+ try {
146
+ const bridge = new RustBridge<TMockCommands>({
147
+ binaryName: 'mock-daemon',
148
+ readyTimeoutMs: 5000,
149
+ });
150
+
151
+ await bridge.connect(socketPath);
152
+
153
+ const eventPromise = new Promise<any>((resolve) => {
154
+ bridge.once('management:testEvent', (data) => resolve(data));
155
+ });
156
+
157
+ await bridge.sendCommand('emitEvent', {
158
+ eventName: 'testEvent',
159
+ eventData: { key: 'value' },
160
+ });
161
+
162
+ const eventData = await eventPromise;
163
+ expect(eventData).toEqual({ key: 'value' });
164
+
165
+ bridge.kill();
166
+ } finally {
167
+ stopMockServer(proc, socketPath);
168
+ }
169
+ });
170
+
171
+ tap.test('socket: should handle multiple concurrent commands', async () => {
172
+ const { proc, socketPath } = await startMockServer('concurrent');
173
+ try {
174
+ const bridge = new RustBridge<TMockCommands>({
175
+ binaryName: 'mock-daemon',
176
+ readyTimeoutMs: 5000,
177
+ });
178
+
179
+ await bridge.connect(socketPath);
180
+
181
+ const results = await Promise.all([
182
+ bridge.sendCommand('echo', { id: 1 }),
183
+ bridge.sendCommand('echo', { id: 2 }),
184
+ bridge.sendCommand('echo', { id: 3 }),
185
+ ]);
186
+
187
+ expect(results[0]).toEqual({ id: 1 });
188
+ expect(results[1]).toEqual({ id: 2 });
189
+ expect(results[2]).toEqual({ id: 3 });
190
+
191
+ bridge.kill();
192
+ } finally {
193
+ stopMockServer(proc, socketPath);
194
+ }
195
+ });
196
+
197
+ tap.test('socket: should handle 1MB payload round-trip', async () => {
198
+ const { proc, socketPath } = await startMockServer('large-payload');
199
+ try {
200
+ const bridge = new RustBridge<TMockCommands>({
201
+ binaryName: 'mock-daemon',
202
+ readyTimeoutMs: 5000,
203
+ requestTimeoutMs: 30000,
204
+ });
205
+
206
+ await bridge.connect(socketPath);
207
+
208
+ const largeString = 'x'.repeat(1024 * 1024);
209
+ const result = await bridge.sendCommand('largeEcho', { data: largeString });
210
+ expect(result.data).toEqual(largeString);
211
+ expect(result.data.length).toEqual(1024 * 1024);
212
+
213
+ bridge.kill();
214
+ } finally {
215
+ stopMockServer(proc, socketPath);
216
+ }
217
+ });
218
+
219
+ tap.test('socket: should disconnect without killing the daemon', async () => {
220
+ const { proc, socketPath } = await startMockServer('no-kill-daemon');
221
+ try {
222
+ // First connection
223
+ const bridge1 = new RustBridge<TMockCommands>({
224
+ binaryName: 'mock-daemon',
225
+ readyTimeoutMs: 5000,
226
+ });
227
+ await bridge1.connect(socketPath);
228
+ expect(bridge1.running).toBeTrue();
229
+
230
+ // Disconnect
231
+ bridge1.kill();
232
+ expect(bridge1.running).toBeFalse();
233
+
234
+ // Second connection — daemon should still be alive
235
+ const bridge2 = new RustBridge<TMockCommands>({
236
+ binaryName: 'mock-daemon',
237
+ readyTimeoutMs: 5000,
238
+ });
239
+ const result = await bridge2.connect(socketPath);
240
+ expect(result).toBeTrue();
241
+ expect(bridge2.running).toBeTrue();
242
+
243
+ // Verify the daemon is functional
244
+ const echoResult = await bridge2.sendCommand('echo', { reconnected: true });
245
+ expect(echoResult).toEqual({ reconnected: true });
246
+
247
+ bridge2.kill();
248
+ } finally {
249
+ stopMockServer(proc, socketPath);
250
+ }
251
+ });
252
+
253
+ tap.test('socket: should stream responses via socket', async () => {
254
+ const { proc, socketPath } = await startMockServer('streaming');
255
+ try {
256
+ const bridge = new RustBridge<TMockCommands>({
257
+ binaryName: 'mock-daemon',
258
+ readyTimeoutMs: 5000,
259
+ requestTimeoutMs: 10000,
260
+ });
261
+
262
+ await bridge.connect(socketPath);
263
+
264
+ const stream = bridge.sendCommandStreaming('streamEcho', { count: 5 });
265
+ const chunks: Array<{ index: number; value: string }> = [];
266
+
267
+ for await (const chunk of stream) {
268
+ chunks.push(chunk);
269
+ }
270
+
271
+ expect(chunks.length).toEqual(5);
272
+ for (let i = 0; i < 5; i++) {
273
+ expect(chunks[i].index).toEqual(i);
274
+ expect(chunks[i].value).toEqual(`chunk_${i}`);
275
+ }
276
+
277
+ const result = await stream.result;
278
+ expect(result.totalChunks).toEqual(5);
279
+
280
+ bridge.kill();
281
+ } finally {
282
+ stopMockServer(proc, socketPath);
283
+ }
284
+ });
285
+
286
+ tap.test('socket: should return false when socket path does not exist', async () => {
287
+ const bridge = new RustBridge<TMockCommands>({
288
+ binaryName: 'mock-daemon',
289
+ readyTimeoutMs: 3000,
290
+ });
291
+
292
+ const result = await bridge.connect('/tmp/nonexistent-smartrust-test.sock');
293
+ expect(result).toBeFalse();
294
+ expect(bridge.running).toBeFalse();
295
+ });
296
+
297
+ tap.test('socket: should throw when sending command while not connected', async () => {
298
+ const bridge = new RustBridge<TMockCommands>({
299
+ binaryName: 'mock-daemon',
300
+ });
301
+
302
+ let threw = false;
303
+ try {
304
+ await bridge.sendCommand('echo', {});
305
+ } catch (err: any) {
306
+ threw = true;
307
+ expect(err.message).toInclude('not running');
308
+ }
309
+ expect(threw).toBeTrue();
310
+ });
311
+
312
+ export default tap.start();
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartrust',
6
- version: '1.2.1',
6
+ version: '1.3.1',
7
7
  description: 'a bridge between JS engines and rust'
8
8
  }
@@ -0,0 +1,51 @@
1
+ import type { IRustBridgeLogger } from './interfaces/index.js';
2
+
3
+ /**
4
+ * Buffer-based newline scanner for streaming binary data.
5
+ * Accumulates chunks and emits complete lines via callback.
6
+ * Used by both StdioTransport and SocketTransport.
7
+ */
8
+ export class LineScanner {
9
+ private buffer: Buffer = Buffer.alloc(0);
10
+ private maxPayloadSize: number;
11
+ private logger: IRustBridgeLogger;
12
+
13
+ constructor(maxPayloadSize: number, logger: IRustBridgeLogger) {
14
+ this.maxPayloadSize = maxPayloadSize;
15
+ this.logger = logger;
16
+ }
17
+
18
+ /**
19
+ * Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
20
+ */
21
+ public push(chunk: Buffer, onLine: (line: string) => void): void {
22
+ this.buffer = Buffer.concat([this.buffer, chunk]);
23
+
24
+ let newlineIndex: number;
25
+ while ((newlineIndex = this.buffer.indexOf(0x0A)) !== -1) {
26
+ const lineBuffer = this.buffer.subarray(0, newlineIndex);
27
+ this.buffer = this.buffer.subarray(newlineIndex + 1);
28
+
29
+ if (lineBuffer.length > this.maxPayloadSize) {
30
+ this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
31
+ continue;
32
+ }
33
+
34
+ const line = lineBuffer.toString('utf8').trim();
35
+ if (line) {
36
+ onLine(line);
37
+ }
38
+ }
39
+
40
+ // Prevent OOM if sender never sends newline
41
+ if (this.buffer.length > this.maxPayloadSize) {
42
+ this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.buffer.length} bytes) without newline, clearing`);
43
+ this.buffer = Buffer.alloc(0);
44
+ }
45
+ }
46
+
47
+ /** Reset the internal buffer. */
48
+ public clear(): void {
49
+ this.buffer = Buffer.alloc(0);
50
+ }
51
+ }