Files
NextEdu/scripts/arch-scan/scanner.test.ts

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);
});
});