Block invalid catalog_items writes at the API level (points currency allowlist, non-negative prices, positive amount, limited stack >= sold count, unique sibling order numbers) and auto-assign unique order numbers on bulk create. Add a catalog-maintenance scan + transactional repair that fixes pre-existing rows: resets unsupported points_type, clamps negative costs, sets amount to 1, raises limited_stack, renumbers duplicate orders and deletes offers with missing page/item references. Surface the issue count and a fix button in the admin maintenance panel. Also: add enabled/retired flag to clone sources, classify poster and currency furniture in item-kind, and remove the obsolete update-Nitrov3.sh.
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
// Read-only audit run that writes the full summary to a log file.
|
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
const runLive = process.env.RUN_CATALOG_AUDIT_LIVE === "1";
|
|
const LOG = "/tmp/catalog-audit-summary.log";
|
|
const log = (msg: string) => {
|
|
appendFileSync(LOG, `${msg}\n`);
|
|
};
|
|
|
|
describe.skipIf(!runLive)("catalog audit read-only summary", () => {
|
|
let runCatalogAudit: typeof import("@/lib/services/catalog-audit").runCatalogAudit;
|
|
|
|
beforeAll(async () => {
|
|
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 auditMod = await import("@/lib/services/catalog-audit");
|
|
runCatalogAudit = auditMod.runCatalogAudit;
|
|
});
|
|
|
|
it("reports the full audit summary", async () => {
|
|
expect(process.env.DATABASE_URL).toMatch(/@/);
|
|
|
|
const events: Array<Record<string, unknown>> = [];
|
|
await runCatalogAudit(
|
|
(evt) => events.push(evt as unknown as Record<string, unknown>),
|
|
{},
|
|
);
|
|
|
|
const last = events[events.length - 1];
|
|
expect(last?.type).toBe("batch_complete");
|
|
const summary = last.summary as Record<string, unknown>;
|
|
const issues = last.issues as Array<{
|
|
type: string;
|
|
severity: string;
|
|
message: string;
|
|
classname?: string;
|
|
}>;
|
|
const unrepairable = last.unrepairable as Array<{
|
|
classname: string;
|
|
itemId: number;
|
|
missing: Array<string>;
|
|
}>;
|
|
|
|
log("=== SUMMARY ===");
|
|
log(JSON.stringify(summary, null, 2));
|
|
log("=== ISSUES ===");
|
|
log(JSON.stringify(issues, null, 2));
|
|
log("=== UNREPAIRABLE ===");
|
|
log(JSON.stringify(unrepairable, null, 2));
|
|
}, 300000);
|
|
});
|