227 lines
7.5 KiB
TypeScript
227 lines
7.5 KiB
TypeScript
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);
|
|
});
|
|
});
|