feat(confidence-engine): use server investigation persistence
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const save = vi.fn();
|
||||
const load = vi.fn();
|
||||
const list = vi.fn();
|
||||
const restart = vi.fn();
|
||||
|
||||
vi.mock("@/lib/storage/providers/server-http.js", () => ({
|
||||
saveInvestigation: (...args) => save(...args),
|
||||
loadInvestigation: (...args) => load(...args),
|
||||
listInvestigations: (...args) => list(...args),
|
||||
restartInvestigation: (...args) => restart(...args),
|
||||
}));
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((next) => { resolve = next; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("server-authoritative investigation storage seam", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
save.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("uses only the server provider for async load and list", async () => {
|
||||
const storage = await import("@/lib/storage/investigation-storage.js");
|
||||
load.mockResolvedValue({ id: "inv-1" });
|
||||
list.mockResolvedValue([{ id: "inv-1" }]);
|
||||
|
||||
await expect(storage.loadInvestigation("inv-1")).resolves.toEqual({ id: "inv-1" });
|
||||
await expect(storage.listInvestigations()).resolves.toEqual([{ id: "inv-1" }]);
|
||||
expect(load).toHaveBeenCalledWith("inv-1");
|
||||
expect(list).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows only one in-flight save per investigation and coalesces rapid pending saves to the latest snapshot", async () => {
|
||||
const storage = await import("@/lib/storage/investigation-storage.js");
|
||||
const first = deferred();
|
||||
const second = deferred();
|
||||
save.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
|
||||
|
||||
const one = storage.saveInvestigation({ id: "inv-1", revision: 1 });
|
||||
const two = storage.saveInvestigation({ id: "inv-1", revision: 2 });
|
||||
const three = storage.saveInvestigation({ id: "inv-1", revision: 3 });
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
expect(save).toHaveBeenCalledWith({ id: "inv-1", revision: 1 }, "inv-1");
|
||||
|
||||
first.resolve({ id: "inv-1", revision: 1 });
|
||||
await Promise.resolve();
|
||||
expect(save).toHaveBeenCalledTimes(2);
|
||||
expect(save).toHaveBeenLastCalledWith({ id: "inv-1", revision: 3 }, "inv-1");
|
||||
second.resolve({ id: "inv-1", revision: 3 });
|
||||
|
||||
await expect(Promise.all([one, two, three])).resolves.toEqual([
|
||||
{ id: "inv-1", revision: 1 },
|
||||
{ id: "inv-1", revision: 3 },
|
||||
{ id: "inv-1", revision: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not permit an older request to complete after a newer request becomes durable", async () => {
|
||||
const storage = await import("@/lib/storage/investigation-storage.js");
|
||||
const first = deferred();
|
||||
const second = deferred();
|
||||
save.mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise);
|
||||
|
||||
const older = storage.saveInvestigation({ id: "inv-2", revision: 2 });
|
||||
const newer = storage.saveInvestigation({ id: "inv-2", revision: 3 });
|
||||
expect(save).toHaveBeenCalledTimes(1);
|
||||
first.resolve({ id: "inv-2", revision: 2 });
|
||||
await Promise.resolve();
|
||||
expect(save).toHaveBeenLastCalledWith({ id: "inv-2", revision: 3 }, "inv-2");
|
||||
second.resolve({ id: "inv-2", revision: 3 });
|
||||
|
||||
await Promise.all([older, newer]);
|
||||
expect(save).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as provider from "@/lib/storage/providers/server-http.js";
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("server HTTP investigation provider", () => {
|
||||
it("maps save, load, list, and restart to authenticated investigation API paths", async () => {
|
||||
const fetch = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ snapshot: { id: "inv/a" } }) })
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ snapshot: { id: "inv/a" } }) })
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ investigations: [] }) })
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ snapshot: { id: "inv/a" } }) });
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
|
||||
await expect(provider.saveInvestigation({ id: "inv/a" }, "inv/a")).resolves.toEqual({ id: "inv/a" });
|
||||
await expect(provider.loadInvestigation("inv/a")).resolves.toEqual({ id: "inv/a" });
|
||||
await expect(provider.listInvestigations()).resolves.toEqual([]);
|
||||
await expect(provider.restartInvestigation("inv/a")).resolves.toEqual({ id: "inv/a" });
|
||||
|
||||
expect(fetch).toHaveBeenNthCalledWith(1, "/api/investigations", expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id: "inv/a", snapshot: { id: "inv/a" } }),
|
||||
}));
|
||||
expect(fetch).toHaveBeenNthCalledWith(2, "/api/investigations/inv%2Fa");
|
||||
expect(fetch).toHaveBeenNthCalledWith(3, "/api/investigations", undefined);
|
||||
expect(fetch).toHaveBeenNthCalledWith(4, "/api/investigations/inv%2Fa/restart", { method: "POST" });
|
||||
});
|
||||
|
||||
it("returns null for a missing investigation (HTTP 404) rather than throwing", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 404, json: async () => ({ error: "Investigation not found" }) }));
|
||||
await expect(provider.loadInvestigation("inv/missing")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces API failures rather than returning an empty result", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 500, json: async () => ({ error: "Unauthorized" }) }));
|
||||
await expect(provider.listInvestigations()).rejects.toThrow("Unauthorized");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user