feat(hooks): add hook-run-registry store helpers

This commit is contained in:
Trevin Chow 2026-01-29 09:16:35 -08:00 committed by Trevin Chow
parent fa5e8481ab
commit 6bf57b4e9a
2 changed files with 165 additions and 0 deletions

View File

@ -0,0 +1,107 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../config/paths.js", () => ({
STATE_DIR: "/mock/state",
}));
vi.mock("../infra/json-file.js", () => ({
loadJsonFile: vi.fn(),
saveJsonFile: vi.fn(),
}));
describe("hook-run-registry.store", () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.resetAllMocks();
});
describe("loadHookRunRegistryFromDisk", () => {
it("returns empty map when file does not exist", async () => {
const { loadJsonFile } = await import("../infra/json-file.js");
vi.mocked(loadJsonFile).mockReturnValue(null);
const { loadHookRunRegistryFromDisk } = await import("./hook-run-registry.store.js");
const result = loadHookRunRegistryFromDisk();
expect(result.size).toBe(0);
});
it("loads and parses existing registry file", async () => {
const { loadJsonFile } = await import("../infra/json-file.js");
vi.mocked(loadJsonFile).mockReturnValue({
version: 1,
runs: {
"run-1": {
runId: "run-1",
sessionKey: "hook:test:1",
jobName: "test",
cleanup: "delete",
cleanupDelayMinutes: 0,
createdAt: 1000,
},
},
});
const { loadHookRunRegistryFromDisk } = await import("./hook-run-registry.store.js");
const result = loadHookRunRegistryFromDisk();
expect(result.size).toBe(1);
expect(result.get("run-1")?.sessionKey).toBe("hook:test:1");
});
it("returns empty map for invalid version", async () => {
const { loadJsonFile } = await import("../infra/json-file.js");
vi.mocked(loadJsonFile).mockReturnValue({
version: 999,
runs: { "run-1": { runId: "run-1" } },
});
const { loadHookRunRegistryFromDisk } = await import("./hook-run-registry.store.js");
const result = loadHookRunRegistryFromDisk();
expect(result.size).toBe(0);
});
});
describe("saveHookRunRegistryToDisk", () => {
it("writes versioned registry to disk", async () => {
const { saveJsonFile } = await import("../infra/json-file.js");
const mockSave = vi.mocked(saveJsonFile);
const { saveHookRunRegistryToDisk } = await import("./hook-run-registry.store.js");
const registry = new Map([
[
"run-1",
{
runId: "run-1",
sessionKey: "hook:test:1",
jobName: "test",
cleanup: "delete" as const,
cleanupDelayMinutes: 0,
createdAt: Date.now(),
},
],
]);
saveHookRunRegistryToDisk(registry);
expect(mockSave).toHaveBeenCalledWith(
expect.stringContaining("hook-runs.json"),
expect.objectContaining({ version: 1 }),
);
});
});
describe("resolveHookRunRegistryPath", () => {
it("returns path under STATE_DIR", async () => {
const { resolveHookRunRegistryPath } = await import("./hook-run-registry.store.js");
const result = resolveHookRunRegistryPath();
expect(result).toContain("/mock/state");
expect(result).toContain("hook-runs.json");
});
});
});

View File

@ -0,0 +1,58 @@
import path from "node:path";
import { STATE_DIR } from "../config/paths.js";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
export type HookRunRecord = {
runId: string;
sessionKey: string;
jobName: string;
cleanup: "delete" | "keep";
cleanupDelayMinutes: number;
createdAt: number;
endedAt?: number;
cleanupAtMs?: number;
cleanupHandled?: boolean;
};
type PersistedHookRunRegistry = {
version: 1;
runs: Record<string, HookRunRecord>;
};
const REGISTRY_VERSION = 1 as const;
export function resolveHookRunRegistryPath(): string {
return path.join(STATE_DIR, "hooks", "hook-runs.json");
}
export function loadHookRunRegistryFromDisk(): Map<string, HookRunRecord> {
const pathname = resolveHookRunRegistryPath();
const raw = loadJsonFile(pathname);
if (!raw || typeof raw !== "object") return new Map();
const record = raw as Partial<PersistedHookRunRegistry>;
if (record.version !== 1) return new Map();
const runsRaw = record.runs;
if (!runsRaw || typeof runsRaw !== "object") return new Map();
const out = new Map<string, HookRunRecord>();
for (const [runId, entry] of Object.entries(runsRaw)) {
if (!entry || typeof entry !== "object") continue;
const typed = entry as HookRunRecord;
if (!typed.runId || typeof typed.runId !== "string") continue;
out.set(runId, typed);
}
return out;
}
export function saveHookRunRegistryToDisk(runs: Map<string, HookRunRecord>): void {
const pathname = resolveHookRunRegistryPath();
const serialized: Record<string, HookRunRecord> = {};
for (const [runId, entry] of runs.entries()) {
serialized[runId] = entry;
}
const out: PersistedHookRunRegistry = {
version: REGISTRY_VERSION,
runs: serialized,
};
saveJsonFile(pathname, out);
}