import { test } from "node:test"; import assert from "node:assert/strict"; import { SKIP_REPORT_WINDOW_MS, SkipReporter, resetSafeRevalidateWarning, runGuarded, safeRevalidate, } from "./safeRevalidate"; // Run with: pnpm -C editor exec tsx --test "app/**/*.test.ts" // // Outside a Next request (which is where a QUEUED job's body runs, and where // this test runs) revalidatePath throws the missing-store invariant. The helper // must swallow exactly that, rethrow anything else, and — since release 10 — // COUNT every skip: the first in a quiet period is logged at once, the rest of // its window are reported together when the window ends. function captureWarn(fn: () => T): { result: T; warnings: string[] } { const warnings: string[] = []; const orig = console.warn; console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(" ")); }; try { return { result: fn(), warnings }; } finally { console.warn = orig; } } // A reporter on a hand-driven clock. `advance` moves time and, unless told // not to, fires the end-of-window timer if it came due — like the event loop. function harness(windowMs = 60_000) { let t = 1_000_000; const lines: string[] = []; type Pending = { at: number; fn: () => void; live: boolean }; let pending: Pending | null = null; const reporter = new SkipReporter({ windowMs, now: () => t, warn: (l) => lines.push(l), schedule: (fn, ms) => { const entry: Pending = { at: t + ms, fn, live: true }; pending = entry; return { cancel: () => { entry.live = false; }, }; }, }); const current = (): Pending | null => pending; return { reporter, lines, advance(ms: number, opts: { fire?: boolean } = {}) { t += ms; const p = current(); if (opts.fire !== false && p && p.live && p.at <= t) { pending = null; p.fn(); } }, armed: () => Boolean(current()?.live), }; } test("outside a request, safeRevalidate does not throw and logs the first skip with its paths", () => { resetSafeRevalidateWarning(); const { warnings } = captureWarn(() => { safeRevalidate(["/channels/x", "/channels", ["/operations/[id]", "page"]]); safeRevalidate(["/"]); }); resetSafeRevalidateWarning(); // drop the pending end-of-window report const ours = warnings.filter((w) => w.includes("[safeRevalidate]")); // ONE line for two calls inside one window: the second is counted. assert.equal(ours.length, 1); assert.match(ours[0], /\/channels\/x/); assert.match(ours[0], /\/operations\/\[id\] \(page\)/); assert.match(ours[0], /1 skip\(s\) since this process started/); assert.match(ours[0], /next 10 min are counted and reported together/); }); test("one call is one skip, however many targets it names", () => { const h = harness(); h.reporter.record(["/a", "/b", "tag:c"]); assert.equal(h.lines.length, 1); assert.match(h.lines[0], /skipped revalidating \/a, \/b, tag:c\./); }); test("skips inside the window are counted, and reported once when it ends", () => { const h = harness(60_000); h.reporter.record(["/channels/one"]); h.advance(1_000); for (let i = 0; i < 5; i++) { h.reporter.record([`/channels/n${i}`]); h.advance(1_000); } // Nothing more logged while the window is open: no flooding. assert.equal(h.lines.length, 1); assert.equal(h.armed(), true); h.advance(60_000); assert.equal(h.lines.length, 2); assert.equal( h.lines[1], "[safeRevalidate] 5 more skip(s) with no request store in the last 1 min " + "(latest: /channels/n4); 6 since this process started.", ); }); test("a quiet window reports nothing at its end, and the next skip is logged at once", () => { const h = harness(60_000); h.reporter.record(["/a"]); // No second skip: no timer armed, no end-of-window line. assert.equal(h.armed(), false); h.advance(10 * 60_000); assert.equal(h.lines.length, 1); h.reporter.record(["/b"]); assert.equal(h.lines.length, 2); assert.match( h.lines[1], /skipped revalidating \/b\. .*2 skip\(s\) since this process started/, ); }); test("a skip after the window, before its timer ran, reports the old window first", () => { const h = harness(60_000); h.reporter.record(["/a"]); h.advance(30_000); h.reporter.record(["/b"]); // counted; the report is due at +60 s // The clock passes the window but the timer has not run yet (a busy loop). h.advance(40_000, { fire: false }); h.reporter.record(["/c"]); // a new window: old report first, then this one assert.equal(h.lines.length, 3); assert.equal( h.lines[1], "[safeRevalidate] 1 more skip(s) with no request store in the last 1 min " + "(latest: /b); 2 since this process started.", ); assert.match(h.lines[2], /skipped revalidating \/c\. .*3 skip\(s\) since/); // The old timer was cancelled: it reports nothing when it would have fired. assert.equal(h.armed(), false); }); test("the process-wide reporter is on globalThis, so every module instance shares one", () => { resetSafeRevalidateWarning(); captureWarn(() => safeRevalidate(["/x"])); assert.ok(globalThis.__yttSafeRevalidateSkips__ instanceof SkipReporter); resetSafeRevalidateWarning(); assert.equal(globalThis.__yttSafeRevalidateSkips__, undefined); }); test("the production window is 10 minutes", () => { assert.equal(SKIP_REPORT_WINDOW_MS, 10 * 60 * 1000); }); test("the real revalidatePath does throw here (the premise of the helper)", async () => { const { revalidatePath } = await import("next/cache"); assert.throws( () => revalidatePath("/channels"), /static generation store missing/, ); }); test("any other error is rethrown; a revalidation that runs is not a skip", () => { assert.throws( () => runGuarded(() => { throw new Error("disk on fire"); }), /disk on fire/, ); assert.equal( runGuarded(() => {}), false, ); });