Files
openmaic/OpenMAIC/tests/generation/scene-content-route-vocational-gate.test.ts
T

142 lines
4.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import type { SceneOutline } from '@/lib/types/generation';
const callLLMMock = vi.hoisted(() => vi.fn());
const resolveModelFromRequestMock = vi.hoisted(() => vi.fn());
const VOCATIONAL_FLAG = 'OPENMAIC_ENABLE_VOCATIONAL';
let originalVocationalFlag: string | undefined;
vi.mock('@/lib/ai/llm', () => ({
callLLM: callLLMMock,
}));
vi.mock('@/lib/server/resolve-model', () => ({
resolveModelFromRequest: resolveModelFromRequestMock,
}));
describe('scene-content vocational gate', () => {
beforeEach(() => {
originalVocationalFlag = process.env[VOCATIONAL_FLAG];
delete process.env[VOCATIONAL_FLAG];
callLLMMock.mockReset();
resolveModelFromRequestMock.mockReset();
resolveModelFromRequestMock.mockResolvedValue({
model: { provider: 'test.chat', modelId: 'test-model' },
modelInfo: { outputWindow: 4096, capabilities: {} },
modelString: 'test:test-model',
thinkingConfig: undefined,
});
});
afterEach(() => {
if (originalVocationalFlag === undefined) {
delete process.env[VOCATIONAL_FLAG];
} else {
process.env[VOCATIONAL_FLAG] = originalVocationalFlag;
}
});
test('flag off direct/replayed procedural-skill outline is downgraded before content generation', async () => {
vi.resetModules();
process.env[VOCATIONAL_FLAG] = 'false';
callLLMMock.mockResolvedValueOnce({
text: htmlForWidget('diagram'),
});
const { POST } = await import('@/app/api/generate/scene-content/route');
const response = await POST(
mockRequest(createProceduralSkillOutline(), { taskEngineMode: true }),
);
const body = await response.json();
expect(body.success).toBe(true);
expect(body.effectiveOutline.widgetType).toBe('diagram');
expect(body.effectiveOutline.widgetOutline.task).toBeUndefined();
expect(body.content.widgetType).toBe('diagram');
expect(body.content.widgetConfig.type).toBe('diagram');
expect(callLLMMock).toHaveBeenCalledTimes(1);
expect(callLLMMock.mock.calls[0][0].system).not.toContain('Procedural Skill');
});
test('flag off without requirements defaults to safe false for persisted procedural-skill outlines', async () => {
vi.resetModules();
callLLMMock.mockResolvedValueOnce({
text: htmlForWidget('diagram'),
});
const { POST } = await import('@/app/api/generate/scene-content/route');
const response = await POST(mockRequest(createProceduralSkillOutline()));
const body = await response.json();
expect(body.success).toBe(true);
expect(body.effectiveOutline.widgetType).toBe('diagram');
expect(body.content.widgetType).toBe('diagram');
});
test('flag on with effective taskEngineMode allows procedural-skill content generation', async () => {
vi.resetModules();
process.env[VOCATIONAL_FLAG] = '1';
callLLMMock.mockResolvedValueOnce({
text: htmlForWidget('procedural-skill'),
});
const { POST } = await import('@/app/api/generate/scene-content/route');
const response = await POST(
mockRequest(createProceduralSkillOutline(), { taskEngineMode: true }),
);
const body = await response.json();
expect(body.success).toBe(true);
expect(body.effectiveOutline.widgetType).toBe('procedural-skill');
expect(body.content.widgetType).toBe('procedural-skill');
expect(body.content.widgetConfig.type).toBe('procedural-skill');
expect(callLLMMock.mock.calls[0][0].system).toContain('Procedural Skill');
});
});
function mockRequest(outline: SceneOutline, requirements?: { taskEngineMode?: boolean }) {
return {
json: async () => ({
outline,
allOutlines: [outline],
stageId: 'stage-1',
stageInfo: { name: 'Test Stage' },
requirements,
}),
} as unknown as Parameters<typeof import('@/app/api/generate/scene-content/route').POST>[0];
}
function createProceduralSkillOutline(): SceneOutline {
return {
id: 'scene-procedural-skill',
type: 'interactive',
title: 'Device Calibration Practice',
description: 'Practice a generic calibration procedure with step feedback.',
keyPoints: ['Follow steps in order', 'Check each success criterion'],
order: 1,
widgetType: 'procedural-skill',
widgetOutline: {
concept: 'calibration procedure',
procedureType: 'operation',
task: 'Calibrate a training device',
tools: ['multimeter', 'checklist'],
steps: ['Inspect the device', 'Connect the tool', 'Confirm the reading'],
successCriteria: ['No visible damage', 'Reading is within range'],
errorConsequences: ['Unsafe readings require stopping and rechecking'],
},
};
}
function htmlForWidget(type: string): string {
return `<!DOCTYPE html>
<html>
<body>
<script type="application/json" id="widget-config">
{"type": "${type}"}
</script>
<main>${type} widget</main>
</body>
</html>`;
}