import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ModelDownloadError } from "@/lib/api/model-download"; import { isModelDownloadable, useModelStore, type ModelSummary } from "./model-store"; // mock 下載 API client(store 的 downloadModel action 依賴它) vi.mock("@/lib/api/model-download", async () => { const actual = await vi.importActual( "@/lib/api/model-download", ); return { ...actual, getModelDownload: vi.fn(), downloadModelFile: vi.fn(), }; }); import { downloadModelFile, getModelDownload } from "@/lib/api/model-download"; const mockGetModelDownload = vi.mocked(getModelDownload); const mockDownloadModelFile = vi.mocked(downloadModelFile); function reset() { useModelStore.setState({ models: [], selectedModel: null, isLoading: false, downloadingId: null, }); } const convertedReady: ModelSummary = { id: "m1", name: "YOLOv5s", targetChip: "kl520", fileSize: 1024, source: "converted", status: "ready", createdAt: "2026-01-01T00:00:00Z", }; describe("model-store(F5 骨架)", () => { beforeEach(reset); it("初始為空 list", () => { const s = useModelStore.getState(); expect(s.models).toEqual([]); expect(s.selectedModel).toBeNull(); expect(s.isLoading).toBe(false); }); it("_setModels 能塞入 list(雛形 / 測試用)", () => { useModelStore.getState()._setModels([ { id: "m1", name: "YOLOv5s", targetChip: "kl520", fileSize: 1024, source: "uploaded", status: "ready", createdAt: "2026-01-01T00:00:00Z", }, ]); expect(useModelStore.getState().models).toHaveLength(1); expect(useModelStore.getState().models[0].name).toBe("YOLOv5s"); }); it("fetchModels 雛形實作不拋錯(為 F6 預留 stub)", async () => { await expect(useModelStore.getState().fetchModels()).resolves.toBeUndefined(); }); }); describe("isModelDownloadable(Phase 0.9)", () => { it("converted + ready → 可下載", () => { expect(isModelDownloadable({ source: "converted", status: "ready" })).toBe(true); }); it("uploaded(即使 ready)→ 不可下載", () => { expect(isModelDownloadable({ source: "uploaded", status: "ready" })).toBe(false); }); it("converted 但非 ready(scanning)→ 不可下載", () => { expect(isModelDownloadable({ source: "converted", status: "scanning" })).toBe(false); }); it("preset → 不可下載", () => { expect(isModelDownloadable({ source: "preset", status: "ready" })).toBe(false); }); }); describe("downloadModel action(Phase 0.9)", () => { beforeEach(() => { reset(); vi.clearAllMocks(); }); afterEach(() => vi.restoreAllMocks()); it("happy path:取授權 → 下載 → 回 { ok: true },過程中 downloadingId = 該 model,結束清空", async () => { mockGetModelDownload.mockResolvedValue({ downloadUrl: "https://faa/files/u/m1.nef", token: "fdt_tok", expiresAt: "2026-06-07T12:00:00Z", }); mockDownloadModelFile.mockResolvedValue(undefined); const promise = useModelStore.getState().downloadModel(convertedReady); // action 同步階段已設 downloadingId expect(useModelStore.getState().downloadingId).toBe("m1"); const result = await promise; expect(result).toEqual({ ok: true }); expect(mockGetModelDownload).toHaveBeenCalledWith("m1"); // 檔名從 URL path 推導(m1.nef) expect(mockDownloadModelFile).toHaveBeenCalledWith( "https://faa/files/u/m1.nef", "fdt_tok", "m1.nef", ); // 結束後清空 loading expect(useModelStore.getState().downloadingId).toBeNull(); }); it("不可下載的 model(uploaded)→ 不打 API,回 upload_not_supported", async () => { const result = await useModelStore.getState().downloadModel({ ...convertedReady, source: "uploaded", }); expect(result).toEqual({ ok: false, code: "upload_not_supported", message: expect.any(String), }); expect(mockGetModelDownload).not.toHaveBeenCalled(); }); it("已有下載進行中 → 回 busy,不重複打 API", async () => { useModelStore.setState({ downloadingId: "other" }); const result = await useModelStore.getState().downloadModel(convertedReady); expect(result).toMatchObject({ ok: false, code: "busy" }); expect(mockGetModelDownload).not.toHaveBeenCalled(); }); it("getModelDownload 拋 ModelDownloadError → 回對應 code,並清空 downloadingId", async () => { mockGetModelDownload.mockRejectedValue( new ModelDownloadError(403, "forbidden", "no"), ); const result = await useModelStore.getState().downloadModel(convertedReady); expect(result).toMatchObject({ ok: false, code: "forbidden" }); expect(useModelStore.getState().downloadingId).toBeNull(); }); it("downloadModelFile 拋 network_error(CORS)→ 回 network_error", async () => { mockGetModelDownload.mockResolvedValue({ downloadUrl: "https://faa/files/u/m1.nef", token: "fdt_tok", expiresAt: "2026-06-07T12:00:00Z", }); mockDownloadModelFile.mockRejectedValue( new ModelDownloadError(0, "network_error", "Failed to fetch"), ); const result = await useModelStore.getState().downloadModel(convertedReady); expect(result).toMatchObject({ ok: false, code: "network_error" }); expect(useModelStore.getState().downloadingId).toBeNull(); }); it("非 ModelDownloadError 的例外 → 退化成 unknown", async () => { mockGetModelDownload.mockRejectedValue(new Error("weird")); const result = await useModelStore.getState().downloadModel(convertedReady); expect(result).toMatchObject({ ok: false, code: "unknown" }); }); });