feat: more work on python scriptiong basics

This commit is contained in:
2026-02-23 11:45:13 +01:00
parent 94b7ca2c80
commit caa3f3c061
18 changed files with 752 additions and 33 deletions

View File

@@ -1,5 +1,6 @@
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;
@@ -81,6 +82,58 @@ describe('PythonRuntimeManager', () => {
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('rejects when runtime returns run error', async () => {
const worker = new MockWorker();
const manager = new PythonRuntimeManager(() => worker as unknown as Worker);
@@ -171,6 +224,116 @@ describe('PythonRuntimeManager', () => {
await expect(runPromise).resolves.toEqual({ result: { html: '<p>ok</p>' }, 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": "<p>ok</p>"}', {
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: '<p>ok</p>' } });
await expect(runPromise).resolves.toEqual({ result: { html: '<p>ok</p>' }, 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": "<p>ok</p>"}',
{
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: '<p>ok</p>' } });
await expect(runPromise).resolves.toEqual({ result: { html: '<p>ok</p>' }, 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": "<p>ok</p>"}',
{
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: '<p>ok</p>' } });
await expect(runPromise).resolves.toEqual({ result: { html: '<p>ok</p>' }, 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);

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { parseMacroContextV1 } from '../../../src/renderer/python/abiV1';
describe('macroContextV1Schema', () => {
it('accepts optional env hook and source metadata', () => {
const parsed = parseMacroContextV1({
env: {
isPreview: true,
mainLanguage: 'en',
hook: 'post:render',
source: {
kind: 'post',
id: 'post-1',
},
},
params: {
title: 'Hello',
},
data: {
post: {
id: 'post-1',
slug: 'hello',
},
},
});
expect(parsed.env.hook).toBe('post:render');
expect(parsed.env.source).toEqual({ kind: 'post', id: 'post-1' });
});
it('rejects unknown env fields', () => {
expect(() =>
parseMacroContextV1({
env: {
isPreview: true,
unknown: 'value',
},
})
).toThrow('Invalid macro context');
});
});

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { createMacroRenderOptions } from '../../../src/renderer/python/macroRenderOptions';
describe('createMacroRenderOptions', () => {
it('maps hook/source metadata into runtime macro options', () => {
const options = createMacroRenderOptions({
hook: 'preview:macro',
source: {
kind: 'post',
id: 'post-5',
},
cacheKey: 'script-1:1:abc',
timeoutMs: 4000,
});
expect(options).toEqual({
macroHook: 'preview:macro',
macroSource: {
kind: 'post',
id: 'post-5',
},
cacheKey: 'script-1:1:abc',
timeoutMs: 4000,
});
});
it('returns empty options when no values are provided', () => {
expect(createMacroRenderOptions()).toEqual({});
});
});