feat: move LLM cache to project-local directory
- Cache now lives in <project>/.pi-project-map/cache/llm-cache.json - Removed global ~/.cache/pi-project-map/ usage - Cache travels with the project, no cross-project collisions - Easy to invalidate: rm -rf .pi-project-map/cache/ Files changed: - llm-cache.ts: accept cacheDir parameter, default to project-local - llm-extract.ts: pass cacheDir through to cache functions - init.ts: pass rootPath as cacheDir - patch.ts: accept and pass cacheDir - cli.ts: pass targetPath as cacheDir - pi-extension.ts: pass ctx.cwd as cacheDir - Tests updated to use temp dirs for cache isolation
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { getCached, setCached } from "../src/llm-cache.js";
|
||||
import { existsSync, unlinkSync, rmdirSync } from "fs";
|
||||
import { existsSync, unlinkSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
import { tmpdir } from "os";
|
||||
|
||||
const TEST_CACHE_DIR = join(homedir(), ".cache", "pi-project-map");
|
||||
const TEST_CACHE_DIR = join(tmpdir(), "pi-project-map-test-cache");
|
||||
const TEST_CACHE_FILE = join(TEST_CACHE_DIR, "llm-cache.json");
|
||||
|
||||
describe("llm-cache", () => {
|
||||
@@ -21,20 +21,20 @@ describe("llm-cache", () => {
|
||||
});
|
||||
|
||||
it("returns undefined for missing entries", () => {
|
||||
const result = getCached("nonexistent-hash");
|
||||
const result = getCached("nonexistent-hash", TEST_CACHE_DIR);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stores and retrieves cached results", () => {
|
||||
setCached("abc123", "PURPOSE: test\nDEPS: none\nCONCEPTS: none");
|
||||
const result = getCached("abc123");
|
||||
setCached("abc123", "PURPOSE: test\nDEPS: none\nCONCEPTS: none", TEST_CACHE_DIR);
|
||||
const result = getCached("abc123", TEST_CACHE_DIR);
|
||||
expect(result).toBe("PURPOSE: test\nDEPS: none\nCONCEPTS: none");
|
||||
});
|
||||
|
||||
it("overwrites existing entries", () => {
|
||||
setCached("abc123", "old");
|
||||
setCached("abc123", "new");
|
||||
const result = getCached("abc123");
|
||||
setCached("abc123", "old", TEST_CACHE_DIR);
|
||||
setCached("abc123", "new", TEST_CACHE_DIR);
|
||||
const result = getCached("abc123", TEST_CACHE_DIR);
|
||||
expect(result).toBe("new");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("llm-extract with mock client", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await extractFileLLM(file, mockClient);
|
||||
const result = await extractFileLLM(file, mockClient, tmpdir());
|
||||
expect(result.purpose).toBe("Test file");
|
||||
expect(result.deps).toEqual([]);
|
||||
expect(result.concepts).toContain("testing");
|
||||
|
||||
@@ -55,7 +55,7 @@ describe.skipIf(!hasKimiKey)("LLM integration with Kimi", () => {
|
||||
);
|
||||
|
||||
const client = createLLMClient("kimi", { model: kimiModel });
|
||||
const result = await extractFileLLM(file, client);
|
||||
const result = await extractFileLLM(file, client, dir);
|
||||
|
||||
expect(result.purpose).toBeTruthy();
|
||||
expect(result.purpose.length).toBeGreaterThan(5);
|
||||
@@ -90,12 +90,12 @@ describe.skipIf(!hasKimiKey)("LLM integration with Kimi", () => {
|
||||
writeFileSync(file, `export const version = "1.0.0";`);
|
||||
|
||||
const client = createLLMClient("kimi", { model: kimiModel });
|
||||
const result1 = await extractFileLLM(file, client);
|
||||
const result1 = await extractFileLLM(file, client, dir);
|
||||
expect(result1.purpose).toBeTruthy();
|
||||
|
||||
// Second call should hit cache — much faster
|
||||
const start = Date.now();
|
||||
const result2 = await extractFileLLM(file, client);
|
||||
const result2 = await extractFileLLM(file, client, dir);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(result2.purpose).toBe(result1.purpose);
|
||||
@@ -119,7 +119,7 @@ describe.skipIf(!hasKimiKey)("LLM integration with Kimi", () => {
|
||||
const start = Date.now();
|
||||
const results = await processFiles(
|
||||
files,
|
||||
async (f) => extractFileLLM(f, client),
|
||||
async (f) => extractFileLLM(f, client, dir),
|
||||
{ concurrency: 3, maxRetries: 1, retryDelaysMs: [2000] },
|
||||
);
|
||||
const elapsed = Date.now() - start;
|
||||
@@ -145,7 +145,7 @@ describe.skipIf(!hasKimiKey)("LLM integration with Kimi", () => {
|
||||
return originalComplete(...args);
|
||||
};
|
||||
|
||||
const result = await extractFileLLM(file, trackingClient);
|
||||
const result = await extractFileLLM(file, trackingClient, dir);
|
||||
expect(result.purpose).toBe("Large/generated file");
|
||||
expect(calls).toBe(0); // Should never call LLM for large files
|
||||
});
|
||||
|
||||
@@ -19,13 +19,23 @@ vi.mock("typebox", () => ({
|
||||
vi.mock("@mariozechner/pi-ai", () => ({
|
||||
complete: vi.fn(async (_model: any, _context: any) => ({
|
||||
role: "assistant" as const,
|
||||
content: [{ type: "text", text: "PURPOSE: Test file\nDEPS: none\nCONCEPTS: testing" }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "PURPOSE: Test file\nDEPS: none\nCONCEPTS: testing",
|
||||
},
|
||||
],
|
||||
})),
|
||||
}));
|
||||
|
||||
import extension from "../pi-extension.js";
|
||||
|
||||
const CACHE_FILE = join(homedir(), ".cache", "pi-project-map", "llm-cache.json");
|
||||
const CACHE_FILE = join(
|
||||
homedir(),
|
||||
".cache",
|
||||
"pi-project-map",
|
||||
"llm-cache.json",
|
||||
);
|
||||
function clearCache() {
|
||||
if (existsSync(CACHE_FILE)) unlinkSync(CACHE_FILE);
|
||||
}
|
||||
@@ -43,9 +53,16 @@ describe("pi-extension", () => {
|
||||
mockNotify = vi.fn();
|
||||
mockCtx = {
|
||||
cwd: "/home/project",
|
||||
model: { provider: "openai", id: "gpt-4o-mini", api: "openai-completions" },
|
||||
model: {
|
||||
provider: "openai",
|
||||
id: "gpt-4o-mini",
|
||||
api: "openai-completions",
|
||||
},
|
||||
modelRegistry: {
|
||||
getApiKeyAndHeaders: vi.fn(async () => ({ apiKey: "test-key", headers: {} })),
|
||||
getApiKeyAndHeaders: vi.fn(async () => ({
|
||||
apiKey: "test-key",
|
||||
headers: {},
|
||||
})),
|
||||
},
|
||||
ui: { notify: mockNotify },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user