// #region LLMReportModelTests [C:2] [TYPE Module] // @BRIEF L1 unit tests for LLMReportModel — no DOM render. // @RELATION BINDS_TO -> [LLMReportModel] import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("$lib/api.js", () => ({ api: { fetchApi: vi.fn(), requestApi: vi.fn().mockResolvedValue({}), getTask: vi.fn().mockResolvedValue({ id: "t1", status: "completed" }), getTaskLogs: vi.fn().mockResolvedValue([]), getStorageFileBlob: vi.fn(), }, })); vi.mock("$lib/cot-logger", () => ({ log: vi.fn() })); import { LLMReportModel } from "../LLMReportModel.svelte.ts"; import { api } from "$lib/api.js"; // #region LLMReportModel.InvariantTests [C:2] [TYPE Function] // @BRIEF L1 invariants: initial state, loadReport transitions, error path. describe("LLMReportModel — L1 invariants", () => { let model: LLMReportModel; beforeEach(() => { vi.clearAllMocks(); model = new LLMReportModel(); }); // #region LLMReportModel.InitialState [C:2] [TYPE Test] it("starts in idle state", () => { expect(model.screenState).toBe("idle"); expect(model.task).toBeNull(); expect(model.logs).toEqual([]); expect(model.error).toBeNull(); }); // #endregion // #region LLMReportModel.LoadReport [C:2] [TYPE Test] it("loadReport transitions to loaded on success", async () => { vi.mocked(api.getTask).mockResolvedValue({ id: "t1", status: "completed" } as any); vi.mocked(api.getTaskLogs).mockResolvedValue([]); model.taskId = "t1"; await model.loadReport(); expect(model.screenState).toBe("loaded"); expect(model.task?.id).toBe("t1"); }); it("loadReport returns early when no taskId", async () => { await model.loadReport(); expect(api.getTask).not.toHaveBeenCalled(); expect(model.screenState).toBe("idle"); }); it("loadReport transitions to error on failure", async () => { vi.mocked(api.getTask).mockRejectedValue(new Error("Task not found")); model.taskId = "t1"; await model.loadReport(); expect(model.screenState).toBe("error"); expect(model.error).toBe("Task not found"); }); it("loadReport stores logs array", async () => { vi.mocked(api.getTask).mockResolvedValue({ id: "t1" } as any); vi.mocked(api.getTaskLogs).mockResolvedValue([{ level: "info", message: "started" }]); model.taskId = "t1"; await model.loadReport(); expect(model.logs).toHaveLength(1); }); it("loadReport normalizes non-array logs to empty array", async () => { vi.mocked(api.getTask).mockResolvedValue({ id: "t1" } as any); vi.mocked(api.getTaskLogs).mockResolvedValue(null); model.taskId = "t1"; await model.loadReport(); expect(model.logs).toEqual([]); }); it("refresh() delegates to loadReport", () => { const spy = vi.spyOn(model, "loadReport").mockResolvedValue(); model.refresh(); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); // #endregion // #region LLMReportModel.Screenshots [C:2] [TYPE Test] it("loadScreenshotBlobs does nothing with empty paths", async () => { await model.loadScreenshotBlobs([]); expect(api.getStorageFileBlob).not.toHaveBeenCalled(); }); it("loadScreenshotBlobs fetches blobs and creates URLs", async () => { const blob = new Blob(["fake"]); vi.mocked(api.getStorageFileBlob).mockResolvedValue(blob); const createSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:url1"); await model.loadScreenshotBlobs(["path/to/screen1.png"]); expect(api.getStorageFileBlob).toHaveBeenCalledWith("path/to/screen1.png"); expect(model.screenshotBlobUrls["path/to/screen1.png"]).toBe("blob:url1"); createSpy.mockRestore(); }); it("loadScreenshotBlobs handles blob fetch errors", async () => { vi.mocked(api.getStorageFileBlob).mockRejectedValue(new Error("Not found")); await model.loadScreenshotBlobs(["bad-path.png"]); expect(model.screenshotBlobUrls).toEqual({}); expect(model.screenshotLoadErrors["bad-path.png"]).toBe("Not found"); }); it("loadScreenshotBlobs respects stale token", async () => { // Simulate a stale token by calling loadScreenshotBlobs while delaying const blob = new Blob(["fake"]); const revokeSpy = vi.spyOn(URL, "revokeObjectURL").mockReturnValue(); const createSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:stale"); // Make blob fetch return a promise that doesn't resolve until we increment the token let resolveBlob: (b: Blob) => void; vi.mocked(api.getStorageFileBlob).mockReturnValue(new Promise((r) => { resolveBlob = r; }) as any); const promise = model.loadScreenshotBlobs(["stale-path.png"]); // Now increment the token to make the in-flight call stale model["_screenshotLoadToken"]++; // Resolve the blob fetch resolveBlob!(blob); await promise; // Since token changed, blobs should be revoked expect(revokeSpy).toHaveBeenCalled(); revokeSpy.mockRestore(); createSpy.mockRestore(); }); // #endregion // #region LLMReportModel.OpenScreenshot [C:2] [TYPE Test] it("openScreenshot opens window with blob url", () => { const openSpy = vi.spyOn(window, "open").mockReturnValue(null); model.screenshotBlobUrls["path.png"] = "blob:url"; model.openScreenshot("path.png"); expect(openSpy).toHaveBeenCalledWith("blob:url", "_blank", "noopener,noreferrer"); openSpy.mockRestore(); }); it("openScreenshot does nothing when url missing", () => { const openSpy = vi.spyOn(window, "open").mockReturnValue(null); model.openScreenshot("missing.png"); expect(openSpy).not.toHaveBeenCalled(); openSpy.mockRestore(); }); // #endregion // #region LLMReportModel.Cleanup [C:2] [TYPE Test] it("cleanupBlobs revokes all URLs", () => { model.screenshotBlobUrls = { a: "blob:a", b: "blob:b" }; const revokeSpy = vi.spyOn(URL, "revokeObjectURL").mockReturnValue(); model.cleanupBlobs(); expect(revokeSpy).toHaveBeenCalledTimes(2); expect(model.screenshotBlobUrls).toEqual({}); revokeSpy.mockRestore(); }); // #endregion }); // #endregion // #region LLMReportModel.DerivedState [C:2] [TYPE Function] // @BRIEF Derived state correctness: result, checkResult, timings, screenshotPaths, sentLogs. describe("LLMReportModel — Derived State", () => { let model: LLMReportModel; beforeEach(() => { vi.clearAllMocks(); model = new LLMReportModel(); }); it("result defaults to empty object", () => { expect(model.result).toEqual({}); }); it("result mirrors task.result", () => { model.task = { id: "t1", result: { status: "PASS", summary: "All good" } as any }; expect(model.result.status).toBe("PASS"); }); it("checkResult returns FAIL for FAIL status", () => { model.task = { id: "t1", result: { status: "FAIL" } as any }; expect(model.checkResult.level).toBe("fail"); }); it("checkResult returns WARN for WARN status", () => { model.task = { id: "t1", result: { status: "WARN" } as any }; expect(model.checkResult.level).toBe("warn"); }); it("checkResult returns PASS for PASS status", () => { model.task = { id: "t1", result: { status: "PASS" } as any }; expect(model.checkResult.level).toBe("pass"); }); it("checkResult returns UNKNOWN for missing status", () => { model.task = { id: "t1", result: {} as any }; expect(model.checkResult.level).toBe("unknown"); }); it("timings returns empty object when no timings", () => { expect(model.timings).toEqual({}); }); it("timings returns validation_duration_ms", () => { model.task = { id: "t1", result: { timings: { validation_duration_ms: 1500 } } as any }; expect(model.timings.validation_duration_ms).toBe(1500); }); it("screenshotPaths uses screenshot_paths array", () => { model.task = { id: "t1", result: { screenshot_paths: ["a.png", "b.png"] } as any }; expect(model.screenshotPaths).toEqual(["a.png", "b.png"]); }); it("screenshotPaths falls back to single screenshot_path", () => { model.task = { id: "t1", result: { screenshot_path: "single.png" } as any }; expect(model.screenshotPaths).toEqual(["single.png"]); }); it("screenshotPaths returns empty when none present", () => { expect(model.screenshotPaths).toEqual([]); }); it("sentLogs returns logs_sent_to_llm array", () => { model.task = { id: "t1", result: { logs_sent_to_llm: ["log1", "log2"] } as any }; expect(model.sentLogs).toEqual(["log1", "log2"]); }); it("sentLogs returns empty array when missing", () => { expect(model.sentLogs).toEqual([]); }); }); // #endregion // #region LLMReportModel.StaticUtils [C:2] [TYPE Function] // @BRIEF Static utility method tests. describe("LLMReportModel — Static Utils", () => { // #region LLMReportModel.FormatDate [C:2] [TYPE Test] describe("formatDate", () => { it("returns '-' for null", () => { expect(LLMReportModel.formatDate(null)).toBe("-"); }); it("returns '-' for invalid date", () => { expect(LLMReportModel.formatDate("not-a-date")).toBe("-"); }); it("formats valid date string", () => { const result = LLMReportModel.formatDate("2026-06-01T12:00:00Z"); expect(result).toContain("2026"); }); }); // #endregion // #region LLMReportModel.FormatMs [C:2] [TYPE Test] describe("formatMs", () => { it("returns '-' for NaN", () => { expect(LLMReportModel.formatMs("abc")).toBe("-"); }); it("returns '-' for negative", () => { expect(LLMReportModel.formatMs(-100)).toBe("-"); }); it("formats ms when < 1000", () => { expect(LLMReportModel.formatMs(500)).toBe("500 ms"); }); it("formats seconds when >= 1000", () => { expect(LLMReportModel.formatMs(2500)).toBe("2.50 s"); }); }); // #endregion // #region LLMReportModel.GetCheckResultClasses [C:2] [TYPE Test] describe("getCheckResultClasses", () => { it("returns fail classes", () => { const cls = LLMReportModel.getCheckResultClasses("fail"); expect(cls).toContain("destructive"); }); it("returns warn classes", () => { const cls = LLMReportModel.getCheckResultClasses("warn"); expect(cls).toContain("warning"); }); it("returns pass classes", () => { const cls = LLMReportModel.getCheckResultClasses("pass"); expect(cls).toContain("success"); }); it("returns muted for unknown level", () => { const cls = LLMReportModel.getCheckResultClasses("unknown" as any); expect(cls).toContain("muted"); }); }); // #endregion }); // #endregion