import { QuickJSWorker } from '../dist'; import { EventEmitter } from 'events'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('QuickJS Integration Tests', () => { let qjs: QuickJSWorker; afterEach(async () => { if (qjs && !qjs.isClosed()) { await qjs.close(); } }); it('should stabilize memory usage after churn', async () => { const initialMemory = process.memoryUsage().rss; // Churn through 500 workers for (let i = 0; i < 500; i++) { const qjs = new QuickJSWorker(); await qjs.eval('1+1'); await qjs.close(); } // Force GC if possible (requires --expose-gc) or allow for some overhead global.gc && global.gc(); const finalMemory = process.memoryUsage().rss; const growth = (finalMemory - initialMemory) / 1024 / 1024; // MB // Expect growth to be less than 50MB (adjust based on your baseline overhead) expect(growth).toBeLessThan(50); }, 30000); describe('Invariant: Worker Isolation', () => { it('should never share state between workers', async () => { const workerA = new QuickJSWorker(); const workerB = new QuickJSWorker(); await workerA.setGlobal('sharedKey', 'VALUE_A'); await workerB.setGlobal('sharedKey', 'VALUE_B'); const valA = await workerA.eval('sharedKey'); const valB = await workerB.eval('sharedKey'); expect(valA).toBe('VALUE_A'); expect(valB).toBe('VALUE_B'); await workerA.close(); await workerB.close(); }); }); // ... [Previous Instantiation / Basic Evaluation / Synchronous / Module / Globals tests remain same] ... describe('Instantiation', () => { it('should instantiate without errors', () => { qjs = new QuickJSWorker(); expect(qjs).toBeInstanceOf(QuickJSWorker); expect(qjs).toBeInstanceOf(EventEmitter); }); it('should accept options during instantiation', () => { qjs = new QuickJSWorker({ maxMemoryBytes: 1024 * 1024 * 10, maxEvalMs: 500 }); expect(qjs).toBeDefined(); }); }); describe('Basic Evaluation (Async)', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should evaluate simple javascript expressions', async () => { const result = await qjs.eval('1 + 1'); expect(result).toBe(2); }); it('should return strings', async () => { const result = await qjs.eval('"Hello " + "World"'); expect(result).toBe('Hello World'); }); it('should return complex objects', async () => { const result = await qjs.eval('({ a: 1, b: "test" })'); expect(result).toEqual({ a: 1, b: "test" }); }); it('should handle runtime errors gracefully', async () => { await expect(qjs.eval('throw new Error("test error")')).rejects.toBeDefined(); }); it('should respect timeout limits', async () => { await qjs.close(); qjs = new QuickJSWorker({ maxEvalMs: 100 }); await expect(qjs.eval('while(true) {}')).rejects.toBeDefined(); }); it('should capture execution stats', async () => { await qjs.eval('1+1'); const stats = qjs.stats.lastExecution; expect(stats).toBeDefined(); expect(stats).toHaveProperty('cpuTimeMs'); expect(stats).toHaveProperty('evalTimeMs'); }); }); describe('Synchronous Evaluation', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should evaluate synchronously', () => { const result = qjs.evalSync('2 * 5'); expect(result).toBe(10); }); it('should throw errors synchronously', () => { expect(() => { qjs.evalSync('throw new Error("sync error")'); }).toThrow('sync error'); }); }); describe('Module Support', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should evaluate ES modules and return a namespace proxy', async () => { const mod = await qjs.module.eval(` export const x = 10; export function add(a, b) { return a + b; } `); expect(mod.x).toBe(10); await expect(mod.add(5, 7)).resolves.toBe(12); }); it('should handle imports if a loader is configured', async () => { qjs = new QuickJSWorker({ imports: (path: string) => { if (path === './math.js') { return 'export function add(a, b) { return a + b; }'; } return false; } }); const mod = await qjs.module.eval(` import { add } from './math.js'; export const result = add(5, 7); `); expect(mod.result).toBe(12); }); }); describe('Globals and Data Injection', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should set global variables', async () => { await qjs.setGlobal('myVar', 123); const result = await qjs.eval('myVar'); expect(result).toBe(123); }); it('should inject complex objects', async () => { await qjs.setGlobal('config', { active: true, limits: { max: 100 } }); const result = await qjs.eval('config.limits.max'); expect(result).toBe(100); }); it('should inject functions that can be called from JS', async () => { const mockFn = jest.fn((x) => x * 2); await qjs.setGlobal('double', mockFn); const result = await qjs.eval('double(5)'); expect(result).toBe(10); expect(mockFn).toHaveBeenCalledWith(5); }); it('should handle async function injection (Promises)', async () => { const asyncFn = jest.fn(async (x) => { await sleep(50); return x + 1; }); await qjs.setGlobal('addAsync', asyncFn); const result = await qjs.eval('(async () => await addAsync(10))()'); expect(result).toBe(11); }); }); // ------------------------------------------------------------- // UPDATED SECTION: // ------------------------------------------------------------- describe('Communication (postMessage)', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should receive messages from QuickJS', async () => { const msg = new Promise((resolve, reject) => { qjs.on('message', (msg) => { try { expect(msg).toBe('Hello from QJS'); resolve(undefined); } catch (e) { reject(e); } }); }); await qjs.eval('postMessage("Hello from QJS")'); await msg; }); it('should preserve nested object data from QuickJS to Node', async () => { const received = new Promise((resolve, reject) => { qjs.on('message', (msg) => { try { expect(msg.ok).toBe(true); expect(typeof msg.when?.toISOString).toBe("function"); expect(msg.when.toISOString()).toBe('2024-02-03T04:05:06.000Z'); expect(msg.bytes).toBeInstanceOf(Uint8Array); expect(Array.from(msg.bytes)).toEqual([4, 5, 6]); resolve(undefined); } catch (error) { reject(error); } }); }); await qjs.eval(` postMessage({ ok: true, when: new Date('2024-02-03T04:05:06.000Z'), bytes: new Uint8Array([4, 5, 6]), }); `); await received; }); it('should send messages to QuickJS', async () => { // Setup listener await qjs.eval(` let received = null; on('message', (msg) => { received = msg; }); `); // Send message qjs.postMessage({ foo: 'bar' }); await new Promise(r => setTimeout(r, 100)); const result = await qjs.eval(`received`); expect(result).toEqual({ foo: 'bar' }); }); it('should send binary messages to QuickJS without JSON fallback', async () => { await qjs.eval(` globalThis.binaryStats = null; on('message', (msg) => { binaryStats = { ctor: msg && msg.constructor && msg.constructor.name, len: msg && msg.length, first: msg && msg[0], last: msg && msg[msg.length - 1], }; }); `); qjs.postMessage(new Uint8Array([1, 2, 255])); await new Promise(r => setTimeout(r, 50)); const result = await qjs.eval(`binaryStats`); expect(result).toEqual({ ctor: 'Uint8Array', len: 3, first: 1, last: 255, }); }); it('should batch messages from Node to QuickJS', async () => { await qjs.eval(` globalThis.receivedBatch = []; on('message', (msg) => { receivedBatch.push(msg); }); `); qjs.postMessages([{ id: 1 }, { id: 2 }, { id: 3 }]); await new Promise(r => setTimeout(r, 50)); const result = await qjs.eval(`receivedBatch`); expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); }); it('should batch messages from QuickJS to Node', async () => { const seen: any[] = []; qjs.on('message', (msg) => seen.push(msg)); await qjs.eval(` postMessages([ { id: 1, when: new Date('2024-02-03T04:05:06.000Z') }, { id: 2, bytes: new Uint8Array([1, 2]) }, { id: 3, ok: true }, ]); `); await new Promise(r => setTimeout(r, 50)); expect(seen).toHaveLength(3); expect(typeof seen[0].when?.toISOString).toBe("function"); expect(seen[0].when.toISOString()).toBe('2024-02-03T04:05:06.000Z'); expect(Array.from(seen[1].bytes)).toEqual([1, 2]); expect(seen[2]).toEqual({ id: 3, ok: true }); }); it('should emit messageBatch with drained messages', async () => { const batches: any[] = []; qjs.on('messageBatch', (msgs) => batches.push(msgs)); await qjs.eval(` postMessages([ { id: 1, ok: true }, { id: 2, ok: true }, { id: 3, ok: true }, ]); `); await new Promise(r => setTimeout(r, 50)); expect(batches).toHaveLength(1); expect(batches[0]).toEqual([ { id: 1, ok: true }, { id: 2, ok: true }, { id: 3, ok: true }, ]); }); it('should preserve nested object data on messages to QuickJS', async () => { await qjs.eval(` globalThis.structuredMessage = null; on('message', (msg) => { structuredMessage = { ok: msg.nested.ok, when: msg.nested.when instanceof Date ? msg.nested.when.toISOString() : null, bytesCtor: msg.nested.bytes && msg.nested.bytes.constructor && msg.nested.bytes.constructor.name, bytes: Array.from(msg.nested.bytes || []), listWhen: msg.list[1] instanceof Date ? msg.list[1].toISOString() : null, }; }); `); qjs.postMessage({ nested: { ok: true, when: new Date('2024-02-03T04:05:06.000Z'), bytes: new Uint8Array([9, 8, 7]), }, list: ['x', new Date('2024-02-03T04:05:07.000Z')], }); await new Promise(r => setTimeout(r, 50)); const result = await qjs.eval(`structuredMessage`); expect(result).toEqual({ ok: true, when: '2024-02-03T04:05:06.000Z', bytesCtor: 'Uint8Array', bytes: [9, 8, 7], listWhen: '2024-02-03T04:05:07.000Z', }); }); }); // ------------------------------------------------------------- describe('Bytecode', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should compile to bytecode and reload', async () => { const source = '1 + 2'; const bytecode = await qjs.getByteCode(source); expect(bytecode).toBeInstanceOf(Uint8Array); expect(bytecode.length).toBeGreaterThan(0); await qjs.loadByteCode(bytecode); const funcSource = 'globalThis.myFunc = () => 42;'; const funcBytecode = await qjs.getByteCode(funcSource); await qjs.loadByteCode(funcBytecode); const result = await qjs.eval('myFunc()'); expect(result).toBe(42); }); }); describe('Memory and GC', () => { beforeEach(() => { qjs = new QuickJSWorker(); }); it('should return memory usage', async () => { const mem = await qjs.memory(); const parsed = typeof mem === 'string' ? JSON.parse(mem) : mem; expect(parsed).toBeDefined(); expect(Object.keys(parsed).length).toBeGreaterThan(0); }); it('should run garbage collection', async () => { await expect(qjs.gc()).resolves.not.toThrow(); }); }); describe('Resource Management', () => { it('should close resources explicitly', async () => { qjs = new QuickJSWorker(); expect(qjs.isClosed()).toBe(false); await qjs.close(); expect(qjs.isClosed()).toBe(true); await expect(qjs.eval('1+1')).rejects.toThrow('Runtime is closed'); }); it('should support AsyncDispose (Symbol.asyncDispose)', async () => { { const worker = new QuickJSWorker(); try { await worker.eval('1+1'); } finally { await worker[Symbol.asyncDispose](); } expect(worker.isClosed()).toBe(true); } }); }); });