feat(catalog): repair page hierarchy cycles and duplicate sibling page order
The Arcturus errors "page hierarchy contains a cycle page 354 and 357" and "sibling order 1 is used more than once (111 problems)" come from catalog_pages, not catalog_items: pages 354/357 point at themselves, and many parents have child pages sharing the same order_num. Extend the emulator catalog scan + fix to detect both: pages whose parent chain loops back get detached (parent_id = 0 on the highest cycle member) and every affected parent's children are renumbered sequentially, preserving their current relative order. Add scripts/diag-emulator.ts to inspect the live catalog state.
This commit is contained in:
1 parent
acbe54fcb7
commit
cea88eaa57
3 files changed
+252
-5
No files matched your search
@@ -7,6 +7,8 @@ const state = vi.hoisted(() => ({
|
||||
pages: [] as Array<Record<string, unknown>>,
|
||||
bases: [] as Array<Record<string, unknown>>,
|
||||
orderRows: [] as Array<Record<string, unknown>>,
|
||||
cycleRows: [] as Array<Record<string, unknown>>,
|
||||
childrenByParent: {} as Record<string, Array<Record<string, unknown>>>,
|
||||
queries: [] as { sql: string; params: unknown[] }[],
|
||||
writes: 0,
|
||||
}));
|
||||
@@ -25,8 +27,24 @@ vi.mock("@/lib/db", () => ({
|
||||
execute: async (query: SQL) => {
|
||||
const compiled = new MySqlDialect().sqlToQuery(query);
|
||||
state.queries.push(compiled);
|
||||
if (compiled.sql.trimStart().startsWith("SELECT"))
|
||||
const sqlText = compiled.sql.replace(/\s+/g, " ").trim();
|
||||
if (sqlText.includes("SELECT id, page_id AS pageId, order_number"))
|
||||
return [state.orderRows, []];
|
||||
if (
|
||||
sqlText.includes(
|
||||
"SELECT id, parent_id AS parentId FROM catalog_pages",
|
||||
)
|
||||
)
|
||||
return [state.cycleRows, []];
|
||||
if (
|
||||
sqlText.includes(
|
||||
"SELECT id, order_num AS orderNum FROM catalog_pages",
|
||||
)
|
||||
) {
|
||||
const parent = String(compiled.params[0]);
|
||||
return [state.childrenByParent[parent] ?? [], []];
|
||||
}
|
||||
if (sqlText.startsWith("SELECT")) return [state.orderRows, []];
|
||||
state.writes++;
|
||||
return [{ affectedRows: 1 }, []];
|
||||
},
|
||||
@@ -53,9 +71,11 @@ const item = (id: number, overrides: Record<string, unknown> = {}) => ({
|
||||
|
||||
beforeEach(() => {
|
||||
state.items = [];
|
||||
state.pages = [{ id: 10 }];
|
||||
state.pages = [{ id: 10, parentId: 0, orderNum: 1 }];
|
||||
state.bases = [{ id: 12 }];
|
||||
state.orderRows = [];
|
||||
state.cycleRows = [];
|
||||
state.childrenByParent = {};
|
||||
state.queries = [];
|
||||
state.writes = 0;
|
||||
});
|
||||
@@ -165,3 +185,70 @@ describe("fixEmulatorIssues", () => {
|
||||
expect(state.writes).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("page hierarchy checks", () => {
|
||||
it("flags pages whose parent chain forms a cycle", async () => {
|
||||
state.pages = [
|
||||
{ id: 354, parentId: 354, orderNum: 4 },
|
||||
{ id: 357, parentId: 357, orderNum: 7 },
|
||||
];
|
||||
const issues = await listEmulatorIssues();
|
||||
const cycles = issues.filter((i) => i.type === "page_cycle");
|
||||
expect(cycles.map((i) => i.id).sort()).toEqual([354, 357]);
|
||||
});
|
||||
|
||||
it("does not flag pages with a valid parent chain", async () => {
|
||||
state.pages = [{ id: 354, parentId: 10, orderNum: 4 }];
|
||||
const issues = await listEmulatorIssues();
|
||||
expect(issues.filter((i) => i.type === "page_cycle")).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags sibling pages that share an order_num under the same parent", async () => {
|
||||
state.pages = [
|
||||
{ id: 110, parentId: 49, orderNum: 1 },
|
||||
{ id: 111, parentId: 49, orderNum: 1 },
|
||||
{ id: 112, parentId: 49, orderNum: 2 },
|
||||
{ id: 120, parentId: 0, orderNum: 1 },
|
||||
];
|
||||
const issues = await listEmulatorIssues();
|
||||
const flags = issues.filter((i) => i.type === "sibling_page_order");
|
||||
expect(flags.map((i) => i.id).sort()).toEqual([110, 111]);
|
||||
expect(flags.every((i) => i.pageId === 49)).toBe(true);
|
||||
});
|
||||
|
||||
it("breaks cycles and re-numbers sibling pages in one transaction", async () => {
|
||||
state.pages = [
|
||||
{ id: 10, parentId: 0, orderNum: 1 },
|
||||
{ id: 354, parentId: 354, orderNum: 4 },
|
||||
{ id: 357, parentId: 357, orderNum: 7 },
|
||||
{ id: 110, parentId: 49, orderNum: 1 },
|
||||
{ id: 111, parentId: 49, orderNum: 1 },
|
||||
{ id: 112, parentId: 49, orderNum: 2 },
|
||||
];
|
||||
state.cycleRows = [
|
||||
{ id: 354, parentId: 354 },
|
||||
{ id: 357, parentId: 357 },
|
||||
];
|
||||
state.childrenByParent["49"] = [
|
||||
{ id: 110, orderNum: 1 },
|
||||
{ id: 111, orderNum: 1 },
|
||||
{ id: 112, orderNum: 2 },
|
||||
];
|
||||
|
||||
await fixEmulatorIssues();
|
||||
|
||||
const bySql = (needle: string) =>
|
||||
state.queries.filter((q) =>
|
||||
q.sql.replace(/\s+/g, " ").trimStart().includes(needle),
|
||||
);
|
||||
const detach = bySql("UPDATE catalog_pages SET parent_id = 0 WHERE id");
|
||||
expect(detach.length).toBe(2);
|
||||
expect([...detach[0].params, ...detach[1].params]).toEqual([354, 357]);
|
||||
|
||||
const reorder = bySql("UPDATE catalog_pages SET order_num =");
|
||||
expect(reorder.length).toBe(2);
|
||||
expect([...reorder[0].params, ...reorder[1].params]).toEqual([
|
||||
2, 111, 3, 112,
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user