feat(hooks): add hook-run-registry store helpers
This commit is contained in:
parent
fa5e8481ab
commit
6bf57b4e9a
107
src/gateway/hook-run-registry.store.test.ts
Normal file
107
src/gateway/hook-run-registry.store.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
58
src/gateway/hook-run-registry.store.ts
Normal file
58
src/gateway/hook-run-registry.store.ts
Normal 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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user