Files
OmniRoute/tests/unit/provider-header-referral-link.test.ts
Diego Rodrigues de Sa e Souza d86ea99713 feat(radar): referral links — free-credits tab + default provider link (#9697)
* feat(radar): client-side schema + accessor for referral links (D28)

Server already publishes a signed `referrals` section on the Radar feed
({fixed, campaigns}); this adds the client mirror: RadarFeedSchema gains a
`.default()`-backed `referrals` field (old cached feeds without it stay
valid) with https-only url validation, and src/lib/radar/index.ts exposes
getRadarReferrals()/getDefaultReferralFor() (never throw: flag off, no
cache, or a corrupt/old payload all resolve to the empty shape). The
provider-default lookup itself lives in a new DB-free src/lib/radar/
referrals.ts so it stays safe to import from a "use client" component.

* feat(radar): add GET /api/radar/referrals route (D28)

Local-only route mirroring the /api/radar/catalog gate order: RADAR_ENABLED
off => 404 before any auth check (byte-identical flag-off inertia),
unauthenticated => 401, otherwise 200 with {fixed, campaigns, tier} read
straight from the local cache. Never proxies the private feed server.

* feat(dashboard): add "free credits" tab to the Radar page (D28)

Reuses the existing /dashboard/radar page instead of a new route (less
routing/i18n surface): a second tab lists fixed referral links (grouped by
provider, with requiredAction + an external-link button) and temporary
campaigns (with validUntil). When campaigns is empty and the served tier is
community, shows a soft upsell note — never gates the fixed links list,
which stays fully populated on every tier. Adds 10 new radarPage i18n keys
(English fallback) to all 43 locale files to avoid dropping i18n-ui-coverage
below threshold.

* feat(providers): use Radar default referral link on the provider name (D28)

ProviderPageHeader already linked the provider name to providerInfo.website
with a precedent for a monetized link (the Kimi partner-link note); this
lets a Radar default referral override that URL, reusing the exact same
discreet note instead of a new visual treatment.

Loose coupling: resolveProviderHeaderLink() in providerPageUtils.ts is a
pure function with no @/lib/radar or @/lib/db/* import (asserted by the new
test), so the providers dashboard never depends on the DB-touching Radar
module to render. ProviderDetailPageClient (a "use client" component) is
the only place that fetches Radar data, via the local /api/radar/referrals
route (same pattern the Radar page itself uses) and the DB-free
findDefaultReferral() helper. With RADAR_ENABLED off, no cache, or no
default referral for the provider, the header renders byte-identical to
before this feature existed.

* docs(radar): document referral links / free credits (D28)

Adds a "Referral links (free credits)" section covering the referrals feed
shape, the getRadarReferrals()/getDefaultReferralFor() accessors, the new
GET /api/radar/referrals route, the Radar page's "Free credits" tab, and
the loosely-coupled referral link on the provider-name header. Also
corrects the local-routes count (four -> five) now that /api/radar/
referrals exists.

---------

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

85 lines
3.7 KiB
TypeScript

/**
* tests/unit/provider-header-referral-link.test.ts
*
* TDD regression guard for the "referral link on the provider name" part of
* D28 (referral links / free credits). Covers the pure decision function
* `resolveProviderHeaderLink` used by ProviderDetailPageClient to decide
* whether the provider-name link (rendered by ProviderPageHeader) points at
* the static catalog `website` or at a Radar default referral.
*
* Loose-coupling requirements from the spec:
* (a) RADAR_ENABLED off => byte-identical to today (covered indirectly:
* when off, the caller never resolves a referralUrl, so this receives
* null/undefined and returns the static website unchanged).
* (b) No cache / no referral for the provider => same as (a).
* (c) This file (providerPageUtils.ts) imports NO @/lib/radar or
* @/lib/db/* symbol — asserted by source inspection so a future edit
* cannot silently reintroduce a DB-touching import into a module a
* "use client" component depends on.
* (d) When a referral applies, isReferralLink=true (the header uses this to
* show a discreet note, reusing the existing `kimiPartnerLinkNote` key).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { resolveProviderHeaderLink } from "../../src/app/(dashboard)/dashboard/providers/providerPageUtils.ts";
test("resolveProviderHeaderLink: no referral => keeps the static website, isReferralLink=false", () => {
const result = resolveProviderHeaderLink("https://groq.com", null);
assert.deepEqual(result, { website: "https://groq.com", isReferralLink: false });
});
test("resolveProviderHeaderLink: no referral, no static website => website undefined (today's no-link case)", () => {
const result = resolveProviderHeaderLink(undefined, null);
assert.deepEqual(result, { website: undefined, isReferralLink: false });
});
test("resolveProviderHeaderLink: referral present => overrides the static website, isReferralLink=true", () => {
const result = resolveProviderHeaderLink("https://groq.com", "https://groq.com/?ref=omniroute");
assert.deepEqual(result, {
website: "https://groq.com/?ref=omniroute",
isReferralLink: true,
});
});
test("resolveProviderHeaderLink: referral present even when catalog has no static website => still links out", () => {
const result = resolveProviderHeaderLink(undefined, "https://groq.com/?ref=omniroute");
assert.deepEqual(result, {
website: "https://groq.com/?ref=omniroute",
isReferralLink: true,
});
});
test("resolveProviderHeaderLink: empty-string referral is treated as absent (falsy)", () => {
const result = resolveProviderHeaderLink("https://groq.com", "");
assert.deepEqual(result, { website: "https://groq.com", isReferralLink: false });
});
// ---------------------------------------------------------------------------
// Loose coupling — providerPageUtils.ts must not depend on the Radar/DB
// modules to compute this (requirement (c) — see file docblock above).
// ---------------------------------------------------------------------------
test("providerPageUtils.ts does not import @/lib/radar or @/lib/db/* (loose coupling — D28)", () => {
const src = fs.readFileSync(
path.resolve(
process.cwd(),
"src/app/(dashboard)/dashboard/providers/providerPageUtils.ts"
),
"utf-8"
);
const importLines = src
.split("\n")
.filter((line) => /^\s*import\b/.test(line));
assert.ok(
!importLines.some((line) => line.includes("@/lib/radar")),
"providerPageUtils.ts must not import @/lib/radar"
);
assert.ok(
!importLines.some((line) => line.includes("@/lib/db/")),
"providerPageUtils.ts must not import @/lib/db/*"
);
});