import { expect, test, spyOn } from "bun:test"; import { resolve } from "node:path"; import { cachePaths, loadConfig, resolveConfig } from "../src"; test("defaults", () => { const warn = spyOn(console, "warn").mockImplementation(() => {}); const c = loadConfig({}); expect(c.port).toBe(3000); expect(c.databaseUrl).toBe("sqlite://./data/stcourses.db"); expect(c.dataDir).toBe("./data"); // configured dirs first, then the importer's folders and the studio's published worktrees expect(c.courseDirs).toEqual(["./overlays", "./courses", resolve("./data/courses"), resolve("./data/worktrees")]); expect(c.importedCoursesDir).toBe(resolve("./data/courses")); expect(c.studioReposDir).toBe(resolve("./data/studio/repos")); expect(c.studioWorktreesDir).toBe(resolve("./data/worktrees")); expect(c.cacheDir).toBe(resolve("./data/cache")); expect(c.importRoots).toEqual([resolve("./data/import-inbox")]); expect(c.uploadMaxMb).toBe(100); expect(c.aiAllowedBaseUrls).toEqual([]); expect(c.transcribeUrl).toBeNull(); expect(c.python).toBe("python3"); expect(c.publicUrl).toBe("http://localhost:3000"); expect(warn).toHaveBeenCalledTimes(1); warn.mockRestore(); }); test("colon splitting and overrides", () => { const c = loadConfig({ STC_PORT: "8080", STC_COURSE_DIRS: "/a b:/c", STC_SECRET: "s", STC_PUBLIC_URL: "https://x.test", STC_DATA_DIR: "/d", STC_IMPORT_ROOTS: "/in box:/other", STC_UPLOAD_MAX_MB: "5", STC_AI_ALLOWED_BASE_URLS: "http://gw.lan:8080, https://x.test", STC_TRANSCRIBE_URL: "http://whisper.lan/v1/audio/transcriptions", STC_PYTHON: "/opt/py/bin/python", }); expect(c.port).toBe(8080); expect(c.courseDirs).toEqual(["/a b", "/c", "/d/courses", "/d/worktrees"]); expect(c.secret).toBe("s"); expect(c.publicUrl).toBe("https://x.test"); expect(c.importRoots).toEqual(["/in box", "/other"]); expect(c.uploadMaxMb).toBe(5); expect(c.aiAllowedBaseUrls).toEqual(["http://gw.lan:8080", "https://x.test"]); expect(c.transcribeUrl).toBe("http://whisper.lan/v1/audio/transcriptions"); expect(c.python).toBe("/opt/py/bin/python"); }); test("configured dirs that already name the derived dirs are not repeated", () => { const c = loadConfig({ STC_SECRET: "s", STC_DATA_DIR: "/d", STC_COURSE_DIRS: "/d/courses:/x" }); expect(c.courseDirs).toEqual(["/d/courses", "/x", "/d/worktrees"]); expect(resolveConfig({ dataDir: "/d", courseDirs: ["/d/worktrees/"] }).courseDirs).toEqual(["/d/worktrees/", "/d/courses"]); }); test("invalid upload cap throws", () => { expect(() => loadConfig({ STC_SECRET: "s", STC_UPLOAD_MAX_MB: "lots" })).toThrow(/STC_UPLOAD_MAX_MB/); }); test("production without a secret throws", () => { expect(() => loadConfig({ NODE_ENV: "production" })).toThrow(/STC_SECRET/); expect(loadConfig({ NODE_ENV: "production", STC_SECRET: "x" }).secret).toBe("x"); }); test("cachePaths: the importer's layout, no escaping segments", () => { const p = cachePaths({ cacheDir: "/d/cache" }); expect(p.deckDir("c", "deck1")).toBe("/d/cache/decks/c/deck1"); expect(p.coversDir("c")).toBe("/d/cache/covers/c"); expect(p.transcriptFile("c", "m1-l1")).toBe("/d/cache/transcripts/c/m1-l1.json"); expect(() => p.coversDir("..")).toThrow(); expect(() => p.deckDir("c", "a/b")).toThrow(); });