Files
pi-config/extensions/pi-graphify/src/lib/runner.test.ts
2026-05-10 15:29:02 +10:00

227 lines
7.5 KiB
TypeScript

import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { detectFiles, detectPython, ensureGraphifyGitignore, ensureInstalled } from "./runner";
// ---------------------------------------------------------------------------
// Mock exec function
// ---------------------------------------------------------------------------
function createMockExec(
responses: Record<string, { stdout: string; stderr: string; exitCode: number }>,
) {
return vi.fn(async (cmd: string, _opts?: unknown) => {
for (const [pattern, response] of Object.entries(responses)) {
if (cmd.includes(pattern)) return response;
}
return { stdout: "", stderr: "unknown command", exitCode: 1 };
});
}
// ---------------------------------------------------------------------------
// detectPython
// ---------------------------------------------------------------------------
describe("detectPython", () => {
it("returns cached python from graphify-out/.graphify_python", async () => {
const mockExec = createMockExec({
"cat graphify-out/.graphify_python": {
stdout: "/usr/bin/python3\n",
stderr: "",
exitCode: 0,
},
});
const result = await detectPython(mockExec, "python3", "/tmp/test");
expect(result).toBe("/usr/bin/python3");
});
it("detects python from graphify shebang when no cache", async () => {
const mockExec = createMockExec({
"cat graphify-out": { stdout: "", stderr: "", exitCode: 1 },
"which graphify": { stdout: "/usr/local/bin/graphify\n", stderr: "", exitCode: 0 },
"head -1": { stdout: "#!/usr/local/bin/python3.11\n", stderr: "", exitCode: 0 },
});
const result = await detectPython(mockExec, "python3", "/tmp/test");
expect(result).toBe("/usr/local/bin/python3.11");
});
it("falls back to config python when graphify not found", async () => {
const mockExec = createMockExec({
"cat graphify-out": { stdout: "", stderr: "", exitCode: 1 },
"which graphify": { stdout: "", stderr: "", exitCode: 1 },
});
const result = await detectPython(mockExec, "python3.12", "/tmp/test");
expect(result).toBe("python3.12");
});
});
// ---------------------------------------------------------------------------
// ensureInstalled
// ---------------------------------------------------------------------------
describe("ensureInstalled", () => {
it("skips install when graphify is importable", async () => {
const mockExec = vi.fn(async (cmd: string, _opts?: unknown) => {
// All calls return success
if (cmd.includes("import graphify")) {
return { stdout: "0", stderr: "", exitCode: 0 };
}
return { stdout: "", stderr: "", exitCode: 0 };
});
await ensureInstalled(mockExec, "python3", "/tmp/test");
// Only called once (the initial check), no pip install
expect(mockExec).toHaveBeenCalledTimes(1);
expect(mockExec).not.toHaveBeenCalledWith(
expect.stringContaining("pip install"),
expect.anything(),
);
});
it("installs graphifyy when not importable", async () => {
let callIdx = 0;
const mockExec = vi.fn(async (cmd: string, _opts?: unknown) => {
callIdx++;
// First call: check import → fails
if (callIdx === 1 && cmd.includes("import graphify")) {
return { stdout: "1", stderr: "", exitCode: 0 };
}
// Second call: pip install → succeeds
if (cmd.includes("pip install")) {
return { stdout: "", stderr: "", exitCode: 0 };
}
// Third call: verify → succeeds
if (callIdx === 3 && cmd.includes("import graphify")) {
return { stdout: "0", stderr: "", exitCode: 0 };
}
return { stdout: "", stderr: "", exitCode: 0 };
});
await ensureInstalled(mockExec, "python3", "/tmp/test");
expect(mockExec).toHaveBeenCalledWith(
expect.stringContaining("pip install graphifyy"),
expect.anything(),
);
});
});
// ---------------------------------------------------------------------------
// detectFiles
// ---------------------------------------------------------------------------
describe("detectFiles", () => {
it("parses detection output correctly", async () => {
const detectionResult = {
total_files: 5,
total_words: 1200,
files: {
code: ["/tmp/test/main.ts", "/tmp/test/util.ts"],
document: ["/tmp/test/README.md"],
paper: [],
image: [],
video: [],
},
};
const mockExec = createMockExec({
"from graphify.detect import detect": {
stdout: JSON.stringify(detectionResult),
stderr: "",
exitCode: 0,
},
});
const result = await detectFiles(mockExec, "python3", "/tmp/test", "/tmp/test");
expect(result.total_files).toBe(5);
expect(result.total_words).toBe(1200);
expect(result.files.code).toHaveLength(2);
expect(result.files.document).toHaveLength(1);
});
it("throws on detection failure", async () => {
const mockExec = createMockExec({
"from graphify.detect import detect": {
stdout: "",
stderr: "ModuleNotFoundError: No module named 'graphify'",
exitCode: 1,
},
});
await expect(detectFiles(mockExec, "python3", "/tmp/test", "/tmp/test")).rejects.toThrow(
"graphify detect failed",
);
});
});
// ---------------------------------------------------------------------------
// ensureGraphifyGitignore
// ---------------------------------------------------------------------------
describe("ensureGraphifyGitignore", () => {
it("creates .gitignore with graphify cache exclusions when missing", async () => {
const dir = await mkdtemp(join(tmpdir(), "pi-graphify-test-"));
try {
const result = await ensureGraphifyGitignore(dir);
expect(result.updated).toBe(true);
const content = await readFile(join(dir, ".gitignore"), "utf-8");
expect(content).toContain("graphify-out/cache/");
expect(content).toContain("graphify-out/.graphify_python");
expect(content).toContain("graphify-out/.graphify_root");
expect(content).toContain("graphify-out/cost.json");
expect(content).not.toContain("graphify-out/\n");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it("removes legacy graphify-out/ ignore and preserves other entries", async () => {
const dir = await mkdtemp(join(tmpdir(), "pi-graphify-test-"));
try {
await writeFile(
join(dir, ".gitignore"),
"node_modules/\ngraphify-out/\ncustom-file.txt\n",
"utf-8",
);
const result = await ensureGraphifyGitignore(dir);
expect(result.updated).toBe(true);
const content = await readFile(join(dir, ".gitignore"), "utf-8");
expect(content).toContain("node_modules/");
expect(content).toContain("custom-file.txt");
expect(content).toContain("graphify-out/cache/");
expect(content).toContain("graphify-out/.graphify_python");
expect(content).toContain("graphify-out/.graphify_root");
expect(content).toContain("graphify-out/cost.json");
expect(content).not.toContain("\ngraphify-out/\n");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it("is idempotent when required entries already exist", async () => {
const dir = await mkdtemp(join(tmpdir(), "pi-graphify-test-"));
try {
const expected =
"node_modules/\ngraphify-out/cache/\ngraphify-out/.graphify_python\ngraphify-out/.graphify_root\ngraphify-out/cost.json\n";
await writeFile(join(dir, ".gitignore"), expected, "utf-8");
const result = await ensureGraphifyGitignore(dir);
expect(result.updated).toBe(false);
const content = await readFile(join(dir, ".gitignore"), "utf-8");
expect(content).toBe(expected);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});