1
0
Fork 0
polaris-task-force/tests/int/arma-catalog-import.int.spec.ts
Z8MB1E d4f9051b17 feat(arma): extract catalog icons and extend the import library
- Add an `icons:arma` tool (extractArmaIcons.ts) that pulls inventory icons
  out of the game's PBOs and materializes the .paa files the catalog needs.
- Extend the catalog import library with the matching planning and upsert
  support, and cover it with integration tests.
2026-10-03 03:29:58 -04:00

457 lines
17 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { mkdirSync, rmSync, writeFileSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import {
ASSET_TYPES,
enrichPlanMedia,
linkWeaponCompatibility,
planRecord,
upsertPlan,
type CatalogRecord,
type MediaEnv,
} from "@/lib/arma-import/catalog";
let payload: Payload;
const RUN = `armacat-${Date.now().toString(36)}`;
let fuelId: number;
let createdFuel = false;
const assetIds: number[] = [];
const vehicleIds: number[] = [];
const mediaIds: number[] = [];
const SHOT_DIR = join(tmpdir(), `ptf-shots-${RUN}`);
// 1x1 PNG: enough for the upload path, tiny enough to inline.
const PNG_1PX = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64",
);
function mediaEnv(screenshots = ""): MediaEnv {
return { paaRoot: "", screenshots, outDir: SHOT_DIR, hemtt: "" };
}
describe("Arma catalog import", () => {
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
// Vehicles need a fuel resource for the fuel group; reuse one if a prior
// fixture left it, otherwise create it (and only then clean it up).
const existingFuel = await payload.find({
collection: "resources",
where: { name: { equals: "Fuel" } },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (existingFuel.docs[0]) {
fuelId = existingFuel.docs[0].id;
} else {
const fuel = await payload.create({
collection: "resources",
data: {
name: "Fuel",
codeName: `res_fuel_${RUN}`,
unitOfMeasure: "liter",
approvalStatus: "in_progress",
type: "fluid",
baseValue: 1,
rarity: "common",
massPerUnit: 0,
gridWidth: 1,
gridHeight: 1,
tradeable: false,
},
depth: 0,
overrideAccess: true,
});
fuelId = fuel.id;
createdFuel = true;
}
});
afterAll(async () => {
for (const id of vehicleIds) {
await payload.delete({ collection: "vehicles", id, overrideAccess: true }).catch(() => {});
}
for (const id of assetIds) {
await payload.delete({ collection: "assets", id, overrideAccess: true }).catch(() => {});
}
for (const id of mediaIds) {
await payload.delete({ collection: "media", id, overrideAccess: true }).catch(() => {});
}
rmSync(SHOT_DIR, { recursive: true, force: true });
if (createdFuel && fuelId) {
await payload.delete({ collection: "resources", id: fuelId, overrideAccess: true }).catch(() => {});
}
});
describe("planRecord classification", () => {
it("trusts type for every contract asset type", () => {
for (const t of ASSET_TYPES) {
const rec: CatalogRecord = {
classname: `PTF_Test_Thing_${t}`,
type: t,
displayName: "Thing",
picture: "",
mod: "CBA_3den",
description: "",
mass: 1,
extra: "",
};
const plan = planRecord(rec);
expect(plan).toBeDefined();
expect(plan!.kind).toBe("asset");
expect(plan!.assetType).toBe(t);
}
});
it("routes vehicle records to the vehicles collection with transportMode from extra", () => {
const air = planRecord({
classname: "B_Heli_Light_01_F",
type: "vehicle",
displayName: "Ghost Hawk",
picture: "",
mod: "Arma 3",
description: "",
mass: 3000,
extra: "air",
});
expect(air!.kind).toBe("vehicle");
expect(air!.transportMode).toBe("air");
const sea = planRecord({ classname: "C_Boat_Civil_01_F", type: "vehicle", extra: "sea" });
expect(sea!.transportMode).toBe("sea");
});
it("defaults transportMode to ground when extra is not a valid mode", () => {
const plan = planRecord({ classname: "B_MRAP_01_F", type: "vehicle", extra: "submarine" });
expect(plan!.kind).toBe("vehicle");
expect(plan!.transportMode).toBe("ground");
});
it("falls back to heuristics for unknown types (miscellaneous, not item)", () => {
const plan = planRecord({ classname: "PTF_Test_Odd_Gadget", type: "qrd_mysterious_device" });
expect(plan!.kind).toBe("asset");
expect(plan!.assetType).toBe("miscellaneous");
});
it("falls back to the legacy vehicle heuristic for unknown types", () => {
const plan = planRecord({ classname: "B_Truck_01_transport_F", type: "who_knows" });
expect(plan!.kind).toBe("vehicle");
expect(plan!.transportMode).toBe("ground");
});
it("captures magazine round counts from extra", () => {
const plan = planRecord({ classname: "30rnd_556x45_Mag", type: "magazine", extra: 30 });
expect(plan!.kind).toBe("asset");
expect(plan!.roundCapacity).toBe(30);
expect(plan!.assetType).toBe("magazine");
});
it("ignores numeric extra for non-magazine assets", () => {
const plan = planRecord({ classname: "PTF_Test_Rifle", type: "weapon", extra: 30 });
expect(plan!.assetType).toBe("weapon");
expect(plan!.roundCapacity).toBeUndefined();
});
it("supports legacy class/category/name aliases and icon-map overrides", () => {
const plan = planRecord(
{ class: "Legacy_Thing", category: "optic", name: "Legacy Optic" },
{ icons: { legacy_thing: "\\a3\\data\\ui\\icon.paa" } },
);
expect(plan!.className).toBe("Legacy_Thing");
expect(plan!.assetType).toBe("optic");
expect(plan!.displayName).toBe("Legacy Optic");
expect(plan!.pictureRef).toBe("\\a3\\data\\ui\\icon.paa");
});
it("defaults mod to Arma 3 and rejects records without a classname", () => {
const plan = planRecord({ classname: "PTF_Test_Thing", type: "weapon" });
expect(plan).toBeDefined();
expect(plan!.mod).toBe("Arma 3");
expect(planRecord({ type: "weapon" })).toBeUndefined();
});
});
describe("upsertPlan assets", () => {
const assetClassName = `${RUN}_asset_rifle`;
it("creates an asset with correct className/assetType and Asset Metadata.mod", async () => {
const plan = planRecord({
classname: assetClassName,
type: "weapon",
displayName: `${RUN} Rifle`,
picture: "",
mod: "PTF_Test_Mod",
description: "An import test rifle.",
mass: 2,
extra: "",
})!;
const res = await upsertPlan(payload, plan);
expect(res.action).toBe("created");
expect(res.collection).toBe("assets");
assetIds.push(res.id);
const doc = await payload.findByID({ collection: "assets", id: res.id, depth: 0, overrideAccess: true });
expect(doc.className).toBe(assetClassName);
expect(doc.assetType).toBe("weapon");
expect(doc.name).toBe(`${RUN} Rifle`);
expect(doc["Asset Metadata"]?.mod).toBe("PTF_Test_Mod");
expect(doc["Asset Metadata"]?.gameDescription).toBe("An import test rifle.");
expect(doc.approvalStatus).toBe("in_progress");
});
it("updates on re-run without duplicating and preserves approvalStatus and Virtual Data", async () => {
const assetId = assetIds[0];
// Simulate human edits before the second import run.
await payload.update({
collection: "assets",
id: assetId,
data: { approvalStatus: "approved", tradeable: true },
overrideAccess: true,
});
const res = await upsertPlan(
payload,
planRecord({
classname: assetClassName,
type: "weapon",
displayName: `${RUN} Rifle MkII`,
picture: "",
mod: "PTF_Test_Mod_2",
description: "Updated in-game description.",
mass: 2,
extra: "",
})!,
undefined, // assets never need fuel
);
expect(res.action).toBe("updated");
expect(res.collection).toBe("assets");
expect(res.id).toBe(assetId);
const count = await payload.count({
collection: "assets",
where: { className: { equals: assetClassName } },
});
expect(count.totalDocs).toBe(1);
const doc = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true });
expect(doc.name).toBe(`${RUN} Rifle MkII`);
expect(doc["Asset Metadata"]?.mod).toBe("PTF_Test_Mod_2");
expect(doc["Asset Metadata"]?.gameDescription).toBe("Updated in-game description.");
expect(doc.approvalStatus).toBe("approved"); // never reset by re-import
expect(doc.tradeable).toBe(true); // Virtual Data untouched
});
});
describe("adding images in a later import pass", () => {
const lateClassName = `${RUN}_late_image_rifle`;
function planFor(screenshot: string) {
const plan = planRecord({
classname: lateClassName,
type: "weapon",
displayName: `${RUN} Late Image Rifle`,
picture: "",
mod: "PTF_Test_Mod",
description: "",
mass: 1,
extra: "",
})!;
enrichPlanMedia(plan, mediaEnv(screenshot));
return plan;
}
it("imports the catalog with no image, then attaches one on a later run", async () => {
// Pass 1: data only (no --screenshots).
const first = await upsertPlan(payload, planFor(""));
expect(first.action).toBe("created");
assetIds.push(first.id);
let doc = await payload.findByID({ collection: "assets", id: first.id, depth: 0, overrideAccess: true });
expect(doc["Asset Metadata"]?.thumbnail).toBeFalsy();
// Pass 2: the screenshot now exists on disk.
mkdirSync(SHOT_DIR, { recursive: true });
writeFileSync(join(SHOT_DIR, `${lateClassName}.png`), PNG_1PX);
const second = await upsertPlan(payload, planFor(SHOT_DIR));
expect(second.action).toBe("updated");
expect(second.id).toBe(first.id); // upsert, not a duplicate
doc = await payload.findByID({ collection: "assets", id: first.id, depth: 0, overrideAccess: true });
const thumbnail = doc["Asset Metadata"]?.thumbnail;
expect(thumbnail).toBeTruthy();
if (typeof thumbnail === "number") mediaIds.push(thumbnail);
else if (thumbnail && typeof thumbnail === "object") mediaIds.push(thumbnail.id);
const count = await payload.count({
collection: "assets",
where: { className: { equals: lateClassName } },
});
expect(count.totalDocs).toBe(1);
});
it("keeps the image when re-run, and repairs a cleared one", async () => {
const assetId = assetIds[assetIds.length - 1];
const before = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true });
const originalId = typeof before["Asset Metadata"]?.thumbnail === "number"
? (before["Asset Metadata"]!.thumbnail as number)
: (before["Asset Metadata"]!.thumbnail as { id: number }).id;
// Re-run: the media doc already exists, so it must not be duplicated.
await upsertPlan(payload, planFor(SHOT_DIR));
const after = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true });
const afterId = typeof after["Asset Metadata"]?.thumbnail === "number"
? (after["Asset Metadata"]!.thumbnail as number)
: (after["Asset Metadata"]!.thumbnail as { id: number }).id;
expect(afterId).toBe(originalId);
// A cleared thumbnail is repaired by the next run (media already exists).
await payload.update({
collection: "assets",
id: assetId,
data: { "Asset Metadata": { thumbnail: null } },
overrideAccess: true,
});
await upsertPlan(payload, planFor(SHOT_DIR));
const repaired = await payload.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true });
expect(repaired["Asset Metadata"]?.thumbnail).toBeTruthy();
});
});
describe("weapon compatibility", () => {
const rifle = `${RUN}_compat_rifle`;
const mag = `${RUN}_compat_mag`;
const optic = `${RUN}_compat_optic`;
const light = `${RUN}_compat_light`;
function catalog() {
return [
{ classname: rifle, type: "weapon", displayName: "Compat Rifle", compat: {
slot: "primary",
// duplicate + different case + a ref that was never imported
magazines: [mag, mag.toUpperCase(), `${RUN}_not_in_app`],
attachments: [optic, light],
} },
{ classname: mag, type: "magazine", displayName: "Compat Mag", extra: 30 },
{ classname: optic, type: "optic", displayName: "Compat Optic" },
{ classname: light, type: "miscellaneous", displayName: "Compat Light" },
].map((r) => planRecord(r as CatalogRecord)!);
}
it("parses compat on weapons only, deduplicating classnames", () => {
const plans = catalog();
expect(plans[0].compat).toEqual({
slot: "primary",
magazines: [mag, `${RUN}_not_in_app`],
attachments: [optic, light],
});
expect(plans[1].compat).toBeUndefined();
expect(planRecord({ classname: "x", type: "magazine", compat: { magazines: ["y"] } })!.compat).toBeUndefined();
});
it("links magazines/attachments as references, merges on re-run, never duplicates", async () => {
const plans = catalog();
const ids: Record<string, number> = {};
for (const plan of plans) {
const res = await upsertPlan(payload, plan);
assetIds.push(res.id);
ids[plan.className] = res.id;
}
const first = await linkWeaponCompatibility(payload, plans);
expect(first.updated).toBe(1);
expect(first.unresolved).toBe(1); // the never-imported magazine
expect(first.skippedType).toBe(1); // rail light is "miscellaneous"
let doc = await payload.findByID({ collection: "assets", id: ids[rifle], depth: 0, overrideAccess: true });
const wc = () => (doc as unknown as { "Weapon Configuration": Record<string, unknown> })["Weapon Configuration"];
expect(wc().slotType).toBe("primary");
expect(wc().compatibleMagazines).toEqual([ids[mag]]);
expect(wc().compatibleAttachments).toEqual([ids[optic]]);
// A link added by hand survives, and re-running adds nothing twice.
const extraOptic = await upsertPlan(payload, planRecord({ classname: `${RUN}_hand_optic`, type: "optic" })!);
assetIds.push(extraOptic.id);
await payload.update({
collection: "assets",
id: ids[rifle],
data: { "Weapon Configuration": { compatibleAttachments: [ids[optic], extraOptic.id] } } as never,
overrideAccess: true,
});
const second = await linkWeaponCompatibility(payload, plans);
expect(second.updated).toBe(0);
expect(second.unchanged).toBe(1);
doc = await payload.findByID({ collection: "assets", id: ids[rifle], depth: 0, overrideAccess: true });
expect(wc().compatibleMagazines).toEqual([ids[mag]]);
expect(wc().compatibleAttachments).toEqual([ids[optic], extraOptic.id]);
// Each referenced item still exists exactly once.
for (const name of [mag, optic]) {
const count = await payload.count({ collection: "assets", where: { className: { equals: name } } });
expect(count.totalDocs).toBe(1);
}
});
});
describe("upsertPlan vehicles", () => {
const vehicleClassname = `${RUN}_heli_ghost`;
it("creates a vehicle with classname and transportMode air", async () => {
const plan = planRecord({
classname: vehicleClassname,
type: "vehicle",
displayName: `${RUN} Ghost Hawk`,
picture: "",
mod: "PTF_Test_Mod",
description: "An import test helicopter.",
mass: 3000,
extra: "air",
})!;
const res = await upsertPlan(payload, plan, fuelId);
expect(res.action).toBe("created");
expect(res.collection).toBe("vehicles");
vehicleIds.push(res.id);
const doc = await payload.findByID({ collection: "vehicles", id: res.id, depth: 0, overrideAccess: true });
expect(doc.classname).toBe(vehicleClassname);
expect(doc.name).toBe(`${RUN} Ghost Hawk`);
expect(doc.transportMode).toBe("air");
expect(doc.fuel.fuelType).toBe(fuelId);
expect(doc.fuel.fuelCapacity).toBe(100);
expect(doc.approvalStatus).toBe("in_progress");
});
it("updates an existing vehicle without duplicating", async () => {
const vehicleId = vehicleIds[0];
const res = await upsertPlan(
payload,
planRecord({
classname: vehicleClassname,
type: "vehicle",
displayName: `${RUN} Ghost Hawk II`,
extra: "air",
})!,
fuelId,
);
expect(res.action).toBe("updated");
expect(res.id).toBe(vehicleId);
const count = await payload.count({
collection: "vehicles",
where: { classname: { equals: vehicleClassname } },
});
expect(count.totalDocs).toBe(1);
const doc = await payload.findByID({ collection: "vehicles", id: vehicleId, depth: 0, overrideAccess: true });
expect(doc.name).toBe(`${RUN} Ghost Hawk II`);
expect(doc.transportMode).toBe("air");
});
});
});