Files
OmniRoute/tests/unit/radar-scheduler.test.ts
Diego Rodrigues de Sa e Souza a1c864373a referrals from standalone /v1/referrals feed (no 30-day delay) (#9762)
* feat(radar): sync referral links from standalone /v1/referrals/latest feed

Referral links previously came from the catalog feed cache, which on the
community tier can be up to 30 days stale -- a newly-added referral would
not reach a free/community user for up to a month. Adds a new sync module
(syncRadarReferrals), Ed25519-verified feed schema, and a dedicated
radar_referrals_cache table (migration 142) so referrals sync on their own,
much shorter cadence instead of inheriting the catalog's delay.

getRadarReferrals()/getDefaultReferralFor() now read the new cache instead
of the catalog feed's embedded referrals field (kept on RadarFeedSchema for
backward-compat with already-cached catalog feeds, but no longer read).

* feat(radar): wire sync-on-read + scheduler side-sync for referrals

GET /api/radar/referrals now triggers syncRadarReferrals() inline whenever
the cache is missing or older than 1h (shouldSyncReferralsOnRead), so fixed
links show up promptly on the next dashboard load instead of waiting on a
background timer. The route itself still never talks to the upstream feed
server directly -- syncRadarReferrals() remains the only network touchpoint.

radarSchedulerTick() also evaluates referrals staleness on the same hourly
tick used for the catalog, independent of the catalog's own due-ness, as a
best-effort side effect that never changes RadarTickResult's shape and is
swallowed on error.

* docs(radar): document the standalone referrals feed sync

Explains the /v1/referrals/latest feed, its no-tier-field-in-body design
(x-omniroute-feed-tier header is the only tier source), the sync-on-read +
scheduler side-sync triggers, and the self-hosting note for forks that only
serve the catalog feed.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 08:10:20 -03:00

235 lines
9.6 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Isolate DATA_DIR before any src import — the scheduler module's default deps
// reference the DB layer (never invoked here: every test injects its deps).
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-radar-scheduler-"));
process.env.DATA_DIR = tmpDir;
const {
radarSchedulerTick,
ensureRadarSyncScheduler,
stopRadarSyncScheduler,
initRadarSyncScheduler,
RADAR_SCHEDULER_TICK_MS,
} = await import("../../src/lib/radar/scheduler.ts");
const NOW = Date.parse("2026-08-06T12:00:00.000Z");
const FRESH = new Date(NOW - 60 * 60 * 1000).toISOString(); // 1h ago — inside the daily window
const STALE = new Date(NOW - 25 * 60 * 60 * 1000).toISOString(); // 25h ago — due
// Referrals staleness window is much shorter (1h, see REFERRALS_STALE_MS) —
// this default must sit well inside it so existing catalog-only subtests
// never trigger a referrals sync as an unasserted side effect.
const REFERRALS_FRESH = new Date(NOW - 5 * 60 * 1000).toISOString(); // 5m ago
const REFERRALS_STALE = new Date(NOW - 2 * 60 * 60 * 1000).toISOString(); // 2h ago — due
/** Fake interval registry so no real timer ever exists in these tests. */
function fakeTimers() {
const registered: Array<{ fn: () => void; ms: number }> = [];
let cleared = 0;
return {
registered,
clearedCount: () => cleared,
setIntervalFn: ((fn: () => void, ms: number) => {
registered.push({ fn, ms });
return registered.length as unknown as ReturnType<typeof setInterval>;
}) as typeof setInterval,
clearIntervalFn: (() => {
cleared += 1;
}) as typeof clearInterval,
};
}
function deps(overrides: Record<string, unknown> = {}) {
const syncCalls: number[] = [];
const referralsSyncCalls: number[] = [];
const timers = fakeTimers();
return {
syncCalls,
referralsSyncCalls,
timers,
d: {
getFlag: () => true,
getSettings: () => ({ optIn: true }),
getCache: () => ({ fetchedAt: STALE }),
sync: async () => {
syncCalls.push(1);
return { status: "updated", version: "2026.08.06.1", tier: "live" } as const;
},
// Referrals side-sync — separate cache/sync from the catalog above.
// Defaults to a FRESH referrals cache so existing subtests (which
// don't care about referrals at all) never trigger a referrals sync
// as an unasserted side effect.
getReferralsCache: () => ({ fetchedAt: REFERRALS_FRESH }),
syncReferrals: async () => {
referralsSyncCalls.push(1);
return { status: "updated", generatedAt: "2026-08-06T12:00:00.000Z", tier: "live" } as const;
},
now: () => NOW,
setIntervalFn: timers.setIntervalFn,
clearIntervalFn: timers.clearIntervalFn,
...overrides,
},
};
}
test("radar sync scheduler", async (t) => {
t.afterEach(() => {
// Module-level timer state must not leak between subtests.
stopRadarSyncScheduler({ clearIntervalFn: (() => {}) as typeof clearInterval });
});
await t.test("tick: flag off => stopped, sync never called", async () => {
const { d, syncCalls } = deps({ getFlag: () => false });
const result = await radarSchedulerTick(d);
assert.deepEqual(result, { action: "stopped", reason: "flag_off" });
assert.equal(syncCalls.length, 0);
});
await t.test("tick: flag off stops a running timer (self-heal to zero-timer state)", async () => {
const { d, timers } = deps();
assert.equal(ensureRadarSyncScheduler(d), true);
assert.equal(timers.registered.length, 1);
const offDeps = { ...d, getFlag: () => false };
await radarSchedulerTick(offDeps);
assert.equal(timers.clearedCount(), 1);
});
await t.test("tick: opt-in off => skipped, no sync", async () => {
const { d, syncCalls } = deps({ getSettings: () => ({ optIn: false }) });
const result = await radarSchedulerTick(d);
assert.deepEqual(result, { action: "skipped", reason: "opt_out" });
assert.equal(syncCalls.length, 0);
});
await t.test("tick: fresh cache => not due, no sync", async () => {
const { d, syncCalls } = deps({ getCache: () => ({ fetchedAt: FRESH }) });
const result = await radarSchedulerTick(d);
assert.deepEqual(result, { action: "skipped", reason: "not_due" });
assert.equal(syncCalls.length, 0);
});
await t.test("tick: no cache at all => syncs immediately", async () => {
const { d, syncCalls } = deps({ getCache: () => null });
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced");
assert.equal(syncCalls.length, 1);
});
await t.test("tick: stale cache (>24h) => syncs", async () => {
const { d, syncCalls } = deps();
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced");
assert.equal(syncCalls.length, 1);
});
await t.test("ensure: registers one hourly timer, fires an immediate tick, idempotent", async () => {
const { d, timers, syncCalls } = deps();
assert.equal(ensureRadarSyncScheduler(d), true);
assert.equal(timers.registered.length, 1);
assert.equal(timers.registered[0].ms, RADAR_SCHEDULER_TICK_MS);
// The immediate tick is fire-and-forget; give the microtask queue a turn.
await new Promise((resolve) => setImmediate(resolve));
assert.equal(syncCalls.length, 1, "immediate tick should have synced the stale cache");
// Second ensure is a no-op — no second timer.
assert.equal(ensureRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 1);
});
await t.test("init: flag off => never arms (flag-off boot stays timer-free)", () => {
const { d, timers } = deps({ getFlag: () => false });
assert.equal(initRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 0);
});
await t.test("init: opt-in off => never arms", () => {
const { d, timers } = deps({ getSettings: () => ({ optIn: false }) });
assert.equal(initRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 0);
});
await t.test("init: flag + opt-in on => arms the timer", () => {
const { d, timers } = deps();
assert.equal(initRadarSyncScheduler(d), true);
assert.equal(timers.registered.length, 1);
});
await t.test("init: settings reader throwing => false, never throws out", () => {
const { d, timers } = deps({
getSettings: () => {
throw new Error("db unavailable");
},
});
assert.equal(initRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 0);
});
// -------------------------------------------------------------------------
// Referrals side-sync — piggybacks on the same hourly tick but its own
// (much shorter, 1h) staleness window, independent of the catalog's
// due-ness. Never surfaces in RadarTickResult (fire-and-await side effect
// only) so the catalog-sync result shape/assertions above stay unchanged.
// -------------------------------------------------------------------------
await t.test("tick: referrals cache fresh => referrals sync NOT called (catalog path unaffected)", async () => {
const { d, syncCalls, referralsSyncCalls } = deps();
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced", "catalog was due and must still sync as before");
assert.equal(syncCalls.length, 1);
assert.equal(referralsSyncCalls.length, 0, "referrals cache was fresh — must not sync");
});
await t.test("tick: referrals cache stale => referrals sync called, independent of catalog due-ness", async () => {
const { d, syncCalls, referralsSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }), // catalog NOT due
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }), // referrals due
});
const result = await radarSchedulerTick(d);
assert.deepEqual(result, { action: "skipped", reason: "not_due" }, "catalog result shape must stay unchanged");
assert.equal(syncCalls.length, 0, "catalog must not sync — it was not due");
assert.equal(referralsSyncCalls.length, 1, "referrals were due and must sync independently");
});
await t.test("tick: referrals cache missing => referrals sync called (missing counts as stale)", async () => {
const { d, referralsSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }),
getReferralsCache: () => null,
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 1);
});
await t.test("tick: flag off => referrals sync NOT called (stopped before any sync check)", async () => {
const { d, referralsSyncCalls } = deps({
getFlag: () => false,
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
});
await t.test("tick: opt-in off => referrals sync NOT called (skipped before any sync check)", async () => {
const { d, referralsSyncCalls } = deps({
getSettings: () => ({ optIn: false }),
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
});
await t.test("tick: referrals sync throwing => swallowed, catalog tick still completes normally", async () => {
const { d, syncCalls } = deps({
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }),
syncReferrals: async () => {
throw new Error("referrals upstream exploded");
},
});
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced", "a throwing referrals sync must never break the catalog tick");
assert.equal(syncCalls.length, 1);
});
});