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.
94 lines
2.8 KiB
TypeScript
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"));
|
|
});
|
|
});
|