Move the record contract, planning, and upsert logic out of the CLI script and into src/lib/arma-import/catalog.ts so it can be unit-tested and reused. The CLI (importArmaLibrary.ts) becomes a thin wrapper. Records are upserted into assets (keyed by className) and vehicles (by classname); thumbnails are materialized from .paa inventory icons, and fuel config is resolved per record type. Adds integration coverage for the catalog import path.
286 lines
9.7 KiB
TypeScript
286 lines
9.7 KiB
TypeScript
import { getPayload, Payload } from "payload";
|
|
import config from "@/payload.config";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
ASSET_TYPES,
|
|
planRecord,
|
|
upsertPlan,
|
|
type CatalogRecord,
|
|
} 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[] = [];
|
|
|
|
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(() => {});
|
|
}
|
|
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("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");
|
|
});
|
|
});
|
|
});
|