Add comprehensive tests for Python macro integration

Co-authored-by: rfc1437 <774975+rfc1437@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-26 21:48:05 +00:00
parent b34cb4a110
commit c9b9d30c7d
5 changed files with 737 additions and 2 deletions

View File

@@ -0,0 +1,252 @@
import { describe, it, expect, vi } from 'vitest';
import {
renderMacro,
replaceAllMacrosAsync,
isBuiltInMacro,
normalizeMacroName,
type PythonMacroRendererContract,
type PythonMacroScript,
} from '../../src/main/engine/PageRenderer';
describe('isBuiltInMacro', () => {
it('returns true for all known JS built-in macro names', () => {
expect(isBuiltInMacro('youtube')).toBe(true);
expect(isBuiltInMacro('vimeo')).toBe(true);
expect(isBuiltInMacro('gallery')).toBe(true);
expect(isBuiltInMacro('photo_archive')).toBe(true);
expect(isBuiltInMacro('tag_cloud')).toBe(true);
expect(isBuiltInMacro('photo_album')).toBe(true);
});
it('returns false for unknown macro names', () => {
expect(isBuiltInMacro('my_custom_macro')).toBe(false);
expect(isBuiltInMacro('python_report')).toBe(false);
expect(isBuiltInMacro('data_table')).toBe(false);
});
});
describe('replaceAllMacrosAsync', () => {
it('replaces built-in JS macros without Python renderer', async () => {
const result = await replaceAllMacrosAsync(
'Before [[youtube id="abc123"]] After',
'post-1',
[],
null,
[],
'en',
null,
);
expect(result).toContain('class="macro-youtube"');
expect(result).toContain('abc123');
expect(result).not.toContain('[[youtube');
});
it('replaces Python macros when renderer is provided', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([
{
id: 'script-1',
slug: 'my_widget',
entrypoint: 'render',
content: 'def render(ctx): return {"html": "<div>Widget</div>"}',
version: 1,
},
] satisfies PythonMacroScript[]),
renderMacro: vi.fn().mockResolvedValue({
html: '<div class="python-widget">Hello from Python</div>',
}),
};
const result = await replaceAllMacrosAsync(
'Before [[my_widget title="Hello"]] After',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(result).toBe('Before <div class="python-widget">Hello from Python</div> After');
expect(mockRenderer.renderMacro).toHaveBeenCalledWith(
expect.objectContaining({
scriptContent: 'def render(ctx): return {"html": "<div>Widget</div>"}',
entrypoint: 'render',
cacheKey: 'script-1:1',
}),
);
});
it('preserves JS macros alongside Python macros', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([
{
id: 'script-2',
slug: 'custom_box',
entrypoint: 'render',
content: 'def render(ctx): return {"html": "<aside>box</aside>"}',
version: 3,
},
] satisfies PythonMacroScript[]),
renderMacro: vi.fn().mockResolvedValue({
html: '<aside class="custom-box">Custom Content</aside>',
}),
};
const result = await replaceAllMacrosAsync(
'[[youtube id="xyz789"]] then [[custom_box]]',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(result).toContain('class="macro-youtube"');
expect(result).toContain('xyz789');
expect(result).toContain('<aside class="custom-box">Custom Content</aside>');
});
it('returns empty string for unknown macros without Python renderer', async () => {
const result = await replaceAllMacrosAsync(
'Before [[unknown_macro]] After',
'post-1',
[],
null,
[],
'en',
null,
);
expect(result).toBe('Before After');
});
it('returns empty string for unmatched Python macros', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([]),
renderMacro: vi.fn(),
};
const result = await replaceAllMacrosAsync(
'Before [[nonexistent_macro]] After',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(result).toBe('Before After');
expect(mockRenderer.renderMacro).not.toHaveBeenCalled();
});
it('handles Python macro rendering errors gracefully', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([
{
id: 'script-err',
slug: 'broken_macro',
entrypoint: 'render',
content: 'def render(ctx): raise Exception("oops")',
version: 1,
},
] satisfies PythonMacroScript[]),
renderMacro: vi.fn().mockRejectedValue(new Error('Python execution failed')),
};
const result = await replaceAllMacrosAsync(
'Before [[broken_macro]] After',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(result).toBe('Before After');
});
it('handles script resolution errors gracefully', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockRejectedValue(new Error('DB error')),
renderMacro: vi.fn(),
};
const result = await replaceAllMacrosAsync(
'Before [[my_macro]] After',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(result).toBe('Before After');
});
it('does not look up Python scripts when all macros are built-in', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([]),
renderMacro: vi.fn(),
};
await replaceAllMacrosAsync(
'[[youtube id="a"]] [[vimeo id="1"]]',
'post-1',
[],
null,
[],
'en',
mockRenderer,
);
expect(mockRenderer.getEnabledMacroScripts).not.toHaveBeenCalled();
});
it('passes correct context to Python macro renderer', async () => {
const mockRenderer: PythonMacroRendererContract = {
getEnabledMacroScripts: vi.fn().mockResolvedValue([
{
id: 'ctx-script',
slug: 'ctx_test',
entrypoint: 'run',
content: 'def run(ctx): return {"html": "ok"}',
version: 2,
},
] satisfies PythonMacroScript[]),
renderMacro: vi.fn().mockResolvedValue({ html: 'ok' }),
};
await replaceAllMacrosAsync(
'[[ctx_test name="Alice" count="5"]]',
'post-42',
[],
null,
[],
'de',
mockRenderer,
);
const call = (mockRenderer.renderMacro as ReturnType<typeof vi.fn>).mock.calls[0][0];
const parsedContext = JSON.parse(call.contextJson);
expect(parsedContext.env.isPreview).toBe(false);
expect(parsedContext.env.mainLanguage).toBe('de');
expect(parsedContext.env.hook).toBe('ctx_test');
expect(parsedContext.env.source).toEqual({ kind: 'macro', id: 'ctx-script' });
expect(parsedContext.params).toEqual({ name: 'Alice', count: '5' });
expect(call.entrypoint).toBe('run');
expect(call.cacheKey).toBe('ctx-script:2');
});
it('returns unchanged text when there are no macros', async () => {
const content = 'Just plain text with no macros';
const result = await replaceAllMacrosAsync(content, '', [], null, [], 'en', null);
expect(result).toBe(content);
});
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PythonMacroWorkerRuntime, type WorkerLike, type WorkerFactory } from '../../src/main/engine/PythonMacroWorkerRuntime';
function createMockWorkerFactory(): {
factory: WorkerFactory;
postMessages: unknown[];
} {
const postMessages: unknown[] = [];
const factory: WorkerFactory = () => {
let messageHandler: ((msg: unknown) => void) | null = null;
const worker: WorkerLike = {
on(event: string, handler: (...args: unknown[]) => void) {
if (event === 'message') {
messageHandler = handler as (msg: unknown) => void;
}
},
postMessage(message: unknown) {
postMessages.push(message);
const msg = message as { type: string; requestId: string };
if (msg.type === 'renderMacro') {
setTimeout(() => {
messageHandler?.({
type: 'macroResult',
requestId: msg.requestId,
html: '<p>Python macro output</p>',
data: { key: 'value' },
warnings: [],
});
}, 10);
}
},
terminate() {},
removeAllListeners() {},
};
setTimeout(() => {
messageHandler?.({ type: 'ready' });
}, 5);
return worker;
};
return { factory, postMessages };
}
describe('PythonMacroWorkerRuntime', () => {
let runtime: PythonMacroWorkerRuntime;
let mockFactory: ReturnType<typeof createMockWorkerFactory>;
beforeEach(() => {
vi.clearAllMocks();
mockFactory = createMockWorkerFactory();
runtime = new PythonMacroWorkerRuntime(mockFactory.factory);
});
it('should render a Python macro successfully', async () => {
const result = await runtime.renderMacro({
scriptContent: 'def render(ctx): return {"html": "<p>output</p>"}',
entrypoint: 'render',
contextJson: JSON.stringify({ env: { isPreview: false } }),
timeoutMs: 3000,
});
expect(result.html).toBe('<p>Python macro output</p>');
expect(result.data).toEqual({ key: 'value' });
});
it('should track macro execution counter', async () => {
expect(runtime.macroCount).toBe(0);
await runtime.renderMacro({
scriptContent: 'def render(ctx): return {"html": ""}',
entrypoint: 'render',
contextJson: '{}',
});
expect(runtime.macroCount).toBe(1);
});
it('should reset counters', async () => {
await runtime.renderMacro({
scriptContent: 'def render(ctx): return {"html": ""}',
entrypoint: 'render',
contextJson: '{}',
});
expect(runtime.macroCount).toBe(1);
runtime.resetCounters();
expect(runtime.macroCount).toBe(0);
expect(runtime.errorCount).toBe(0);
expect(runtime.timeoutCount).toBe(0);
});
it('should dispose without errors', async () => {
await runtime.renderMacro({
scriptContent: 'def render(ctx): return {"html": ""}',
entrypoint: 'render',
contextJson: '{}',
});
expect(() => runtime.dispose()).not.toThrow();
});
it('should pass cacheKey to the worker', async () => {
await runtime.renderMacro({
scriptContent: 'def render(ctx): return {"html": ""}',
entrypoint: 'render',
contextJson: '{}',
cacheKey: 'script-1:v2',
});
const lastMessage = mockFactory.postMessages[mockFactory.postMessages.length - 1] as Record<string, unknown>;
expect(lastMessage.type).toBe('renderMacro');
expect(lastMessage.cacheKey).toBe('script-1:v2');
});
});

View File

@@ -286,4 +286,95 @@ describe('ScriptEngine', () => {
expect(result.deleted).toBe(1);
expect(result.processedFiles).toBe(3);
});
describe('macro resolution', () => {
it('getEnabledMacroScripts returns only enabled macro scripts', async () => {
await scriptEngine.createScript({
title: 'My Macro',
kind: 'macro',
content: 'def render(ctx): return {"html": "<p>hi</p>"}',
enabled: true,
});
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-script-id-2');
await scriptEngine.createScript({
title: 'My Transform',
kind: 'transform',
content: 'def transform(post): return post',
enabled: true,
});
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-script-id-3');
await scriptEngine.createScript({
title: 'Disabled Macro',
kind: 'macro',
content: 'def render(ctx): return {"html": ""}',
enabled: false,
});
const macros = await scriptEngine.getEnabledMacroScripts();
expect(macros).toHaveLength(1);
expect(macros[0].kind).toBe('macro');
expect(macros[0].enabled).toBe(true);
expect(macros[0].title).toBe('My Macro');
});
it('getMacroScriptBySlug finds enabled macro by slug', async () => {
await scriptEngine.createScript({
title: 'Widget Macro',
kind: 'macro',
content: 'def render(ctx): return {"html": "<div>widget</div>"}',
enabled: true,
});
const found = await scriptEngine.getMacroScriptBySlug('widget_macro');
expect(found).not.toBeNull();
expect(found?.slug).toBe('widget_macro');
expect(found?.kind).toBe('macro');
});
it('getMacroScriptBySlug returns null for non-macro scripts', async () => {
await scriptEngine.createScript({
title: 'My Transform',
kind: 'transform',
content: 'def transform(post): return post',
enabled: true,
});
const found = await scriptEngine.getMacroScriptBySlug('my_transform');
expect(found).toBeNull();
});
it('getMacroScriptBySlug returns null for disabled macros', async () => {
await scriptEngine.createScript({
title: 'Disabled Macro',
kind: 'macro',
content: 'def render(ctx): return {"html": ""}',
enabled: false,
});
const found = await scriptEngine.getMacroScriptBySlug('disabled_macro');
expect(found).toBeNull();
});
it('getMacroScriptBySlug is case-insensitive', async () => {
await scriptEngine.createScript({
title: 'Case Test',
kind: 'macro',
content: 'def render(ctx): return {"html": ""}',
enabled: true,
});
const found = await scriptEngine.getMacroScriptBySlug('CASE_TEST');
expect(found).not.toBeNull();
expect(found?.slug).toBe('case_test');
});
it('getMacroScriptBySlug returns null for non-existent slug', async () => {
const found = await scriptEngine.getMacroScriptBySlug('nonexistent');
expect(found).toBeNull();
});
});
});