134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
PROMPT_IDS,
|
|
buildPrompt,
|
|
interpolateVariables,
|
|
loadPrompt,
|
|
processConditionalBlocks,
|
|
processSnippets,
|
|
} from '@openmaic/generation';
|
|
|
|
function createPromptsFixture(): string {
|
|
const promptsDir = mkdtempSync(join(tmpdir(), 'openmaic-generation-prompts-'));
|
|
mkdirSync(join(promptsDir, 'snippets'));
|
|
mkdirSync(join(promptsDir, 'templates'));
|
|
return promptsDir;
|
|
}
|
|
|
|
describe('loader semantics', () => {
|
|
test('loads and interpolates a known prompt', () => {
|
|
const result = buildPrompt(PROMPT_IDS.SLIDE_ACTIONS, {
|
|
title: 'Test Slide',
|
|
keyPoints: '1. point one',
|
|
description: 'desc',
|
|
elements: '[]',
|
|
courseContext: '',
|
|
agents: '',
|
|
userProfile: '',
|
|
languageDirective: 'en',
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result!.system.length).toBeGreaterThan(100);
|
|
expect(result!.user).toContain('Test Slide');
|
|
});
|
|
|
|
test('defaults the project summary for the previous pbl-actions variable set', () => {
|
|
const result = buildPrompt(PROMPT_IDS.PBL_ACTIONS, {
|
|
title: 'Resilient Systems',
|
|
projectTopic: 'Retry handling',
|
|
projectDescription: 'Practice resilient generation',
|
|
keyPoints: '1. Classify failures',
|
|
description: 'Build a retry-aware workflow',
|
|
courseContext: '',
|
|
agents: '',
|
|
languageDirective: 'en',
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result!.system).toContain(
|
|
'(No generated milestones are available; introduce the project topic without inventing any.)',
|
|
);
|
|
expect(`${result!.system}\n${result!.user}`).not.toMatch(/\{\{\w[\w-]*\}\}/);
|
|
});
|
|
|
|
test('inlines a snippet exactly once', () => {
|
|
const promptsDir = createPromptsFixture();
|
|
writeFileSync(join(promptsDir, 'snippets', 'example.md'), 'inlined content\n');
|
|
|
|
expect(processSnippets('Before {{snippet:example}} after', promptsDir)).toBe(
|
|
'Before inlined content after',
|
|
);
|
|
});
|
|
|
|
test('returns null for an unknown prompt', () => {
|
|
// @ts-expect-error -- exercise the runtime failure path.
|
|
expect(loadPrompt('does-not-exist')).toBeNull();
|
|
});
|
|
|
|
test('throws when a referenced snippet is missing', () => {
|
|
expect(() => processSnippets('{{snippet:does-not-exist}}')).toThrow(/Snippet not found/);
|
|
});
|
|
|
|
test('does not recursively process snippet markers inside snippets', () => {
|
|
const promptsDir = createPromptsFixture();
|
|
writeFileSync(join(promptsDir, 'snippets', 'outer.md'), 'outer {{snippet:inner}}\n');
|
|
writeFileSync(join(promptsDir, 'snippets', 'inner.md'), 'inner\n');
|
|
|
|
expect(processSnippets('{{snippet:outer}}', promptsDir)).toBe('outer {{snippet:inner}}');
|
|
});
|
|
|
|
test('propagates a missing snippet referenced by user.md', () => {
|
|
const promptsDir = createPromptsFixture();
|
|
const promptDir = join(promptsDir, 'templates', 'fixture');
|
|
mkdirSync(promptDir);
|
|
writeFileSync(join(promptDir, 'system.md'), 'system prompt\n');
|
|
writeFileSync(join(promptDir, 'user.md'), '{{snippet:not-packaged}}\n');
|
|
|
|
// @ts-expect-error -- fixture is intentionally outside the public PromptId union.
|
|
expect(() => loadPrompt('fixture', promptsDir)).toThrow(/Snippet not found: not-packaged/);
|
|
});
|
|
|
|
test('keeps conditional content when its condition is truthy', () => {
|
|
expect(processConditionalBlocks('A {{#if enabled}}kept{{/if}} B', { enabled: true })).toBe(
|
|
'A kept B',
|
|
);
|
|
});
|
|
|
|
test('drops conditional content when its condition is falsey', () => {
|
|
expect(processConditionalBlocks('A {{#if enabled}}dropped{{/if}} B', { enabled: false })).toBe(
|
|
'A B',
|
|
);
|
|
});
|
|
|
|
test('does not recursively process nested conditional blocks', () => {
|
|
const template = '{{#if outer}}outer {{#if inner}}inner{{/if}}{{/if}}';
|
|
expect(processConditionalBlocks(template, { outer: true, inner: true })).toBe(
|
|
'outer {{#if inner}}inner{{/if}}',
|
|
);
|
|
});
|
|
|
|
test('interpolates string variables', () => {
|
|
expect(interpolateVariables('Hello {{name}}', { name: 'Ada' })).toBe('Hello Ada');
|
|
});
|
|
|
|
test('interpolates object variables as two-space JSON', () => {
|
|
expect(interpolateVariables('{{payload}}', { payload: { nested: true } })).toBe(
|
|
'{\n "nested": true\n}',
|
|
);
|
|
});
|
|
|
|
test('preserves placeholders whose variable is undefined', () => {
|
|
expect(interpolateVariables('{{missing}}', { missing: undefined })).toBe('{{missing}}');
|
|
});
|
|
|
|
test('leaves kebab-case placeholders untouched', () => {
|
|
expect(interpolateVariables('{{next-agent}}', { 'next-agent': 'ignored' })).toBe(
|
|
'{{next-agent}}',
|
|
);
|
|
});
|
|
});
|