Add Catalog Studio inspection and guided import review
CI / check (push) Successful in 1m39s
CI / deploy (push) Successful in 1m18s
CI / e2e (push) Successful in 25s

This commit is contained in:
Simo committed 2026-09-05 13:10:43 +02:00
1 parent 2578bf6a09
commit 26f071117b
7 files changed
+961 -44

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import {
compareFurniture,
type FurnitureInspection,
validateClassnames,
} from "./studio-inspection";
const source = {
name: "Chair",
description: "A chair",
revision: 2,
type: "flooritem",
};
const empty: FurnitureInspection = {
classname: "chair",
sql: [],
catalog: [],
furnidata: [],
furnidataReadable: true,
nitro: { exists: false, bytes: 0 },
icon: { exists: false, bytes: 0 },
};
describe("studio inspection", () => {
it("rejects paths and oversized batches", () => {
expect(validateClassnames(["../file"])).toBeNull();
expect(validateClassnames(["a\\b"])).toBeNull();
expect(validateClassnames(Array(501).fill("chair"))).toBeNull();
expect(validateClassnames(["chair*0", "chair*0"])).toEqual(["chair*0"]);
});
it("identifies new furniture only after a successful lookup", () => {
expect(compareFurniture(source, empty).state).toBe("new");
expect(
compareFurniture(source, { ...empty, furnidataReadable: false }).state,
).toBe("unknown");
});
it("compares values without treating allocated IDs as conflicts", () => {
const local = {
...empty,
sql: [{ id: 901, spriteId: 902, name: "Old chair", type: "s" }],
furnidata: [
{
id: 902,
name: "Old chair",
description: "Old",
revision: 1,
type: "flooritem",
},
],
};
expect(compareFurniture(source, local).state).toBe("changed");
expect(
compareFurniture(source, local).fields.find((f) => f.label === "Name")
?.changed,
).toBe(true);
});
it("flags duplicate SQL classnames as conflicts", () => {
expect(
compareFurniture(source, {
...empty,
sql: [
{ id: 1, spriteId: 1, name: "Chair", type: "s" },
{ id: 2, spriteId: 2, name: "Chair", type: "s" },
],
}).state,
).toBe("conflict");
});
});
+70
View File
@@ -0,0 +1,70 @@
export interface FurnitureInspection {
classname: string;
sql: Array<{ id: number; spriteId: number; name: string; type: string }>;
catalog: Array<{
id: number;
pageId: number;
credits: number;
points: number;
}>;
furnidata: Array<{
id: number;
name: string;
description: string;
revision: number;
type: string;
}>;
furnidataReadable: boolean;
nitro: { exists: boolean | null; bytes: number };
icon: { exists: boolean | null; bytes: number };
}
export function validateClassnames(value: unknown): string[] | null {
if (
!Array.isArray(value) ||
!value.length ||
value.length > 500 ||
value.some(
(v) =>
typeof v !== "string" || v.length > 200 || !/^[a-zA-Z0-9_*-]+$/.test(v),
)
)
return null;
return [...new Set(value)];
}
export function compareFurniture(
source: { name: string; description: string; revision: number; type: string },
local: FurnitureInspection,
) {
const fd = local.furnidata[0],
sql = local.sql[0];
const values = [
["Name", sql?.name ?? fd?.name, source.name],
["Description", fd?.description, source.description],
["Revision", fd?.revision, source.revision || undefined],
[
"Type",
sql ? (sql.type === "i" ? "wallitem" : "flooritem") : fd?.type,
source.type,
],
] as const;
const fields = values.map(([label, before, after]) => ({
label,
before,
after,
changed:
before !== undefined &&
after !== undefined &&
String(before) !== String(after),
}));
const state =
local.sql.length > 1 || local.furnidata.length > 1
? "conflict"
: !local.furnidataReadable
? "unknown"
: !sql && !fd
? "new"
: fields.some((f) => f.changed)
? "changed"
: "present";
return { state, fields } as const;
}