feat: integrate learning module
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 20:52:16 +08:00
parent 26b52d76e3
commit 01bee3188b
107 changed files with 6318 additions and 12063 deletions

View File

@@ -0,0 +1,95 @@
// @vitest-environment node
import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import AdmZip from 'adm-zip';
import {
createLearningPlayerServer,
registerLearningCoursePackage,
} from '@electron/services/learning-player-server';
async function writePlayerArtifact(root: string): Promise<void> {
const html = '<!doctype html><main>player</main>';
await mkdir(join(root, '_next', 'static', 'chunks'), { recursive: true });
await mkdir(join(root, 'avatars'), { recursive: true });
await mkdir(join(root, 'public'), { recursive: true });
await writeFile(join(root, 'index.html'), html);
await writeFile(join(root, 'artifact.json'), JSON.stringify({
schemaVersion: 1,
entrypoint: 'index.html',
htmlSha256: createHash('sha256').update(html).digest('hex'),
}));
await writeFile(join(root, '_next', 'static', 'chunks', 'app.js'), 'window.player = true');
await writeFile(join(root, 'avatars', 'teacher.png'), 'avatar');
await writeFile(join(root, 'public', 'openmaic-mark.png'), 'mark');
}
describe('Learning player static server', () => {
let root: string | null = null;
let close: (() => Promise<void>) | null = null;
afterEach(async () => {
if (close) await close();
if (root) await rm(root, { recursive: true, force: true });
close = null;
root = null;
});
it('serves only the embedded OpenMAIC player and immutable static assets', async () => {
root = await mkdtemp(join(tmpdir(), 'makelore-player-server-'));
await writePlayerArtifact(root);
const server = await createLearningPlayerServer({ artifactRoot: root });
close = server.close;
const index = await fetch(server.url);
expect(index.status).toBe(200);
expect(await index.text()).toContain('player');
expect(index.headers.get('content-security-policy')).toContain("object-src 'none'");
const asset = await fetch(new URL('/_next/static/chunks/app.js', server.url));
expect(asset.headers.get('cache-control')).toContain('immutable');
expect(await asset.text()).toContain('window.player');
const avatar = await fetch(new URL('/avatars/teacher.png', server.url));
expect(avatar.status).toBe(200);
expect(avatar.headers.get('content-type')).toBe('image/png');
const publicAsset = await fetch(new URL('/openmaic-mark.png', server.url));
expect(publicAsset.status).toBe(200);
expect(publicAsset.headers.get('cache-control')).toContain('immutable');
expect(await publicAsset.text()).toBe('mark');
expect((await fetch(new URL('/package.json', server.url))).status).toBe(404);
expect((await fetch(new URL('/%2e%2e/artifact.json', server.url))).status).toBe(404);
});
it('serves only audio and media entries from a verified registered course package', async () => {
root = await mkdtemp(join(tmpdir(), 'makelore-player-server-'));
await writePlayerArtifact(root);
const archivePath = join(root, 'course.zip');
const archive = new AdmZip();
archive.addFile('modules/module-1/audio/line.mp3', Buffer.from('audio-bytes'));
archive.addFile('modules/module-1/media/diagram.png', Buffer.from('image-bytes'));
archive.addFile('modules/module-1/bundle.json', Buffer.from('{}'));
archive.writeZip(archivePath);
const contentHash = 'a'.repeat(64);
registerLearningCoursePackage('course-assets-test', contentHash, archivePath);
const server = await createLearningPlayerServer({ artifactRoot: root });
close = server.close;
const audio = await fetch(new URL(
`/course-assets/course-assets-test/${contentHash}/modules/module-1/audio/line.mp3`,
server.url,
));
expect(audio.status).toBe(200);
expect(audio.headers.get('content-type')).toBe('audio/mpeg');
expect(await audio.text()).toBe('audio-bytes');
expect((await fetch(new URL(
`/course-assets/course-assets-test/${contentHash}/modules/module-1/bundle.json`,
server.url,
))).status).toBe(404);
});
});