diff --git a/src/app/api/withings/status/route.ts b/src/app/api/withings/status/route.ts index 8b45d8e02..41f6ea1b6 100644 --- a/src/app/api/withings/status/route.ts +++ b/src/app/api/withings/status/route.ts @@ -5,6 +5,7 @@ import { apiSuccess } from "@/lib/api-response"; import { decrypt, encrypt } from "@/lib/crypto"; import { getUserWithingsCredentials } from "@/lib/withings/credentials"; import { refreshAccessToken } from "@/lib/withings/client"; +import { isWithingsRefreshReauthFailure } from "@/lib/withings/sync"; import { markReauthRequired } from "@/lib/integrations/status"; /** @@ -80,17 +81,7 @@ export const GET = apiHandler(async () => { // the user reconnects. We mirror the same status-code parsing // the sync path does. const message = error instanceof Error ? error.message : String(error); - const statusMatch = /Withings\s+\w+\s+error:\s*(\d+)/.exec(message); - const statusCode = statusMatch - ? Number.parseInt(statusMatch[1], 10) - : NaN; - const isReauth = - Number.isFinite(statusCode) && - (statusCode === 100 || - statusCode === 101 || - statusCode === 102 || - (statusCode >= 200 && statusCode <= 299)); - if (isReauth) { + if (isWithingsRefreshReauthFailure(message)) { await markReauthRequired(user.id, "withings", message).catch(() => {}); } } diff --git a/src/components/settings/notification-status-card.tsx b/src/components/settings/notification-status-card.tsx index 1fbceda47..e1f0d6314 100644 --- a/src/components/settings/notification-status-card.tsx +++ b/src/components/settings/notification-status-card.tsx @@ -327,7 +327,6 @@ function stateBadgeFor( icon: , }; case "manually_disabled": - default: return { label: t("settings.notificationStatus.stateManuallyDisabled"), variant: "secondary", diff --git a/src/lib/ai/__tests__/codex-slug-cache.test.ts b/src/lib/ai/__tests__/codex-slug-cache.test.ts index 82f2b840d..d3070b937 100644 --- a/src/lib/ai/__tests__/codex-slug-cache.test.ts +++ b/src/lib/ai/__tests__/codex-slug-cache.test.ts @@ -4,7 +4,6 @@ import { setCachedCodexSlug, invalidateCachedCodexSlug, clearCodexSlugCache, - inspectCodexSlugCache, CODEX_SLUG_CACHE_TTL_MS, } from "../codex-slug-cache"; @@ -19,7 +18,6 @@ describe("Codex slug cache", () => { it("starts empty", () => { expect(getCachedCodexSlug()).toBeNull(); - expect(inspectCodexSlugCache()).toBeNull(); }); it("set + get returns the cached slug within TTL", () => { @@ -43,8 +41,8 @@ describe("Codex slug cache", () => { it("expired entry is evicted on read", () => { setCachedCodexSlug("gpt-5.3-codex", 1_000); getCachedCodexSlug(1_000 + CODEX_SLUG_CACHE_TTL_MS + 1); - // Subsequent inspect should be null too. - expect(inspectCodexSlugCache(1_000 + CODEX_SLUG_CACHE_TTL_MS + 1)).toBeNull(); + // Subsequent get should be null too (no resurrection). + expect(getCachedCodexSlug(1_000 + CODEX_SLUG_CACHE_TTL_MS + 1)).toBeNull(); }); it("invalidate clears the slot", () => { @@ -58,10 +56,4 @@ describe("Codex slug cache", () => { setCachedCodexSlug("b", 2_000); expect(getCachedCodexSlug(2_500)).toBe("b"); }); - - it("inspect reports age in ms", () => { - setCachedCodexSlug("gpt-5.3-codex", 1_000); - const inspected = inspectCodexSlugCache(1_500); - expect(inspected).toEqual({ slug: "gpt-5.3-codex", ageMs: 500 }); - }); }); diff --git a/src/lib/ai/__tests__/codex-slug-fallback.test.ts b/src/lib/ai/__tests__/codex-slug-fallback.test.ts index f2d79892b..1005d8750 100644 --- a/src/lib/ai/__tests__/codex-slug-fallback.test.ts +++ b/src/lib/ai/__tests__/codex-slug-fallback.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { CodexClient, __test } from "../codex-client"; import { clearCodexSlugCache, - inspectCodexSlugCache, + getCachedCodexSlug, setCachedCodexSlug, CODEX_SLUG_CACHE_TTL_MS, } from "../codex-slug-cache"; @@ -105,7 +105,7 @@ describe("Codex slug fallback chain", () => { expect(diagnostics?.cacheState).toBe("miss"); expect(diagnostics?.workingSlug).toBe("gpt-5.3-codex"); - expect(inspectCodexSlugCache()?.slug).toBe("gpt-5.3-codex"); + expect(getCachedCodexSlug()).toBe("gpt-5.3-codex"); }); it("walks past first slug on 'not supported when using Codex with a ChatGPT account' 400", async () => { @@ -134,7 +134,7 @@ describe("Codex slug fallback chain", () => { expect(diagnostics?.workingSlug).toBe("gpt-5.3-codex"); // The working slug — not the rejected one — is now cached. - expect(inspectCodexSlugCache()?.slug).toBe("gpt-5.3-codex"); + expect(getCachedCodexSlug()).toBe("gpt-5.3-codex"); }); it("walks past first slug on 400 with model_not_found body", async () => { @@ -302,7 +302,7 @@ describe("Codex slug fallback chain", () => { it("cache invalidated when cached slug starts rejecting", async () => { setCachedCodexSlug("gpt-5-codex"); - expect(inspectCodexSlugCache()?.slug).toBe("gpt-5-codex"); + expect(getCachedCodexSlug()).toBe("gpt-5-codex"); const mockFetch = vi .fn() @@ -320,7 +320,7 @@ describe("Codex slug fallback chain", () => { // After walking to "gpt-5.3-codex" (the next entry after the // cached one), the cache is updated to the new working slug. - expect(inspectCodexSlugCache()?.slug).toBe("gpt-5.3-codex"); + expect(getCachedCodexSlug()).toBe("gpt-5.3-codex"); }); it("isSlugRejection helper recognises canonical rejection bodies", () => { diff --git a/src/lib/ai/codex-slug-cache.ts b/src/lib/ai/codex-slug-cache.ts index 0b62c9bd1..9b1ef3e16 100644 --- a/src/lib/ai/codex-slug-cache.ts +++ b/src/lib/ai/codex-slug-cache.ts @@ -59,16 +59,4 @@ export function clearCodexSlugCache(): void { cache.clear(); } -/** - * Diagnostic helper — exposes (slug, ageMs) for Wide-Event annotation. - * Returns null when nothing is cached. - */ -export function inspectCodexSlugCache( - now: number = Date.now(), -): { slug: string; ageMs: number } | null { - const entry = cache.get(CACHE_KEY); - if (!entry) return null; - return { slug: entry.slug, ageMs: now - entry.cachedAtMs }; -} - export const CODEX_SLUG_CACHE_TTL_MS = CACHE_TTL_MS; diff --git a/src/lib/gamification/achievements.ts b/src/lib/gamification/achievements.ts index f2924034c..fbf13ee39 100644 --- a/src/lib/gamification/achievements.ts +++ b/src/lib/gamification/achievements.ts @@ -41,11 +41,6 @@ export interface AchievementDefinition { format: "count" | "days" | "percent"; } -/** - * Stable metric → category mapping. Add a metric here when it joins the - * `AchievementMetricKey` union; the page falls back to "engagement" if - * a future definition slips through, so there is no silent drop-off. - */ export function getAchievementCategory( metric: AchievementMetricKey, ): AchievementCategory { diff --git a/src/lib/notifications/channel-state.ts b/src/lib/notifications/channel-state.ts index 25515c036..62e7cc937 100644 --- a/src/lib/notifications/channel-state.ts +++ b/src/lib/notifications/channel-state.ts @@ -8,7 +8,6 @@ import { prisma } from "@/lib/db"; import { auditLog } from "@/lib/auth/audit"; import { - MAX_CONSECUTIVE_FAILURES, nextRetryAt, shouldAutoDisableAfterTransient, type SendOutcome, @@ -167,5 +166,3 @@ export function isChannelInCooldown( ): boolean { return channel.nextRetryAt !== null && channel.nextRetryAt > now; } - -export const TEST_CONSTANTS = { MAX_CONSECUTIVE_FAILURES } as const; diff --git a/src/lib/notifications/retry-policy.ts b/src/lib/notifications/retry-policy.ts index 9a6d960a7..792afcd40 100644 --- a/src/lib/notifications/retry-policy.ts +++ b/src/lib/notifications/retry-policy.ts @@ -25,8 +25,6 @@ * response that doesn't match the hard-reject rules. */ -export type RejectKind = "hard" | "soft" | "ok"; - export interface SendOutcome { ok: boolean; /** True iff the upstream signaled a permanent failure (410, blocked-by-user, …). */ diff --git a/src/lib/withings/sync.ts b/src/lib/withings/sync.ts index 4de206589..5b1a7ce3a 100644 --- a/src/lib/withings/sync.ts +++ b/src/lib/withings/sync.ts @@ -237,7 +237,7 @@ export async function syncUserMeasurements( * The error message format from `refreshAccessToken()` is * "Withings refresh error: - ". */ -function isWithingsRefreshReauthFailure(message: string): boolean { +export function isWithingsRefreshReauthFailure(message: string): boolean { const status = extractWithingsStatus(message); if (!status) return false; const n = Number.parseInt(status, 10); @@ -247,7 +247,7 @@ function isWithingsRefreshReauthFailure(message: string): boolean { return false; } -function extractWithingsStatus(message: string): string | undefined { +export function extractWithingsStatus(message: string): string | undefined { // Captures the digit run after "Withings error:" — same shape // exchangeCode and refreshAccessToken use. const m = /Withings\s+\w+\s+error:\s*(\d+)/.exec(message);