import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { Script, createContext } from "node:vm"; import { describe, expect, it, vi, afterEach } from "vitest"; import { ModuleKind, ScriptTarget, transpileModule } from "typescript"; interface CliOptions { brainPath: string; workspaceId: string; userId: string; apply: boolean; } interface MockDirent { name: string; isFile: () => boolean; } interface ScriptInternals { parseCliArgs: (args: string[]) => CliOptions; mapTaskStatus: (rawStatus: string | null) => { status: string; issue: string | null }; mapTaskPriority: (rawPriority: string | null) => { priority: string; issue: string | null }; mapProjectStatus: (rawStatus: string | null) => { status: string; issue: string | null }; normalizeDomain: (rawDomain: string | null | undefined) => string | null; listJsonFiles: (directoryPath: string) => Promise; parseTaskFile: ( rawFile: unknown, sourceFile: string, parseIssues: string[] ) => { version: string; domain: string; sourceFile: string; tasks: Array<{ id: string; title: string }>; } | null; parseProjectFile: ( rawFile: unknown, sourceFile: string, parseIssues: string[] ) => { version: string; sourceFile: string; project: { id: string; name: string }; } | null; loadTaskFiles: ( taskDirectory: string, parseIssues: string[], rootPath: string ) => Promise< Array<{ version: string; domain: string; sourceFile: string; tasks: Array<{ id: string; title: string }>; }> >; main: () => Promise; } interface LoaderOptions { argv?: string[]; homeDirectory?: string; readdirImpl?: (directoryPath: string, options: unknown) => Promise; readFileImpl?: (filePath: string, encoding: string) => Promise; } interface LoaderResult { internals: ScriptInternals; readdirMock: ReturnType; readFileMock: ReturnType; prismaClientConstructor: ReturnType; } const testDir = path.dirname(fileURLToPath(import.meta.url)); const scriptPath = path.resolve(testDir, "..", "migrate-brain.ts"); const nativeRequire = createRequire(import.meta.url); const makeDirent = (name: string, isFile: boolean): MockDirent => ({ name, isFile: () => isFile, }); function buildInstrumentedSource(source: string): string { const invocationMarker = "main().catch((error: unknown) => {"; const markerIndex = source.lastIndexOf(invocationMarker); if (markerIndex < 0) { throw new Error("Could not find main invocation in migrate-brain.ts"); } const sourceWithoutMain = source.slice(0, markerIndex); return `${sourceWithoutMain} module.exports.__test = { parseCliArgs, mapTaskStatus, mapTaskPriority, mapProjectStatus, normalizeDomain, listJsonFiles, parseTaskFile, parseProjectFile, loadTaskFiles, main, }; `; } function loadMigrateBrainModule(options: LoaderOptions = {}): LoaderResult { const source = readFileSync(scriptPath, "utf8"); const instrumentedSource = buildInstrumentedSource(source); const transpiled = transpileModule(instrumentedSource, { compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2022, esModuleInterop: true, }, fileName: scriptPath, }); const readdirMock = vi.fn(options.readdirImpl ?? (async (): Promise => [])); const readFileMock = vi.fn(options.readFileImpl ?? (async (): Promise => "{}")); const prismaClientConstructor = vi.fn(() => ({ $disconnect: vi.fn(async () => undefined), })); const processMock = Object.create(process) as NodeJS.Process; processMock.argv = options.argv ?? [ "node", scriptPath, "--workspace-id", "workspace-1", "--user-id", "user-1", ]; const prismaClientModule = { ActivityAction: { CREATED: "CREATED" }, EntityType: { DOMAIN: "DOMAIN", PROJECT: "PROJECT", TASK: "TASK" }, Prisma: {}, PrismaClient: prismaClientConstructor, ProjectStatus: { ACTIVE: "ACTIVE", PLANNING: "PLANNING", PAUSED: "PAUSED", ARCHIVED: "ARCHIVED", }, TaskPriority: { HIGH: "HIGH", MEDIUM: "MEDIUM", LOW: "LOW", }, TaskStatus: { COMPLETED: "COMPLETED", IN_PROGRESS: "IN_PROGRESS", NOT_STARTED: "NOT_STARTED", PAUSED: "PAUSED", ARCHIVED: "ARCHIVED", }, }; const module = { exports: {} as Record }; const requireFromScript = (specifier: string): unknown => { if (specifier === "node:fs/promises") { return { readdir: readdirMock, readFile: readFileMock, }; } if (specifier === "node:os") { return { homedir: () => options.homeDirectory ?? "/home/tester", }; } if (specifier === "node:process") { return processMock; } if (specifier === "../apps/api/node_modules/@prisma/client") { return prismaClientModule; } return nativeRequire(specifier); }; const context = createContext({ module, exports: module.exports, require: requireFromScript, __dirname: path.dirname(scriptPath), __filename: scriptPath, console, process: processMock, setTimeout, clearTimeout, setInterval, clearInterval, Buffer, }); new Script(transpiled.outputText, { filename: scriptPath }).runInContext(context); const exported = module.exports as { __test?: ScriptInternals }; if (!exported.__test) { throw new Error("Failed to expose migrate-brain internals for tests"); } return { internals: exported.__test, readdirMock, readFileMock, prismaClientConstructor, }; } afterEach(() => { vi.restoreAllMocks(); }); describe("migrate-brain mapping helpers", () => { it("maps statuses and priorities, including unknown-value fallbacks", () => { const { internals } = loadMigrateBrainModule(); expect(internals.mapTaskStatus("done")).toEqual({ status: "COMPLETED", issue: null, }); expect(internals.mapTaskStatus("mystery")).toEqual({ status: "NOT_STARTED", issue: 'Unknown task status "mystery" mapped to NOT_STARTED', }); expect(internals.mapTaskPriority("critical")).toEqual({ priority: "HIGH", issue: null, }); expect(internals.mapTaskPriority(null)).toEqual({ priority: "MEDIUM", issue: 'Unknown task priority "null" mapped to MEDIUM', }); expect(internals.mapProjectStatus("in-progress")).toEqual({ status: "ACTIVE", issue: null, }); expect(internals.mapProjectStatus("untracked")).toEqual({ status: "PLANNING", issue: 'Unknown project status "untracked" mapped to PLANNING', }); }); it("normalizes domain strings into lowercase slugs", () => { const { internals } = loadMigrateBrainModule(); expect(internals.normalizeDomain(" Platform Core ")).toBe("platform-core"); expect(internals.normalizeDomain("###")).toBeNull(); expect(internals.normalizeDomain(undefined)).toBeNull(); }); }); describe("migrate-brain CLI parsing", () => { it("parses required arguments and default brain path", () => { const { internals } = loadMigrateBrainModule({ homeDirectory: "/opt/home" }); const parsed = internals.parseCliArgs([ "--workspace-id", "workspace-abc", "--user-id", "user-xyz", ]); expect(parsed).toEqual({ brainPath: path.resolve("/opt/home/src/jarvis-brain"), workspaceId: "workspace-abc", userId: "user-xyz", apply: false, }); }); it("supports inline flags and apply mode", () => { const { internals } = loadMigrateBrainModule({ homeDirectory: "/opt/home" }); const parsed = internals.parseCliArgs([ "--brain-path=~/custom-brain", "--workspace-id=workspace-1", "--user-id=user-1", "--apply", ]); expect(parsed).toEqual({ brainPath: path.resolve("/opt/home/custom-brain"), workspaceId: "workspace-1", userId: "user-1", apply: true, }); }); it("throws on missing required flags and unknown flags", () => { const { internals } = loadMigrateBrainModule(); expect(() => internals.parseCliArgs(["--workspace-id", "workspace-1"])).toThrowError( "Both --workspace-id and --user-id are required" ); expect(() => internals.parseCliArgs(["--workspace-id", "workspace-1", "--user-id", "user-1", "--nope"]) ).toThrowError("Unknown flag: --nope"); }); }); describe("migrate-brain file discovery", () => { it("returns only .json files in sorted order", async () => { const readdirImpl = async (): Promise => [ makeDirent("z.json", true), makeDirent("notes.md", true), makeDirent("nested", false), makeDirent("a.json", true), ]; const { internals, readdirMock } = loadMigrateBrainModule({ readdirImpl }); const files = await internals.listJsonFiles("/tmp/brain/data/tasks"); expect(files).toEqual([ path.join("/tmp/brain/data/tasks", "a.json"), path.join("/tmp/brain/data/tasks", "z.json"), ]); expect(readdirMock).toHaveBeenCalledWith("/tmp/brain/data/tasks", { withFileTypes: true, }); }); }); describe("migrate-brain parsing and validation", () => { it("loads task files and tracks validation issues for invalid task records", async () => { const taskDirectory = "/tmp/brain/data/tasks"; const taskFilePath = path.join(taskDirectory, "core.json"); const readdirImpl = async (): Promise => [makeDirent("core.json", true)]; const readFileImpl = async (filePath: string): Promise => { if (filePath === taskFilePath) { return JSON.stringify({ version: "1", domain: "core", tasks: [{ id: "TASK-1", title: "Valid" }, { id: "TASK-2" }], }); } throw new Error(`Unexpected file read: ${filePath}`); }; const { internals, readFileMock } = loadMigrateBrainModule({ readdirImpl, readFileImpl, }); const parseIssues: string[] = []; const loaded = await internals.loadTaskFiles(taskDirectory, parseIssues, "/tmp/brain"); expect(loaded).toHaveLength(1); expect(loaded[0]).toMatchObject({ version: "1", domain: "core", sourceFile: path.join("data", "tasks", "core.json"), }); expect(loaded[0].tasks).toHaveLength(1); expect(parseIssues).toContain( 'data/tasks/core.json task[1]: required field "title" missing or invalid' ); expect(readFileMock).toHaveBeenCalledWith(taskFilePath, "utf8"); }); it("rejects invalid project payloads", () => { const { internals } = loadMigrateBrainModule(); const parseIssues: string[] = []; const parsed = internals.parseProjectFile( { version: "1", project: { name: "Project without ID", }, }, "data/projects/project.json", parseIssues ); expect(parsed).toBeNull(); expect(parseIssues).toContain( 'data/projects/project.json project: required field "id" missing or invalid' ); }); }); describe("migrate-brain dry-run behavior", () => { it("does not instantiate Prisma client when --apply is omitted", async () => { const brainPath = "/tmp/brain"; const readdirImpl = async (): Promise => []; const { internals, prismaClientConstructor, readdirMock } = loadMigrateBrainModule({ argv: [ "node", scriptPath, "--brain-path", brainPath, "--workspace-id", "workspace-1", "--user-id", "user-1", ], readdirImpl, }); const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); await internals.main(); expect(prismaClientConstructor).not.toHaveBeenCalled(); expect(readdirMock).toHaveBeenCalledWith(path.join(brainPath, "data", "tasks"), { withFileTypes: true, }); expect(readdirMock).toHaveBeenCalledWith(path.join(brainPath, "data", "projects"), { withFileTypes: true, }); expect(logSpy).toHaveBeenCalledWith("Dry-run complete. Re-run with --apply to write records."); }); });