138 lines
5.4 KiB
TypeScript
138 lines
5.4 KiB
TypeScript
/**
|
|
* Coverage for the document conversion cache layered over the markit wrappers
|
|
* (src/utils/markit + src/utils/markit-cache). Successful conversions are cached
|
|
* by content hash + normalized extension so repeated reads of unchanged bytes
|
|
* reuse converted markdown; failed, empty, and imageDir conversions are never
|
|
* cached. The underlying converter (`Markit.prototype.convert`) is mocked so the
|
|
* tests assert cache hit/miss/skipped behavior and converter call counts.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { Markit } from "@oh-my-pi/pi-coding-agent/markit";
|
|
import { convertBufferWithMarkit, convertFileWithMarkit } from "@oh-my-pi/pi-coding-agent/utils/markit";
|
|
import { pruneMarkitConversionCache } from "@oh-my-pi/pi-coding-agent/utils/markit-cache";
|
|
import { __resetDirsFromEnvForTests, getAgentDir, Snowflake, setAgentDir } from "@oh-my-pi/pi-utils";
|
|
|
|
function restoreEnv(key: string, value: string | undefined): void {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
describe("document conversion cache", () => {
|
|
let testDir: string;
|
|
let originalPiCodingAgentDir: string | undefined;
|
|
let originalOmpProfile: string | undefined;
|
|
let originalPiProfile: string | undefined;
|
|
let originalXdgCacheHome: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
originalPiCodingAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
originalOmpProfile = process.env.OMP_PROFILE;
|
|
originalPiProfile = process.env.PI_PROFILE;
|
|
originalXdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
testDir = path.join(os.tmpdir(), `markit-cache-${Snowflake.next()}`);
|
|
await fs.mkdir(testDir, { recursive: true });
|
|
setAgentDir(path.join(testDir, "agent"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
restoreEnv("PI_CODING_AGENT_DIR", originalPiCodingAgentDir);
|
|
restoreEnv("OMP_PROFILE", originalOmpProfile);
|
|
restoreEnv("PI_PROFILE", originalPiProfile);
|
|
restoreEnv("XDG_CACHE_HOME", originalXdgCacheHome);
|
|
__resetDirsFromEnvForTests();
|
|
await fs.rm(testDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("caches successful buffer conversions by content hash and normalized extension", async () => {
|
|
const convert = vi.spyOn(Markit.prototype, "convert").mockResolvedValue({ markdown: "cached body" });
|
|
const bytes = new TextEncoder().encode("hello pdf bytes");
|
|
|
|
const first = await convertBufferWithMarkit(bytes, "pdf");
|
|
expect(first).toEqual({ ok: true, content: "cached body", cache: "miss" });
|
|
|
|
const second = await convertBufferWithMarkit(bytes, ".pdf");
|
|
expect(second).toEqual({ ok: true, content: "cached body", cache: "hit" });
|
|
|
|
expect(convert).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not cache failed conversions", async () => {
|
|
const convert = vi.spyOn(Markit.prototype, "convert");
|
|
convert.mockRejectedValueOnce(new Error("boom"));
|
|
const bytes = new TextEncoder().encode("retry me");
|
|
|
|
const first = await convertBufferWithMarkit(bytes, ".pdf");
|
|
expect(first.ok).toBe(false);
|
|
|
|
convert.mockResolvedValueOnce({ markdown: "recovered" });
|
|
const second = await convertBufferWithMarkit(bytes, ".pdf");
|
|
expect(second.ok).toBe(true);
|
|
expect(second.content).toBe("recovered");
|
|
expect(second.cache).toBe("miss");
|
|
|
|
expect(convert).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("invalidates file conversions by content hash", async () => {
|
|
const convert = vi.spyOn(Markit.prototype, "convert");
|
|
const docPath = path.join(testDir, "doc.pdf");
|
|
|
|
await fs.writeFile(docPath, new TextEncoder().encode("v1"));
|
|
convert.mockResolvedValueOnce({ markdown: "first" });
|
|
const v1 = await convertFileWithMarkit(docPath);
|
|
expect(v1.cache).toBe("miss");
|
|
expect(v1.content).toBe("first");
|
|
|
|
await fs.writeFile(docPath, new TextEncoder().encode("v2"));
|
|
convert.mockResolvedValueOnce({ markdown: "second" });
|
|
const v2 = await convertFileWithMarkit(docPath);
|
|
expect(v2.cache).toBe("miss");
|
|
expect(v2.content).toBe("second");
|
|
|
|
const v2Again = await convertFileWithMarkit(docPath);
|
|
expect(v2Again.cache).toBe("hit");
|
|
expect(v2Again.content).toBe("second");
|
|
|
|
expect(convert).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("skips cache for imageDir conversions", async () => {
|
|
const convert = vi.spyOn(Markit.prototype, "convert").mockResolvedValue({ markdown: "image body" });
|
|
const docPath = path.join(testDir, "image-doc.pdf");
|
|
await fs.writeFile(docPath, new TextEncoder().encode("image bytes"));
|
|
const imageDir = path.join(testDir, "images");
|
|
|
|
const first = await convertFileWithMarkit(docPath, undefined, { imageDir });
|
|
expect(first.cache).toBe("skipped");
|
|
|
|
const second = await convertFileWithMarkit(docPath, undefined, { imageDir });
|
|
expect(second.cache).toBe("skipped");
|
|
|
|
expect(convert).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("sweeps orphaned .tmp files during prune", async () => {
|
|
const cacheDir = path.join(getAgentDir(), "cache", "document-conversions");
|
|
await fs.mkdir(cacheDir, { recursive: true });
|
|
|
|
const stalePath = path.join(cacheDir, "orphan.123.456.tmp");
|
|
const freshPath = path.join(cacheDir, "active.789.012.tmp");
|
|
await fs.writeFile(stalePath, "stale");
|
|
await fs.writeFile(freshPath, "fresh");
|
|
const old = new Date(Date.now() - 60 * 60 * 1000);
|
|
await fs.utimes(stalePath, old, old);
|
|
|
|
await pruneMarkitConversionCache(cacheDir);
|
|
|
|
expect(await fs.exists(stalePath)).toBe(false);
|
|
expect(await fs.exists(freshPath)).toBe(true);
|
|
});
|
|
});
|