feat: productionize learning engine and classroom

This commit is contained in:
inman
2026-08-16 21:44:52 +08:00
parent 2d04197f3f
commit ba35adfbfa
124 changed files with 9071 additions and 796 deletions

View File

@@ -202,6 +202,31 @@ describe('packageCourseware (happy path)', () => {
});
describe('packageCourseware (missing resources)', () => {
test('allows intentional silent narration when TTS was disabled', async () => {
const scenes = structuredClone(makeScenes());
const speech = scenes[0].actions?.find((action) => action.type === 'speech');
if (speech?.type === 'speech') speech.audioUrl = 'https://should-not-survive.example/audio.mp3';
const result = await packageCourseware({
coursewareId: 'cw-silent',
version: 1,
stage: makeStage(),
scenes,
requireNarrationAudio: false,
requireComplete: true,
publishedAt: '2026-08-15T00:00:00.000Z',
});
expect(result.completeness.complete).toBe(true);
expect(result.completeness.audioCount).toBe(0);
expect(result.completeness.missing).not.toContainEqual(
expect.objectContaining({ kind: 'audio' }),
);
expect(result.meta.runtimeCapabilities?.packaged.audio).toBe(false);
expect(result.manifest.mediaIndex['audio/aud-1.mp3']).toBeUndefined();
expect(result.manifest.scenes[0].actions?.[0]).not.toHaveProperty('audioRef');
expect(result.manifest.scenes[0].actions?.[0]).not.toHaveProperty('audioUrl');
});
test('reports missing audio and marks the media index entry', async () => {
const result = await packageCourseware({
coursewareId: 'cw-1',
@@ -301,4 +326,30 @@ describe('packageCourseware (strict frozen publish contract)', () => {
}),
).rejects.toThrow(/incomplete.*interactive.*lab\.js/i);
});
test('rejects an inlined Pyodide loader whose wasm and stdlib remain runtime downloads', async () => {
const scenes = makeScenes().map((scene) =>
scene.content.type === 'interactive'
? ({
...scene,
content: {
...scene.content,
html: '<main>Python lab</main><script>const py = await loadPyodide();</script>',
},
} as Scene)
: scene,
);
await expect(packageCourseware({
coursewareId: 'cw-offline-python',
version: 1,
stage: makeStage(),
scenes,
resolveAudioBytes: async () => AUDIO_BYTES,
publishedAt: '2026-08-16T00:00:00.000Z',
requireAgentRoster: true,
requireInteractiveHtml: true,
strictInteractiveAssets: true,
requireComplete: true,
})).rejects.toThrow(/incomplete.*loadPyodide/i);
});
});