Files
Epicnabbo-Catalogus-Updated…/src/lib/services/pet-import.test.ts
T
openhands adffac7360 feat(catalog): store furniture bundles as .hab instead of .nitro
Every bundle the CMS writes — upload, clone, sync, repair and the pet /
effect / figure importers — now lands as `<classname>.hab`, the extension
this deployment's renderer asks for. `.hab` and `.nitro` are the same
container, so an upload of either extension is accepted.

Resolution goes through one module, src/lib/furni/bundle-file.ts, so
nothing has to know the extension twice. Every existence check probes
`.hab` first and falls back to `.nitro`: the on-disk asset set is still
predominantly `.nitro`, and without the fallback Studio would report every
imported item as missing and the cleanup scan would classify 18k live
bundles as fake leftovers. Downloads are unchanged — Habbo's CDN and every
configured clone source still serve `.nitro`, so the conversion happens on
write, not on request.

Deliberately unchanged: the staged-attachment store in furni-attachment.ts
keys on a UUID and never reaches the client, so renaming it would break
in-flight recovery jobs.

Adds scripts/migrate-nitro-to-hab.ts to rename the existing asset set. It
refuses to run without --dry-run or --yes, never overwrites an existing
.hab, never deletes, and is idempotent.

Note: renderer-config.json lives outside this repo and was patched to
.hab separately; that file is served with a 30-day max-age, so returning
clients need a cms-client cache purge to pick the change up.
2026-10-10 17:09:36 +02:00

94 lines
2.8 KiB
TypeScript

// @ts-nocheck
// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/services/import/core/download", () => ({
downloadFile: vi.fn(async () => ({ ok: true, size: 100 })),
}));
vi.mock("@/lib/services/pet-source", () => ({
CANONICAL_PETS: ["dog", "cat"],
resolvePetSwfUrl: vi.fn(async (lib: string) => `https://x/${lib}.swf`),
}));
const convertSwfToNitro = vi.fn(() => ({
bundle: Buffer.from("nitro"),
warnings: [],
}));
vi.mock("@/lib/services/swf-to-nitro", () => ({
convertSwfToNitro: (...a: unknown[]) =>
convertSwfToNitro(...(a as Parameters<typeof convertSwfToNitro>)),
}));
// `vi.mock` factories are hoisted, so these handles have to exist before the
// module body runs or the fs factory hits the temporal dead zone.
const { writeFile, unlink, exists } = vi.hoisted(() => ({
writeFile: vi.fn(async () => {}),
unlink: vi.fn(async () => {}),
exists: vi.fn((_: string) => false),
}));
vi.mock("node:fs", async (orig) => {
const real = (await orig()) as typeof import("node:fs");
return {
...real,
existsSync: exists,
promises: {
...real.promises,
readFile: vi.fn(async () => Buffer.from("CWS")),
writeFile,
mkdir: vi.fn(async () => {}),
unlink,
},
};
});
import { deleteImportedPet, importSinglePet } from "./pet-import";
describe("pet-import.importSinglePet", () => {
it("downloads, converts, writes .hab", async () => {
const r = await importSinglePet({ lib: "dog" });
expect(r.ok).toBe(true);
expect(convertSwfToNitro).toHaveBeenCalled();
// Pets follow the same convention as furniture: `.hab` on disk.
expect(writeFile).toHaveBeenCalledWith(
expect.stringContaining("dog.hab"),
expect.any(Buffer),
);
});
it("fails cleanly when conversion throws", async () => {
convertSwfToNitro.mockImplementationOnce(() => {
throw new Error("bad swf");
});
const r = await importSinglePet({ lib: "cat" });
expect(r.ok).toBe(false);
expect(r.error).toContain("conversion");
});
});
describe("pet-import.deleteImportedPet", () => {
beforeEach(() => {
writeFile.mockClear();
unlink.mockClear();
exists.mockReset();
});
it("reports nothing deleted when the pet was never imported", async () => {
exists.mockReturnValue(false);
await expect(deleteImportedPet("dog")).resolves.toEqual({
deletedFile: false,
});
expect(unlink).not.toHaveBeenCalled();
});
// A re-import writes `.hab`; leaving a legacy `.nitro` behind would let the
// client pick up the stale bundle, so both are cleared.
it("clears both the `.hab` and the legacy `.nitro`", async () => {
exists.mockReturnValue(true);
await expect(deleteImportedPet("dog")).resolves.toEqual({
deletedFile: true,
});
expect(unlink).toHaveBeenCalledWith(expect.stringContaining("dog.hab"));
expect(unlink).toHaveBeenCalledWith(expect.stringContaining("dog.nitro"));
});
});