2f198ea0d2
BREAKING: Heuristic fallback removed — LLM client now required Changes: - Remove extractFileHeuristic, extractPackageHeuristic, all helpers - extractFileLLM / extractPackageLLM now throw LLMError when client missing - Add hybrid binary detection: extension blacklist + content sniffing - Increase file size limit: 50KB → 500KB (text files only) - Restructure src/ into subdirectories: - src/llm/ — all LLM clients, extract, batch, cache, error - src/ast/ — AST extraction - src/cli/ — CLI entry point - Update all imports across codebase and tests - Add tests/mock-llm.ts helper for deterministic mock clients - Update all tests to use mock LLM clients (no heuristics dependency) - All 52 tests passing (including 8 real LLM integration tests)
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { extractFileLLM } from "../src/llm/llm-extract.js";
|
|
import { writeFileSync, mkdtempSync } from "fs";
|
|
import { join } from "path";
|
|
import { tmpdir } from "os";
|
|
import type { LLMClient } from "../src/llm/llm-client.js";
|
|
|
|
function createMockClient(response: string): LLMClient {
|
|
return {
|
|
async complete() {
|
|
return response;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("llm-extract with mock client", () => {
|
|
it("uses LLM client when provided", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-map-"));
|
|
const file = join(dir, "test.ts");
|
|
writeFileSync(file, `export const foo = 1;`);
|
|
|
|
const mockClient = createMockClient(
|
|
"PURPOSE: Test file\nDEPS: none\nCONCEPTS: testing",
|
|
);
|
|
|
|
const result = await extractFileLLM(file, mockClient, tmpdir());
|
|
expect(result.purpose).toBe("Test file");
|
|
expect(result.deps).toEqual([]);
|
|
expect(result.concepts).toContain("testing");
|
|
});
|
|
|
|
it("throws without LLM client", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-map-"));
|
|
const file = join(dir, "test.ts");
|
|
writeFileSync(file, `export const foo = 1;`);
|
|
|
|
await expect(extractFileLLM(file)).rejects.toThrow("No LLM client configured");
|
|
});
|
|
|
|
it("skips large files", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-map-"));
|
|
const file = join(dir, "big.ts");
|
|
writeFileSync(file, "x".repeat(600 * 1024));
|
|
|
|
const mockClient = createMockClient(
|
|
"PURPOSE: Should not call\nDEPS: none\nCONCEPTS: none",
|
|
);
|
|
|
|
const result = await extractFileLLM(file, mockClient);
|
|
expect(result.purpose).toBe("Large file");
|
|
});
|
|
|
|
it("skips binary files", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-map-"));
|
|
const file = join(dir, "image.png");
|
|
// Write some binary-looking content with null bytes
|
|
const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
writeFileSync(file, buf);
|
|
|
|
const mockClient = createMockClient(
|
|
"PURPOSE: Should not call\nDEPS: none\nCONCEPTS: none",
|
|
);
|
|
|
|
const result = await extractFileLLM(file, mockClient);
|
|
expect(result.purpose).toBe("Binary file");
|
|
});
|
|
|
|
it("parses response with deps and concepts", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-map-"));
|
|
const file = join(dir, "test.ts");
|
|
writeFileSync(file, `import { foo } from "bar";\nexport const x = 1;`);
|
|
|
|
const mockClient = createMockClient(
|
|
"PURPOSE: Config module\nDEPS: bar, baz\nCONCEPTS: constants, config",
|
|
);
|
|
|
|
const result = await extractFileLLM(file, mockClient, tmpdir());
|
|
expect(result.purpose).toBe("Config module");
|
|
expect(result.deps).toEqual(["bar", "baz"]);
|
|
expect(result.concepts).toEqual(["constants", "config"]);
|
|
});
|
|
});
|