Files
openmaic/OpenMAIC/packages/@openmaic/generation/test/outline-generator.test.ts
2026-08-16 14:58:47 +08:00

225 lines
7.3 KiB
TypeScript

// Behavior-parity port of lib/generation/outline-generator.ts assertions from
// tests/generation/media-prompt-wiring.test.ts and procedural-skill-content-gates.test.ts.
import { describe, expect, test, vi } from 'vitest';
import {
DEFAULT_LANGUAGE_DIRECTIVE,
applyOutlineFallbacks,
generateSceneOutlinesFromRequirements,
sanitizeProceduralSkillOutline,
type AICallFn,
type GenerationLogger,
type SceneOutline,
type UserRequirements,
} from '@openmaic/generation';
const baseOutline: SceneOutline = {
id: 'scene_1',
type: 'slide',
title: 'Photosynthesis',
description: 'How plants make food',
keyPoints: ['light', 'water', 'carbon dioxide'],
order: 99,
};
describe('generateSceneOutlinesFromRequirements', () => {
test('returns enriched outlines from a valid wrapped response', async () => {
const aiCall: AICallFn = vi.fn(async () =>
JSON.stringify({
languageDirective: 'Teach in English.',
courseTitle: 'Photosynthesis Basics',
outlines: [{ ...baseOutline, id: '', order: 42 }],
}),
);
const result = await generateSceneOutlinesFromRequirements(
{ requirement: 'Teach photosynthesis' },
undefined,
undefined,
aiCall,
);
expect(result.success).toBe(true);
expect(result.data?.languageDirective).toBe('Teach in English.');
expect(result.data?.courseTitle).toBe('Photosynthesis Basics');
expect(result.data?.outlines[0]?.id).toBeTruthy();
expect(result.data?.outlines[0]?.order).toBe(1);
});
test('integrates repairable JSON parsing', async () => {
const response = `{
"languageDirective": "Teach in English.",
"courseTitle": "Repair",
"outlines": [{
"id": "scene_1",
"type": "slide",
"title": "Repairable",
"description": "A repaired response",
"keyPoints": ["one"],
"order: 7"
}]
}`;
const result = await generateSceneOutlinesFromRequirements(
{ requirement: 'Test repair' },
undefined,
undefined,
async () => response,
);
expect(result.success).toBe(true);
expect(result.data?.outlines).toMatchObject([{ title: 'Repairable', order: 1 }]);
});
test('supports the legacy flat-array response with a default language directive', async () => {
const result = await generateSceneOutlinesFromRequirements(
{ requirement: 'Teach photosynthesis' },
undefined,
undefined,
async () => JSON.stringify([baseOutline]),
);
expect(result.success).toBe(true);
expect(result.data?.languageDirective).toBe(DEFAULT_LANGUAGE_DIRECTIVE);
});
test('passes media enable flags into prompt conditionals', async () => {
let capturedPrompt = '';
const aiCall: AICallFn = async (system, user) => {
capturedPrompt = `${system}\n${user}`;
return JSON.stringify({
languageDirective: 'Teach in English.',
courseTitle: 'Evaporation',
outlines: [],
});
};
const requirements: UserRequirements = {
requirement: 'Teach evaporation with an animation',
};
const result = await generateSceneOutlinesFromRequirements(
requirements,
undefined,
undefined,
aiCall,
{ imageGenerationEnabled: false, videoGenerationEnabled: true },
);
expect(result.success).toBe(true);
expect(capturedPrompt).toContain('gen_vid_1');
expect(capturedPrompt).not.toContain('gen_img_');
expect(capturedPrompt).not.toContain('suggestedImageIds');
expect(capturedPrompt).not.toContain('{{');
});
const requirements: UserRequirements = { requirement: 'Teach photosynthesis' };
async function runWith(raw: unknown) {
return generateSceneOutlinesFromRequirements(requirements, undefined, undefined, async () =>
JSON.stringify(raw),
);
}
test('trims and caps a string courseTitle', async () => {
const result = await runWith({
languageDirective: 'Teach in English.',
courseTitle: ` ${'A '.repeat(80)} `,
outlines: [],
});
expect(result.data?.courseTitle?.length).toBeLessThanOrEqual(120);
expect(result.data?.courseTitle?.startsWith(' ')).toBe(false);
});
test.each([
[{ languageDirective: 'Teach in English.', outlines: [] }],
[{ languageDirective: 'Teach in English.', courseTitle: ' ', outlines: [] }],
[{ languageDirective: 'Teach in English.', courseTitle: 123, outlines: [] }],
])('omits a missing, empty, or non-string courseTitle', async (raw) => {
const result = await runWith(raw);
expect(result.success).toBe(true);
expect(result.data?.courseTitle).toBeUndefined();
});
});
describe('outline fallbacks', () => {
test('downgrades incomplete interactive and PBL outlines', () => {
expect(applyOutlineFallbacks({ ...baseOutline, type: 'interactive' }, true).type).toBe('slide');
expect(applyOutlineFallbacks({ ...baseOutline, type: 'pbl' }, true).type).toBe('slide');
expect(
applyOutlineFallbacks(
{
...baseOutline,
type: 'pbl',
pblConfig: { projectTopic: 'Garden', projectDescription: 'Grow it', targetSkills: [] },
},
false,
).type,
).toBe('slide');
});
test('keeps configured interactive and PBL outlines when a language model is present', () => {
const interactive = {
...baseOutline,
type: 'interactive' as const,
widgetType: 'diagram' as const,
widgetOutline: { concept: 'Cycle' },
};
const pbl = {
...baseOutline,
type: 'pbl' as const,
pblConfig: { projectTopic: 'Garden', projectDescription: 'Grow it', targetSkills: [] },
};
expect(applyOutlineFallbacks(interactive, true)).toBe(interactive);
expect(applyOutlineFallbacks(pbl, true)).toBe(pbl);
});
test('logs a fallback through the injected structural logger', () => {
const warn = vi.fn();
const logger: GenerationLogger = {
debug: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
};
applyOutlineFallbacks({ ...baseOutline, type: 'interactive' }, true, { logger });
expect(warn).toHaveBeenCalledOnce();
});
});
describe('sanitizeProceduralSkillOutline', () => {
const procedural: SceneOutline = {
...baseOutline,
type: 'interactive',
widgetType: 'procedural-skill',
widgetOutline: {
concept: 'calibration procedure',
procedureType: 'operation',
task: 'Calibrate a device',
tools: ['meter'],
steps: ['inspect'],
successCriteria: ['within range'],
errorConsequences: ['stop'],
interactions: ['inspect details'],
},
};
test('strips every task-engine field and preserves unrelated widget fields', () => {
const safe = sanitizeProceduralSkillOutline(procedural);
expect(safe.widgetType).toBe('diagram');
expect(safe.widgetOutline).toEqual({
concept: 'calibration procedure',
interactions: ['inspect details'],
});
expect(safe.description).toContain('Present this as a process or structure diagram.');
});
test('uses the fallback description when the source description is empty', () => {
expect(sanitizeProceduralSkillOutline({ ...procedural, description: '' }).description).toBe(
'Present this topic as a process or structure diagram.',
);
});
test('retains procedural-skill only when explicitly allowed', () => {
expect(applyOutlineFallbacks(procedural, true, { allowProceduralSkill: true }).widgetType).toBe(
'procedural-skill',
);
});
});