Files
STCourses/packages/feature-studio/test/import-fixture.ts
T
Lucas WintherandClaude Opus 5.5 df6d235070 feat(studio): convert an imported course into a studio repo, media kept outside git
Small files are committed unchanged (visibility and source.kind stay); large media
are referenced from .stcourses/media.json, ignored, and hard-linked into every
worktree from <dataDir>/media/<id>, where the import folder is moved. Studio writes
to referenced media are refused and never write through a shared inode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:31:19 +01:00

87 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A synthetic imported course, laid out as the importer leaves it in <dataDir>/courses/<id>. Nothing here is real material.
import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { LARGE_FILE_BYTES } from "../src/server/media-refs";
import { tempDir } from "./helpers";
export const VIDEO = "media/01 Intro – Übersicht.mp4";
export const VIDEO2 = "media/[2] Next step.mp4";
export const BIG_PDF = "decks/big deck.pdf";
/** Fake MP4 bytes: the server only streams them, nothing decodes them. */
export const videoBytes = (n = 4096, seed = 7) => new Uint8Array(n).map((_, i) => (i * seed) % 256);
export interface ImportFixtureOpts {
id?: string;
kind?: string;
manifestId?: string;
root?: string;
visibility?: string;
/** Extra hostile bits: a symlink to outside, a .git dir, a nested .gitignore that hides a lesson. */
hostile?: boolean;
}
function put(dir: string, rel: string, content: string | Uint8Array) {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), content);
}
export function manifestYaml(o: ImportFixtureOpts = {}): string {
const id = o.manifestId ?? o.id ?? "imp";
return [
"schema: 1",
`id: ${id}`,
"title: Imported Course",
`visibility: ${o.visibility ?? "restricted"}`,
...(o.root ? [`root: ${o.root}`] : []),
"cover: media/cover.jpg",
"source:",
` kind: ${o.kind ?? "video-folder"}`,
" origin: /import/synthetic",
"decks:",
" - id: d1",
` file: ${BIG_PDF}`,
" attachTo: l1",
"modules:",
" - id: m1",
" title: Videos",
" lessons:",
" - id: l1",
" title: Intro",
" type: video",
` file: "${VIDEO}"`,
" transcript: transcripts/l1.md",
" - id: l2",
" title: Next step",
" type: video",
` file: "${VIDEO2}"`,
" - id: notes",
" title: Reading",
" type: text",
" file: code/S01 - Basics/README.md",
"",
].join("\n");
}
/** Writes the course into `<dataDir>/courses/<id>` and returns that folder. */
export function makeImportedCourse(dataDir: string, o: ImportFixtureOpts = {}): string {
const id = o.id ?? "imp";
const dir = join(dataDir, "courses", id);
mkdirSync(dir, { recursive: true });
put(dir, "stcourse.yaml", manifestYaml(o));
put(dir, VIDEO, videoBytes(4096, 7));
put(dir, VIDEO2, videoBytes(2048, 11));
put(dir, "media/cover.jpg", new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0x10, 0x4a, 0x46, 0x49, 0x46, 0, 1, 0xff, 0xd9]));
put(dir, BIG_PDF, new Uint8Array(LARGE_FILE_BYTES + 100).fill(0x25));
put(dir, "transcripts/l1.md", "# Intro\n\n[0:00] Hello.\n");
put(dir, "code/S01 - Basics/README.md", "# Basics\n\nRead me.\n");
if (o.hostile) {
// the companion code repo's own .gitignore hides Markdown: the lesson must still be committed
put(dir, "code/S01 - Basics/.gitignore", "*.md\n");
put(dir, "code/.git/config", "[core]\n");
const outside = join(tempDir(), "secret.txt");
writeFileSync(outside, "outside\n");
symlinkSync(outside, join(dir, "code/leak.txt"));
}
return dir;
}