mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
* 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>
85 lines
3.7 KiB
TypeScript
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/*"
|
|
);
|
|
});
|