| 1 | import assert from "node:assert/strict"; |
| 2 | import { loadProjectTreePageWindow } from "../lib/projectTreeWindow"; |
| 3 | |
| 4 | for (const size of [205, 405, 1000]) { |
| 5 | const calls: string[] = []; |
| 6 | let expired = false; |
| 7 | const result = await loadProjectTreePageWindow("", size, async (cursor, limit) => { |
| 8 | calls.push(cursor); |
| 9 | if (cursor === "old:200") { expired = true; throw Object.assign(new Error("expired"), { code: "stale_cursor" }); } |
| 10 | const id = expired ? "fresh" : "old"; |
| 11 | const offset = Number(cursor.split(":")[1] ?? 0); |
| 12 | return { snapshotId: id, items: Array.from({ length: Math.min(limit, size - offset) }, (_, i) => offset + i), nextCursor: offset + limit < size ? `${id}:${offset + limit}` : "", revision: 1 }; |
| 13 | }); |
| 14 | assert.equal(result.replacedSnapshot, true); |
| 15 | assert.equal(result.snapshotId, "fresh"); |
| 16 | assert.deepEqual(result.items, Array.from({ length: size }, (_, i) => i)); |
| 17 | assert.equal(calls.filter((cursor) => cursor === "").length, 2); |
| 18 | } |
| 19 | |
| 20 | let attempts = 0; |
| 21 | await assert.rejects(loadProjectTreePageWindow("", 405, async (cursor) => { |
| 22 | if (!cursor) { attempts++; return { items: [1], nextCursor: "next", revision: 1 }; } |
| 23 | throw Object.assign(new Error("expired"), { code: "stale_cursor" }); |
| 24 | }), /expired/); |
| 25 | assert.equal(attempts, 2, "one recovery per logical operation"); |
| 26 | |
| 27 | const replacement = await loadProjectTreePageWindow("expired:200", 5, async (cursor, limit) => { |
| 28 | if (cursor.startsWith("expired")) throw new Error("session_operation:stale_cursor:expired"); |
| 29 | const offset = Number(cursor || 0); |
| 30 | return { items: Array.from({ length: limit }, (_, i) => offset + i), nextCursor: String(offset + limit), revision: 2, snapshotId: "replacement" }; |
| 31 | }, 205); |
| 32 | assert.equal(replacement.items.length, 205, "append recovery restores the entire visible window"); |
| 33 | assert.equal(replacement.replacedSnapshot, true); |
| 34 | |
| 35 | await assert.rejects(loadProjectTreePageWindow("next", 5, async () => ({ items: [1], nextCursor: "next", revision: 1 })), /did not advance/); |
| 36 | await assert.rejects(loadProjectTreePageWindow("", 205, async (cursor) => ({ items: Array(200).fill(1), nextCursor: "next", snapshotId: cursor ? "b" : "a", revision: 1 })), /Mixed read snapshots/); |
| 37 | console.log("PASS immutable windows, internal-page expiry, bounded recovery and whole-window replacement"); |
| 38 |