import { beforeEach, expect, it, vi } from "vitest"; import type { ImportJob } from "@/lib/furni/import-job"; const mocks = vi.hoisted(() => ({ set: vi.fn(), eval: vi.fn(), list: vi.fn(), save: vi.fn(), import: vi.fn(), attachment: vi.fn(), export: vi.fn(), translate: vi.fn(), })); vi.mock("@/lib/redis", () => ({ redis: { set: mocks.set, eval: mocks.eval } })); vi.mock("@/lib/server-log", () => ({ logServerError: vi.fn() })); vi.mock("./audit", () => ({ logAudit: vi.fn() })); vi.mock("./catalog-git-queue", () => ({ withCatalogExport: mocks.export })); vi.mock("./clone-sources", () => ({ getSource: async () => null })); vi.mock("./furni-job-store", () => ({ ImportJobStore: class { list = mocks.list; save = mocks.save; }, })); vi.mock("./furni-import", () => ({ ensureDirectories: async () => {}, importSingleFurni: mocks.import, })); vi.mock("./furni-data-i18n", () => ({ patchLocalizedFurniDataEntries: mocks.translate, })); vi.mock("./furni-attachment", () => ({ readFurnitureAttachment: mocks.attachment, })); vi.mock("./rcon", () => ({ rcon: { updateCatalog: async () => {}, updateItems: async () => {} }, })); vi.mock("./catalog-git-export", () => ({ runCatalogExport: vi.fn() })); import { drainFurnitureImports } from "./furni-job-worker"; let job: ImportJob; beforeEach(() => { vi.clearAllMocks(); mocks.set.mockResolvedValue("OK"); mocks.eval.mockResolvedValue(1); mocks.export.mockImplementation((fn) => fn()); mocks.import.mockResolvedValue({ ok: true, itemId: 900, warnings: [] }); mocks.save.mockResolvedValue(undefined); job = { id: "job", userId: 5, createdAt: "now", updatedAt: "now", state: "queued", translate: false, items: [ { id: 1, classname: "chair", name: "Chair", description: "", type: "flooritem", revision: 1, category: "other", state: "pending", }, ], }; mocks.list.mockResolvedValue([job]); }); it("imports with repair enabled and saves the allocated local ID", async () => { await drainFurnitureImports(); expect(mocks.import).toHaveBeenCalledWith( expect.objectContaining({ classname: "chair", repairExisting: true }), ); expect(job.state).toBe("completed"); expect(job.items[0]).toMatchObject({ state: "done", itemId: 900 }); expect(mocks.export).toHaveBeenCalledOnce(); }); it("does not run a second worker while another holds the lease", async () => { mocks.set.mockResolvedValue(null); await drainFurnitureImports(); expect(mocks.import).not.toHaveBeenCalled(); }); it("marks abandoned work interrupted without repeating an uncertain import", async () => { job.state = "running"; job.items[0].state = "running"; await drainFurnitureImports(); expect(job.state).toBe("interrupted"); expect(job.items[0].state).toBe("failed"); expect(mocks.import).not.toHaveBeenCalled(); }); it("keeps errors for failed items and continues the batch", async () => { job.items.push({ ...job.items[0], classname: "table" }); mocks.import.mockResolvedValueOnce({ ok: false, error: "Missing asset", warnings: [], }); await drainFurnitureImports(); expect(job.items.map((i) => i.state)).toEqual(["failed", "done"]); expect(job.items[0].error).toBe("Missing asset"); }); it("loads attachments using the job owner and exact classname", async () => { job.items[0].attachmentId = "upload"; mocks.attachment.mockResolvedValue(Buffer.from("verified")); await drainFurnitureImports(); expect(mocks.attachment).toHaveBeenCalledWith("upload", "chair", 5); expect(mocks.import).toHaveBeenCalledWith( expect.objectContaining({ providedNitro: Buffer.from("verified") }), ); });