fix(catalog): resolve classname candidates using public_name
CI / check (push) Failing after 3m17s
CI / deploy (push) Skipped

This commit is contained in:
openhands committed 2026-09-07 19:15:35 +02:00
1 parent 8c47c3c158
commit ca6aa97eb2
5 files changed
+187 -93

No files matched your search

+4 -2
View File
@@ -193,13 +193,15 @@ describe.skipIf(!runLive)("catalog audit live (read-only)", () => {
if (types.NO_ASSET_KINDS.has(kind)) continue; if (types.NO_ASSET_KINDS.has(kind)) continue;
if ( if (
!types !types
.assetNameCandidates(r.item_name) .assetNameCandidates(r.item_name, r.public_name)
.nitro.some((n) => nitroSet.has(n)) .nitro.some((n) => nitroSet.has(n))
) { ) {
missingNitro++; missingNitro++;
} }
if ( if (
!types.assetNameCandidates(r.item_name).icon.some((n) => iconSet.has(n)) !types
.assetNameCandidates(r.item_name, r.public_name)
.icon.some((n) => iconSet.has(n))
) { ) {
missingIcon++; missingIcon++;
} }
+26 -18
View File
@@ -320,9 +320,10 @@ export async function runCatalogAudit(
itemId: item.id, itemId: item.id,
message: `"${item.item_name}" (ID ${item.id}) has no catalog_items entry — not purchasable`, message: `"${item.item_name}" (ID ${item.id}) has no catalog_items entry — not purchasable`,
}); });
const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) => const hasNitro = assetNameCandidates(
nitroSet.has(n), item.item_name,
); item.public_name,
).nitro.some((n) => nitroSet.has(n));
// Pets ship their assets through the pet pipeline, not as furni // Pets ship their assets through the pet pipeline, not as furni
// .nitro bundles, so they can get a catalog entry regardless. // .nitro bundles, so they can get a catalog entry regardless.
if (kind === "pet" || hasNitro) { if (kind === "pet" || hasNitro) {
@@ -398,9 +399,10 @@ export async function runCatalogAudit(
// Badges, pets, effects, bots and sticky notes never ship a furni // Badges, pets, effects, bots and sticky notes never ship a furni
// .nitro bundle — skip the asset check for them. // .nitro bundle — skip the asset check for them.
if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue; if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue;
const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) => const hasNitro = assetNameCandidates(
nitroSet.has(n), item.item_name,
); item.public_name,
).nitro.some((n) => nitroSet.has(n));
if (!hasNitro) { if (!hasNitro) {
missingNitroClassnames.push(item.item_name); missingNitroClassnames.push(item.item_name);
issues.push({ issues.push({
@@ -429,9 +431,10 @@ export async function runCatalogAudit(
// Badges use album1584 .gif icons; pets, effects, bots and sticky notes // Badges use album1584 .gif icons; pets, effects, bots and sticky notes
// never ship a furni icon file — skip the asset check for them. // never ship a furni icon file — skip the asset check for them.
if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue; if (NO_ASSET_KINDS.has(kindOf(item.item_name))) continue;
const hasIcon = assetNameCandidates(item.item_name).icon.some((n) => const hasIcon = assetNameCandidates(
iconSet.has(n), item.item_name,
); item.public_name,
).icon.some((n) => iconSet.has(n));
if (!hasIcon) { if (!hasIcon) {
missingIconClassnames.push(item.item_name); missingIconClassnames.push(item.item_name);
issues.push({ issues.push({
@@ -493,9 +496,11 @@ export async function runCatalogAudit(
// left over cannot be restored from any source → legacy/broken. // left over cannot be restored from any source → legacy/broken.
const stillMissing: string[] = []; const stillMissing: string[] = [];
for (const classname of missingIconClassnames) { for (const classname of missingIconClassnames) {
const hasIcon = assetNameCandidates(classname).icon.some((n) => const item = itemByName.get(classname);
iconSet.has(n), const hasIcon = assetNameCandidates(
); classname,
item?.public_name,
).icon.some((n) => iconSet.has(n));
if (!hasIcon) { if (!hasIcon) {
stillMissing.push(classname); stillMissing.push(classname);
} }
@@ -566,9 +571,11 @@ export async function runCatalogAudit(
// left over cannot be restored from any source → legacy/broken. // left over cannot be restored from any source → legacy/broken.
const stillMissing: string[] = []; const stillMissing: string[] = [];
for (const classname of missingNitroClassnames) { for (const classname of missingNitroClassnames) {
const hasNitro = assetNameCandidates(classname).nitro.some((n) => const item = itemByName.get(classname);
nitroSet.has(n), const hasNitro = assetNameCandidates(
); classname,
item?.public_name,
).nitro.some((n) => nitroSet.has(n));
if (!hasNitro) { if (!hasNitro) {
stillMissing.push(classname); stillMissing.push(classname);
} }
@@ -714,9 +721,10 @@ export async function runCatalogAudit(
} }
if (NO_CATALOG_KINDS.has(kind)) continue; if (NO_CATALOG_KINDS.has(kind)) continue;
if (postCatalogIds.has(item.id)) continue; if (postCatalogIds.has(item.id)) continue;
const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) => const hasNitro = assetNameCandidates(
nitroSet.has(n), item.item_name,
); item.public_name,
).nitro.some((n) => nitroSet.has(n));
if (kind === "pet" || hasNitro) { if (kind === "pet" || hasNitro) {
missingCatalogEntries.push({ missingCatalogEntries.push({
classname: item.item_name, classname: item.item_name,
+26 -7
View File
@@ -77,8 +77,15 @@ export const NO_CATALOG_KINDS: ReadonlySet<CatalogItemKind> = new Set([
* files. Returns `.nitro` and icon candidates (most specific first): * files. Returns `.nitro` and icon candidates (most specific first):
* the exact classname, the star-variant base, the star→underscore name and the * the exact classname, the star-variant base, the star→underscore name and the
* dot→underscore name (`post.it` → `post_it.nitro`). * dot→underscore name (`post.it` → `post_it.nitro`).
*
* When `publicName` is provided it is treated as an alternative classname (many
* hotels store the real classname in `items_base.public_name` while `item_name`
* holds a display name). Candidates from both names are merged and deduplicated.
*/ */
export function assetNameCandidates(classname: string): { export function assetNameCandidates(
classname: string,
publicName?: string,
): {
nitro: string[]; nitro: string[];
icon: string[]; icon: string[];
} { } {
@@ -97,12 +104,24 @@ export function assetNameCandidates(classname: string): {
const starUnderscore = classname.replace(/\*/g, "_"); const starUnderscore = classname.replace(/\*/g, "_");
const dotUnderscore = classname.replace(/\./g, "_"); const dotUnderscore = classname.replace(/\./g, "_");
const bases = [classname, starBase, dotUnderscore, starUnderscore];
// Also try public_name when it looks like a real classname (no spaces,
// not empty, not "0" which some hotels use as a "no value" marker).
if (
publicName &&
publicName !== classname &&
!publicName.includes(" ") &&
publicName !== "0"
) {
const pnBase = publicName.split("*")[0];
const pnStar = publicName.replace(/\*/g, "_");
const pnDot = publicName.replace(/\./g, "_");
bases.push(publicName, pnBase, pnDot, pnStar);
}
return { return {
nitro: unique([classname, starBase, dotUnderscore, starUnderscore]).map( nitro: unique(bases).map((n) => `${n}.nitro`),
(n) => `${n}.nitro`, icon: unique(bases).map((n) => `${n}_icon.png`),
),
icon: unique([classname, starUnderscore, dotUnderscore, starBase]).map(
(n) => `${n}_icon.png`,
),
}; };
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff