Skip to content

Commit e1d9f1e

Browse files
committed
test(cache): add tests for cache invalidation and document schema-aware behavior
- Add test for garbage collection deleting expired files in cache-manager.test.ts - Add comprehensive tests for TTL-based fetch and schema-aware invalidation in doc-service.test.ts - Update README to document schema-aware invalidation for cached docs missing anchorIndex
1 parent d1cbe62 commit e1d9f1e

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ fetch_python_doc path="library/asyncio" offset=5000 limit=5000
7070
- **Doc cache**: 7 days TTL
7171
- **Location**: `~/.cache/opencode/python-docs/`
7272
- **Garbage collection**: Runs on startup and server reconnect
73+
- **Schema-aware invalidation**: Cached docs missing required fields (e.g., anchor index) are refetched even within TTL
7374

7475
## Development
7576

tests/cache-manager.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2-
import { existsSync, rmSync } from "node:fs";
2+
import { existsSync, rmSync, utimesSync } from "node:fs";
33
import { tmpdir } from "node:os";
44

55
import { CacheManager, type CacheManagerInterface } from "../src/testing";
@@ -89,5 +89,22 @@ describe("CacheManager", () => {
8989
expect(stats.deleted).toBe(0);
9090
expect(stats.errors).toBe(0);
9191
});
92+
93+
it("should delete expired files", () => {
94+
const testIndexPath = cache.getIndexPath("3.12");
95+
const testDocPath = cache.getDocPath("3.12", "test");
96+
cache.write(testIndexPath, { entries: [] });
97+
cache.write(testDocPath, { markdown: "# test", anchorIndex: [], fetchedAt: Date.now() });
98+
99+
// Set mtime to 2 days ago (longer than any TTL)
100+
const oldTime = Date.now() - 2 * 24 * 60 * 60 * 1000;
101+
utimesSync(testIndexPath, new Date(oldTime), new Date(oldTime));
102+
utimesSync(testDocPath, new Date(oldTime), new Date(oldTime));
103+
104+
const stats = cache.runGarbageCollection(1000, 1000); // 1 second TTL
105+
expect(stats.deleted).toBe(2);
106+
expect(existsSync(testIndexPath)).toBe(false);
107+
expect(existsSync(testDocPath)).toBe(false);
108+
});
92109
});
93110
});

tests/doc-service.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, expect, it, mock } from "bun:test";
2+
import type { CachedDoc, CacheManagerInterface, Logger } from "../src/testing";
3+
import { createDocService } from "../src/testing";
4+
5+
describe("DocService", () => {
6+
const mockCache: CacheManagerInterface = {
7+
getIndexPath: mock(() => "/mock/index.json"),
8+
getDocPath: mock(() => "/mock/doc.json"),
9+
isValid: mock(),
10+
read: mock(),
11+
write: mock(),
12+
runGarbageCollection: mock(() => ({ scanned: 0, deleted: 0, errors: 0 })),
13+
};
14+
15+
const mockLogger: Logger = {
16+
info: mock(),
17+
error: mock(),
18+
};
19+
20+
const docService = createDocService(mockCache, mockLogger);
21+
22+
describe("getIndex", () => {
23+
it("should return cached index if valid", async () => {
24+
const cachedIndex = { entries: [] };
25+
mockCache.isValid.mockReturnValue(true);
26+
mockCache.read.mockReturnValue(cachedIndex);
27+
28+
const result = await docService.getIndex("3.12");
29+
30+
expect(result).toEqual(cachedIndex);
31+
expect(mockCache.isValid).toHaveBeenCalledWith("/mock/index.json", expect.any(Number));
32+
expect(mockCache.read).toHaveBeenCalledWith("/mock/index.json");
33+
});
34+
35+
it("should fetch and cache index if invalid", async () => {
36+
const fetchedIndex = { entries: [{ name: "test", path: "test.html", type: "module" }] };
37+
mockCache.isValid.mockReturnValue(false);
38+
global.fetch = mock(() =>
39+
Promise.resolve({
40+
ok: true,
41+
text: () => Promise.resolve(JSON.stringify(fetchedIndex)),
42+
} as Response),
43+
);
44+
45+
const result = await docService.getIndex("3.12");
46+
47+
expect(result).toEqual(fetchedIndex);
48+
expect(mockCache.write).toHaveBeenCalledWith("/mock/index.json", fetchedIndex);
49+
});
50+
});
51+
52+
describe("getDoc", () => {
53+
it("should return cached doc if valid and has anchorIndex", async () => {
54+
const cachedDoc: CachedDoc = {
55+
markdown: "# test",
56+
anchorIndex: [],
57+
fetchedAt: Date.now(),
58+
};
59+
mockCache.isValid.mockReturnValue(true);
60+
mockCache.read.mockReturnValue(cachedDoc);
61+
62+
const result = await docService.getDoc("3.12", "test");
63+
64+
expect(result).toEqual({ ...cachedDoc, fromCache: true, path: "test" });
65+
expect(mockCache.isValid).toHaveBeenCalledWith("/mock/doc.json", expect.any(Number));
66+
expect(mockCache.read).toHaveBeenCalledWith("/mock/doc.json");
67+
});
68+
69+
it("should refetch if cache is invalid", async () => {
70+
mockCache.isValid.mockReturnValue(false);
71+
global.fetch = mock(() =>
72+
Promise.resolve({
73+
ok: true,
74+
text: () => Promise.resolve("<html><body>test</body></html>"),
75+
} as Response),
76+
);
77+
78+
const result = await docService.getDoc("3.12", "test");
79+
80+
expect(result.fromCache).toBe(false);
81+
expect(result.path).toBe("test");
82+
expect(result.markdown).toBeDefined();
83+
expect(result.anchorIndex).toBeDefined();
84+
expect(mockCache.write).toHaveBeenCalled();
85+
});
86+
87+
it("should refetch if cached doc lacks anchorIndex", async () => {
88+
const cachedDocWithoutAnchorIndex = {
89+
markdown: "# test",
90+
fetchedAt: Date.now(),
91+
// no anchorIndex
92+
};
93+
mockCache.isValid.mockReturnValue(true);
94+
mockCache.read.mockReturnValue(cachedDocWithoutAnchorIndex);
95+
global.fetch = mock(() =>
96+
Promise.resolve({
97+
ok: true,
98+
text: () => Promise.resolve("<html><body>test</body></html>"),
99+
} as Response),
100+
);
101+
102+
const result = await docService.getDoc("3.12", "test");
103+
104+
expect(result.fromCache).toBe(false);
105+
expect(result.path).toBe("test");
106+
expect(result.markdown).toBeDefined();
107+
expect(result.anchorIndex).toBeDefined();
108+
expect(mockCache.write).toHaveBeenCalled();
109+
});
110+
});
111+
});

0 commit comments

Comments
 (0)