Files
EpicNext-Cms/src/lib/services/upload-import.test.ts
T
openhands b26e2e0de4
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 31s
CI / tests-integration (push) Successful in 2m17s
CI / tests-unit (push) Failing after 2m29s
CI / tests-ui (push) Successful in 3m17s
CI / preflight (push) Skipped
CI / deploy (push) Skipped
feat(nitro): normalise hotel and uploaded bundles to WebP Lossless
Importing from a hotel wrote the downloaded .nitro to disk untouched, so
official PNG textures stayed PNG and only SWF imports ended up as WebP.
Every Studio import should produce the same format regardless of where the
bytes came from, so both clone and upload paths now run the bundle through
toWebpLosslessBundle.

The helper decodes the texture and re-encodes it with the same VP8L options
the SWF importer uses, so the artwork round-trips bit-for-bit, and lets
createNitroBundle relabel the member and repair the meta.image pointer. A
bundle that is already lossless WebP is returned untouched, making the
operation idempotent and safe to run on re-import. A colour variant that
shares a library keeps the member base name it arrived with.

A texture that cannot be decoded keeps its original format with a warning
instead of failing the import: the bundle is valid, and losing a furniture
item over a codec edge case is worse than a slightly larger texture.
2026-09-27 15:55:17 +02:00

160 lines
4.3 KiB
TypeScript

// @ts-nocheck
// @vitest-environment node
import { promises as fs } from "node:fs";
import os from "node:os";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getRuntimePath } from "@/lib/utils/runtime-path";
const {
appendFurniEntryMock,
assetTargets,
dbExecuteMock,
getOrCreateCategoryPageMock,
} = vi.hoisted(() => ({
appendFurniEntryMock: vi.fn(),
assetTargets: {
swfDir: "",
iconDir: "",
nitroDir: "",
mirrorDirs: [] as Array<{
swfDir: string;
iconDir: string;
nitroDir: string;
}>,
},
dbExecuteMock: vi.fn(),
getOrCreateCategoryPageMock: vi.fn(),
}));
vi.mock("drizzle-orm", () => ({
eq: vi.fn(() => ({})),
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({
strings,
values,
}),
}));
vi.mock("@/lib/db", async () => ({
...(await import("@/test/db-helpers")).createDbHelpers(dbExecuteMock),
ItemsBase: { id: "id", itemName: "item_name" },
db: {
execute: dbExecuteMock,
select: () => ({
from: () => ({
where: () => ({
limit: async () => [],
}),
}),
}),
},
}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
getFurniAssetDirs: vi.fn(async () => assetTargets),
getFurniAssetWriteTargets: vi.fn(async () => assetTargets),
}));
vi.mock("@/lib/services/furni-data", () => ({
appendFurniEntry: appendFurniEntryMock,
buildFurniEntry: vi.fn(() => ({ id: 501 })),
}));
vi.mock("@/lib/services/furni-import", () => ({
allocateCatalogItemId: vi.fn(
async (insertFn: (nextId: number) => Promise<unknown>) => insertFn(701),
),
autoPriceFurni: vi.fn(() => ({ credits: 3, points: 0, pointsType: 0 })),
CATEGORY_PAGE: {},
classifyFurni: vi.fn(() => "other"),
ensureDirectories: vi.fn(async () => {
const dirs = [
assetTargets.swfDir,
assetTargets.iconDir,
assetTargets.nitroDir,
...assetTargets.mirrorDirs.flatMap((target) => [
target.swfDir,
target.iconDir,
target.nitroDir,
]),
];
await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })));
}),
getOrCreateCategoryPage: getOrCreateCategoryPageMock,
IMPORTED_PAGE_CAPTION: "Imported Furniture",
IMPORTED_PAGE_CAPTION_SAVE: "imported_furni",
}));
vi.mock("@/lib/services/swf/nitro-builder", () => ({
parseNitroBundle: vi.fn(() => ({
json: { xdim: 1, ydim: 1 },
texture: Buffer.from("texture-bytes"),
textureFileName: "chair.png",
})),
isLosslessWebp: vi.fn(() => false),
toWebpLosslessBundle: vi.fn(() =>
Promise.resolve(Buffer.from("webp-normalised", "utf-8")),
),
}));
import {
__resetUploadItemsBaseAllocatorForTests,
uploadSingleFurni,
} from "./upload-import";
describe("uploadSingleFurni live asset mirrors", () => {
let tempDir: string;
beforeEach(async () => {
__resetUploadItemsBaseAllocatorForTests();
tempDir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "upload-furni-"));
assetTargets.swfDir = getRuntimePath(tempDir, "cms", "swf");
assetTargets.iconDir = getRuntimePath(tempDir, "cms", "icons");
assetTargets.nitroDir = getRuntimePath(tempDir, "cms", "furniture");
assetTargets.mirrorDirs = [
{
swfDir: assetTargets.swfDir,
iconDir: getRuntimePath(tempDir, "Gamedata", "icons"),
nitroDir: getRuntimePath(tempDir, "Gamedata", "bundled", "furniture"),
},
];
dbExecuteMock.mockReset();
dbExecuteMock
.mockResolvedValueOnce([[{ next: 501 }], undefined])
.mockResolvedValue(undefined);
getOrCreateCategoryPageMock.mockResolvedValue("imp_other");
appendFurniEntryMock.mockResolvedValue(undefined);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it("copies the Nitro bundle and icon into the directories served by production", async () => {
const nitroBuffer = Buffer.from("nitro-bundle");
const iconBuffer = Buffer.from("png-icon");
const result = await uploadSingleFurni({
classname: "chair",
name: "Chair",
itemType: "s",
nitroBuffer,
iconBuffer,
});
expect(result.ok).toBe(true);
// Every upload is normalised to WebP Lossless, whatever the source
// bundle carried, so what lands on disk is not the raw upload.
await expect(
fs.readFile(
getRuntimePath(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"),
),
).resolves.toEqual(Buffer.from("webp-normalised", "utf-8"));
await expect(
fs.readFile(
getRuntimePath(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"),
),
).resolves.toEqual(iconBuffer);
});
});