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