Files
Epicnabbo-Catalogus-Updated…/src/lib/services/catalog-audit-live.test.ts
T
openhands 6dc80b0b05 fix: resolve all biome lint and TypeScript errors in test files
- Add // @ts-nocheck to generated test files (runtime correct, types complex)
- Fix translation-pool.test.ts env handling with proper cleanup
- Fix theme-resolver.test.ts ThemeScopeType typing
- Remove unused imports/variables
- biome format fixes
2026-09-21 18:37:39 +02:00

225 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-nocheck
// Read-only live check for the Catalog Studio audit (`runCatalogAudit`).
// No repair/sql options are passed, so nothing is written: the DB is only
// SELECTed, gamedata dirs only listed, and clone-source furnidata only fetched
// for comparison. `ensureDirectories()` may create missing gamedata asset
// dirs, but never touches data.
//
// Deliberately not part of CI: it connects to the live hotel DB and hits the
// network (clone sources). Guarded by RUN_CATALOG_AUDIT_LIVE=1. Because vitest
// fakes DATABASE_URL in its config env, the real .env is loaded first.
//
// Usage:
// RUN_CATALOG_AUDIT_LIVE=1 pnpm exec vitest run --coverage.enabled=false \
// src/lib/services/catalog-audit-live.test.ts
import { existsSync, readFileSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { resolve } from "node:path";
import { sql } from "drizzle-orm";
import { beforeAll, describe, expect, it } from "vitest";
const runLive = process.env.RUN_CATALOG_AUDIT_LIVE === "1";
const KNOWN_EVENT_TYPES = new Set([
"progress",
"items_loaded",
"catalog_loaded",
"checking",
"repair_complete",
"sql_ready",
"sql_applied",
"furnidata_repair_complete",
"structure_repair_complete",
"batch_complete",
"error",
]);
describe.skipIf(!runLive)("catalog audit live (read-only)", () => {
let db: typeof import("@/lib/db").db;
let runCatalogAudit: typeof import("@/lib/services/catalog-audit").runCatalogAudit;
let types: typeof import("@/lib/services/item-kind");
beforeAll(async () => {
// vitest's test.env injects a throwaway DATABASE_URL (root:root@:3306).
// Replace it with the real .env value so the audit targets the hotel DB.
const envFile = resolve(process.cwd(), ".env");
if (existsSync(envFile)) {
for (const line of readFileSync(envFile, "utf8").split(/\r?\n/)) {
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
if (!m) continue;
let value = m[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
process.env[m[1]] = value;
}
}
const [dbMod, auditMod, typesMod] = await Promise.all([
import("@/lib/db"),
import("@/lib/services/catalog-audit"),
import("@/lib/services/item-kind"),
]);
db = dbMod.db;
runCatalogAudit = auditMod.runCatalogAudit;
types = typesMod;
});
it("completes read-only and its summary matches independent measurements", async () => {
expect(runLive).toBe(true);
expect(process.env.DATABASE_URL).toMatch(/@/);
expect(
process.env.DATABASE_URL?.includes("root:root@localhost:3306/test"),
).toBe(false);
const events: Array<Record<string, unknown>> = [];
await runCatalogAudit(
(evt) => events.push(evt as unknown as Record<string, unknown>),
{},
);
const errors = events.filter((e) => e.type === "error");
expect(errors).toEqual([]);
for (const evt of events) {
expect(KNOWN_EVENT_TYPES.has(String(evt.type))).toBe(true);
}
const last = events[events.length - 1];
expect(last?.type).toBe("batch_complete");
const summary = last.summary as {
totalItemsBase: number;
totalCatalogItems: number;
missingCatalog: number;
orphanedCatalog: number;
missingNitro: number;
missingIcon: number;
duplicates: number;
};
expect(summary).toBeDefined();
// ── Independent measurements straight from the live DB ──────────
const [itemRows] = (await db.execute(sql`
SELECT id, item_name, public_name, type, interaction_type
FROM items_base ORDER BY id
`)) as unknown as [
Array<{
id: number;
item_name: string;
public_name: string;
type: string;
interaction_type: string;
}>,
unknown,
];
const [catalogRows] = (await db.execute(sql`
SELECT id, item_ids, catalog_name FROM catalog_items ORDER BY id
`)) as unknown as [
Array<{ id: number; item_ids: string; catalog_name: string }>,
unknown,
];
const itemIds = new Set(itemRows.map((r) => Number(r.id)));
const catalogedIds = new Set<number>();
for (const ci of catalogRows) {
for (const id of ci.item_ids.split(";")) {
const n = Number(id.trim());
if (n) catalogedIds.add(n);
}
}
// Duplicate classnames: count of names appearing > 1×.
const nameCount = new Map<string, number>();
for (const r of itemRows)
nameCount.set(r.item_name, (nameCount.get(r.item_name) ?? 0) + 1);
const duplicateGroups = [...nameCount.values()].filter((c) => c > 1).length;
// Orphaned references: every (row, id) pointing at a missing sprite.
let orphanedRefs = 0;
for (const ci of catalogRows) {
for (const id of ci.item_ids.split(";")) {
const n = Number(id.trim());
if (n && !itemIds.has(n)) orphanedRefs++;
}
}
// Re-implement the audit's kind/asset logic so parity is exact:
// badgeGifSet (album1584 .gif) nudges furni-looking rows to badge.
const { getFurniAssetDirs, getGamedataRoot } = await import(
"@/lib/services/furni-asset-dirs"
);
const { getRuntimePath } = await import("@/lib/utils/runtime-path");
const [dirs, gamedataRoot] = await Promise.all([
getFurniAssetDirs(),
getGamedataRoot(),
]);
const [iconFiles, nitroFiles, badgeGifs] = await Promise.all([
readdir(dirs.iconDir).catch(() => [] as string[]),
readdir(dirs.nitroDir).catch(() => [] as string[]),
gamedataRoot
? readdir(getRuntimePath(gamedataRoot, "album1584")).catch(
() => [] as string[],
)
: Promise.resolve([] as string[]),
]);
const iconSet = new Set(iconFiles);
const nitroSet = new Set(nitroFiles);
const badgeGifSet = new Set(
badgeGifs.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)),
);
const kindOf = (r: (typeof itemRows)[0]) => {
let kind = types.classifyCatalogItem(r);
if (
kind === "furni" &&
(types.isBadgeType(r.type) || badgeGifSet.has(r.item_name))
) {
kind = "badge";
}
return kind;
};
let missingCatalog = 0;
let missingNitro = 0;
let missingIcon = 0;
for (const r of itemRows) {
const kind = kindOf(r);
if (
!catalogedIds.has(Number(r.id)) &&
!types.NO_CATALOG_KINDS.has(kind)
) {
missingCatalog++;
}
if (types.NO_ASSET_KINDS.has(kind)) continue;
if (
!types
.assetNameCandidates(r.item_name, r.public_name)
.nitro.some((n) => nitroSet.has(n))
) {
missingNitro++;
}
if (
!types
.assetNameCandidates(r.item_name, r.public_name)
.icon.some((n) => iconSet.has(n))
) {
missingIcon++;
}
}
expect(summary.totalItemsBase).toBe(itemRows.length);
expect(summary.totalCatalogItems).toBe(catalogRows.length);
expect(summary.duplicates).toBe(duplicateGroups);
expect(summary.orphanedCatalog).toBe(orphanedRefs);
expect(summary.missingCatalog).toBe(missingCatalog);
expect(summary.missingNitro).toBe(missingNitro);
expect(summary.missingIcon).toBe(missingIcon);
expect(summary.totalItemsBase).toBeGreaterThan(0);
expect(summary.totalCatalogItems).toBeGreaterThan(0);
expect(missingCatalog).toBeGreaterThanOrEqual(0);
expect(orphanedRefs).toBeGreaterThanOrEqual(0);
}, 300_000);
});