import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PythonRuntimeManager } from '../../../src/renderer/python/PythonRuntimeManager'; import { createMacroRenderOptions } from '../../../src/renderer/python/macroRenderOptions'; class MockWorker { onmessage: ((event: MessageEvent) => void) | null = null; onerror: ((event: ErrorEvent) => void) | null = null; terminated = false; postedMessages: unknown[] = []; postMessage(message: unknown): void { this.postedMessages.push(message); } terminate(): void { this.terminated = true; } emitMessage(data: unknown): void { this.onmessage?.({ data } as MessageEvent); } emitError(error: Error): void { this.onerror?.({ error } as ErrorEvent); } } describe('PythonRuntimeManager', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('initializes worker once and resolves on ready signal', async () => { const createdWorkers: MockWorker[] = []; const manager = new PythonRuntimeManager(() => { const worker = new MockWorker(); createdWorkers.push(worker); return worker as unknown as Worker; }); const initA = manager.initialize(); const initB = manager.initialize(); expect(initA).toBe(initB); expect(createdWorkers).toHaveLength(1); createdWorkers[0].emitMessage({ type: 'ready' }); await expect(initA).resolves.toBeUndefined(); expect(manager.isReady()).toBe(true); }); it('rejects when worker emits an error before ready', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitError(new Error('bootstrap failed')); await expect(initPromise).rejects.toThrow('bootstrap failed'); expect(manager.isReady()).toBe(false); }); it('executes code and returns stdout and result', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.execute('print("hello")\n1 + 1'); await Promise.resolve(); const request = worker.postedMessages[0] as { type: string; requestId: string; code: string }; worker.emitMessage({ type: 'stdout', requestId: request.requestId, chunk: 'hello\n' }); worker.emitMessage({ type: 'runResult', requestId: request.requestId, result: '2' }); await expect(runPromise).resolves.toEqual({ result: '2', stdout: 'hello\n' }); }); it('forwards compile cache key in execute request options', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.execute('value = 1', { cacheKey: 'script-1:3' }); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string; cacheKey?: string }; expect(request.cacheKey).toBe('script-1:3'); worker.emitMessage({ type: 'runResult', requestId: request.requestId, result: '' }); await expect(runPromise).resolves.toEqual({ result: '', stdout: '' }); }); it('forwards selected entrypoint in execute request options', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.execute('def helper():\n return 42', { entrypoint: 'helper' }); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string; entrypoint?: string }; expect(request.entrypoint).toBe('helper'); worker.emitMessage({ type: 'runResult', requestId: request.requestId, result: '42' }); await expect(runPromise).resolves.toEqual({ result: '42', stdout: '' }); }); it('inspects script and returns available function names', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const inspectPromise = manager.inspectEntrypoints('def render(context):\n return {}\n\ndef helper():\n return 1'); await Promise.resolve(); const request = worker.postedMessages[0] as { type: string; requestId: string; code: string }; expect(request.type).toBe('inspectEntrypoints'); worker.emitMessage({ type: 'entrypoints', requestId: request.requestId, entrypoints: ['render', 'helper'] }); await expect(inspectPromise).resolves.toEqual(['render', 'helper']); }); it('checks syntax and returns structured syntax errors', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const syntaxPromise = manager.syntaxCheck('def broken(:\n pass'); await Promise.resolve(); const request = worker.postedMessages[0] as { type: string; requestId: string; code: string }; expect(request.type).toBe('syntaxCheck'); worker.emitMessage({ type: 'syntaxResult', requestId: request.requestId, errors: [ { line: 1, column: 11, endLine: 1, endColumn: 12, message: 'invalid syntax', }, ], }); await expect(syntaxPromise).resolves.toEqual({ errors: [ { line: 1, column: 11, endLine: 1, endColumn: 12, message: 'invalid syntax', }, ], }); }); it('rejects when runtime returns run error', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.execute('raise RuntimeError("boom")'); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string }; worker.emitMessage({ type: 'runError', requestId: request.requestId, error: 'boom' }); await expect(runPromise).rejects.toThrow('boom'); }); it('queues concurrent execute calls and sends the next request only after completion', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const firstRun = manager.execute('1 + 1'); const secondRun = manager.execute('2 + 2'); await Promise.resolve(); expect(worker.postedMessages).toHaveLength(1); const firstRequest = worker.postedMessages[0] as { requestId: string }; worker.emitMessage({ type: 'runResult', requestId: firstRequest.requestId, result: '2' }); await expect(firstRun).resolves.toEqual({ result: '2', stdout: '' }); await Promise.resolve(); expect(worker.postedMessages).toHaveLength(2); const secondRequest = worker.postedMessages[1] as { requestId: string }; worker.emitMessage({ type: 'runResult', requestId: secondRequest.requestId, result: '4' }); await expect(secondRun).resolves.toEqual({ result: '4', stdout: '' }); }); it('terminates timed out worker and recovers with a new worker', async () => { const workers: MockWorker[] = []; const manager = new PythonRuntimeManager(() => { const worker = new MockWorker(); workers.push(worker); return worker as unknown as Worker; }); const firstInit = manager.initialize(); workers[0].emitMessage({ type: 'ready' }); await firstInit; const timedOutRun = manager.execute('while True: pass', { timeoutMs: 100 }); await Promise.resolve(); vi.advanceTimersByTime(101); await expect(timedOutRun).rejects.toThrow('timed out'); expect(workers[0].terminated).toBe(true); expect(manager.isReady()).toBe(false); const secondInit = manager.initialize(); workers[1].emitMessage({ type: 'ready' }); await secondInit; const secondRun = manager.execute('40 + 2'); await Promise.resolve(); const request = workers[1].postedMessages[0] as { requestId: string }; workers[1].emitMessage({ type: 'runResult', requestId: request.requestId, result: '42' }); await expect(secondRun).resolves.toEqual({ result: '42', stdout: '' }); }); it('rejects macro execution when ABI context is invalid on caller side', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1('def render(context):\n return {"html": "

ok

"}', { env: { isPreview: 'yes', }, }); await expect(runPromise).rejects.toThrow('Invalid macro context'); expect(worker.postedMessages).toHaveLength(0); }); it('returns validated macro result and stdout', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1('def render(context):\n return {"html": "

ok

"}', { env: { isPreview: true, }, params: { title: 'Hello', }, }); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string }; worker.emitMessage({ type: 'stdout', requestId: request.requestId, chunk: 'rendering\n' }); worker.emitMessage({ type: 'macroResult', requestId: request.requestId, result: { html: '

ok

' } }); await expect(runPromise).resolves.toEqual({ result: { html: '

ok

' }, stdout: 'rendering\n' }); }); it('accepts optional env hook and source metadata for macro execution', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1('def render(context):\n return {"html": "

ok

"}', { env: { isPreview: true, hook: 'post:render', source: { kind: 'post', id: 'post-1', }, }, }); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string; context: { env: { hook?: string; source?: { kind: string; id?: string } } }; }; expect(request.context.env.hook).toBe('post:render'); expect(request.context.env.source).toEqual({ kind: 'post', id: 'post-1' }); worker.emitMessage({ type: 'macroResult', requestId: request.requestId, result: { html: '

ok

' } }); await expect(runPromise).resolves.toEqual({ result: { html: '

ok

' }, stdout: '' }); }); it('injects env hook and source metadata from macro execution options', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1( 'def render(context):\n return {"html": "

ok

"}', { env: { isPreview: true, }, }, createMacroRenderOptions({ hook: 'preview:macro', source: { kind: 'post', id: 'post-77', }, }) ); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string; context: { env: { hook?: string; source?: { kind: string; id?: string } } }; }; expect(request.context.env.hook).toBe('preview:macro'); expect(request.context.env.source).toEqual({ kind: 'post', id: 'post-77' }); worker.emitMessage({ type: 'macroResult', requestId: request.requestId, result: { html: '

ok

' } }); await expect(runPromise).resolves.toEqual({ result: { html: '

ok

' }, stdout: '' }); }); it('preserves explicit env hook and source over macro execution options', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1( 'def render(context):\n return {"html": "

ok

"}', { env: { isPreview: true, hook: 'explicit:hook', source: { kind: 'page', id: 'page-9', }, }, }, createMacroRenderOptions({ hook: 'preview:macro', source: { kind: 'post', id: 'post-77', }, }) ); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string; context: { env: { hook?: string; source?: { kind: string; id?: string } } }; }; expect(request.context.env.hook).toBe('explicit:hook'); expect(request.context.env.source).toEqual({ kind: 'page', id: 'page-9' }); worker.emitMessage({ type: 'macroResult', requestId: request.requestId, result: { html: '

ok

' } }); await expect(runPromise).resolves.toEqual({ result: { html: '

ok

' }, stdout: '' }); }); it('rejects macro execution when worker result violates ABI schema', async () => { const worker = new MockWorker(); const manager = new PythonRuntimeManager(() => worker as unknown as Worker); const initPromise = manager.initialize(); worker.emitMessage({ type: 'ready' }); await initPromise; const runPromise = manager.renderMacroV1('def render(context):\n return {"html": "

ok

"}', { env: { isPreview: true, }, }); await Promise.resolve(); const request = worker.postedMessages[0] as { requestId: string }; worker.emitMessage({ type: 'macroResult', requestId: request.requestId, result: { html: 42 } }); await expect(runPromise).rejects.toThrow('Invalid macro result'); }); });