import { describe, it, expect } from "vitest"; import Database from "better-sqlite3"; import { initSchema } from "./schema"; import { scanModules, scanFiles, scanSymbols, scanCalls, scanTechTags, scanModuleDeps, scanFileImports } from "./scanner"; describe("scanModules", () => { it("should detect src/modules/* as modules", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); const modules = db .prepare("SELECT name FROM modules ORDER BY name") .all() as { name: string }[]; const names = modules.map((m) => m.name); expect(names).toContain("exams"); expect(names).toContain("grades"); expect(names).toContain("classes"); }); it("should detect shared/ as a module with layer=shared", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/shared"); const shared = db .prepare("SELECT * FROM modules WHERE name = 'shared'") .get() as { layer: string }; expect(shared.layer).toBe("shared"); }); it("should set layer=modules for src/modules/*", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); const exams = db .prepare("SELECT * FROM modules WHERE name = 'exams'") .get() as { layer: string }; expect(exams.layer).toBe("modules"); }); }); describe("scanFiles", () => { it("should scan .ts and .tsx files in modules", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); const files = db .prepare("SELECT path, kind FROM files WHERE path LIKE '%exams%' LIMIT 5") .all() as { path: string; kind: string }[]; expect(files.length).toBeGreaterThan(0); const actionFile = files.find((f) => f.kind === "actions"); expect(actionFile).toBeDefined(); }); it("should detect file kind from filename", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); const actionsFile = db .prepare("SELECT kind FROM files WHERE path LIKE '%exams/actions.ts'") .get() as { kind: string }; expect(actionsFile.kind).toBe("actions"); const dataAccessFile = db .prepare("SELECT kind FROM files WHERE path LIKE '%exams/data-access.ts'") .get() as { kind: string }; expect(dataAccessFile.kind).toBe("data-access"); }); it("should count lines correctly", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); const file = db .prepare("SELECT lines FROM files WHERE path LIKE '%exams/actions.ts'") .get() as { lines: number }; expect(file.lines).toBeGreaterThan(0); }); it("should detect use server and use client directives", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); const serverFile = db .prepare("SELECT is_server FROM files WHERE path LIKE '%exams/actions.ts'") .get() as { is_server: number }; expect(serverFile.is_server).toBe(1); }); }); describe("scanSymbols", () => { it("should extract exported functions from actions.ts", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); const symbols = db .prepare( "SELECT name, is_exported, is_async FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path LIKE '%exams/actions.ts') LIMIT 5" ) .all() as { name: string; is_exported: number; is_async: number }[]; expect(symbols.length).toBeGreaterThan(0); const exported = symbols.find((s) => s.is_exported === 1); expect(exported).toBeDefined(); }); it("should detect Server Action by 'use server' directive", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); const serverActions = db .prepare("SELECT name FROM symbols WHERE is_server_action = 1 LIMIT 5") .all() as { name: string }[]; expect(serverActions.length).toBeGreaterThan(0); }); it("should extract function signatures", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); const withSignature = db .prepare("SELECT signature FROM symbols WHERE signature IS NOT NULL AND signature != '' LIMIT 1") .get() as { signature: string }; expect(withSignature).toBeDefined(); }); }); describe("scanCalls", () => { it("should detect calls between symbols", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); scanCalls(db); const calls = db .prepare("SELECT count(*) as count FROM calls") .get() as { count: number }; expect(calls.count).toBeGreaterThan(0); }); it("should record external calls (callee_external)", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); scanCalls(db); const externalCalls = db .prepare("SELECT callee_external FROM calls WHERE callee_external IS NOT NULL LIMIT 5") .all() as { callee_external: string }[]; expect(externalCalls.length).toBeGreaterThan(0); }); }); describe("scanTechTags", () => { it("should detect cacheFn usage", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); scanTechTags(db); const cacheFnFiles = db .prepare( `SELECT s.name FROM symbols s JOIN symbol_tech_tags stt ON s.id = stt.symbol_id JOIN tech_tags t ON stt.tag_id = t.id WHERE t.name = 'cacheFn' LIMIT 5` ) .all() as { name: string }[]; expect(cacheFnFiles.length).toBeGreaterThan(0); }); it("should create tech_tags entries", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); scanTechTags(db); const tags = db .prepare("SELECT name FROM tech_tags ORDER BY name") .all() as { name: string }[]; const tagNames = tags.map((t) => t.name); expect(tagNames).toContain("cacheFn"); expect(tagNames).toContain("zustand"); expect(tagNames).toContain("Drizzle"); }); }); describe("scanModuleDeps", () => { it("should detect module dependencies from imports", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanFileImports(db); scanModuleDeps(db); const deps = db .prepare("SELECT count(*) as count FROM module_deps") .get() as { count: number }; expect(deps.count).toBeGreaterThan(0); }); it("should detect data-access-call dependency type", () => { const db = new Database(":memory:"); initSchema(db); scanModules(db, "src/modules"); scanFiles(db, "src/modules"); scanSymbols(db); scanCalls(db); scanModuleDeps(db); const dataAccessDeps = db .prepare("SELECT count(*) as count FROM module_deps WHERE dep_type = 'data-access-call'") .get() as { count: number }; expect(dataAccessDeps.count).toBeGreaterThan(0); }); });