diff --git a/e2e/today-just-in.spec.ts b/e2e/today-just-in.spec.ts
new file mode 100644
index 000000000..49e00ba81
--- /dev/null
+++ b/e2e/today-just-in.spec.ts
@@ -0,0 +1,145 @@
+import { expect, test } from "@playwright/test";
+import pg from "pg";
+
+import { E2E_USER, STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * v1.31.0 — Today reacts in place.
+ *
+ * The milestone's actual moment: new data lands, and the ALREADY-OPEN
+ * dashboard starts reading differently on the cadence it already runs. No new
+ * poll, no push, no socket — a state change, not a notification.
+ *
+ * The spec drives that end-to-end against the real read path:
+ *
+ * 1. load the dashboard with nothing fresh — no chip, day is provisional;
+ * 2. land last night's sleep through the REAL batch endpoint, and the
+ * arrival marker the spine's worker would have written;
+ * 3. bring the tab back to focus — which is one of the three refetch
+ * triggers the digest hook ALREADY declares (`refetchOnWindowFocus`,
+ * alongside `staleTime` and the 120 s foreground poll). Using the focus
+ * trigger keeps the test inside the real cadence while staying well
+ * under the poll interval, which no test timeout would survive;
+ * 4. assert the chip appeared and the phase flipped — in place, no reload.
+ *
+ * The marker is seeded directly rather than waited for from pg-boss: whether
+ * the queue drains inside the e2e web server is the SPINE's contract and has
+ * its own tests. What this spec owns is everything downstream of a marker
+ * existing — the read path, the DTO, and the surface.
+ *
+ * Assertions anchor on `data-slot` / `data-phase`, never on visible text:
+ * responsive `sm:hidden` classes have broken `getByText` in this repo before.
+ */
+test.use({ storageState: STORAGE_STATE_PATH });
+
+/** The user's local day, in the same profile-tz space the digest files under. */
+function localDayKey(at: Date, timeZone: string): string {
+ // en-CA renders ISO-ordered YYYY-MM-DD, which is exactly the key shape.
+ return new Intl.DateTimeFormat("en-CA", {
+ timeZone,
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(at);
+}
+
+test("a fresh arrival surfaces the just-in chip and flips the day in place", async ({
+ page,
+}) => {
+ const dbUrl = process.env.DATABASE_URL;
+ test.skip(!dbUrl, "DATABASE_URL is required to seed the arrival marker");
+
+ const pool = new pg.Pool({ connectionString: dbUrl });
+
+ try {
+ const { rows } = await pool.query<{ id: string; timezone: string | null }>(
+ "SELECT id, timezone FROM users WHERE username = $1",
+ [E2E_USER.username],
+ );
+ expect(rows.length, "seeded e2e user must exist").toBe(1);
+ const userId = rows[0].id;
+ const timezone = rows[0].timezone ?? "UTC";
+
+ // A clean slate: no marker, and last night's sleep not yet in. Both are
+ // preconditions for the "before" assertion to mean anything.
+ await pool.query("DELETE FROM arrival_reactions WHERE user_id = $1", [
+ userId,
+ ]);
+ await pool.query(
+ "UPDATE users SET morning_digest_refreshed_on = NULL WHERE id = $1",
+ [userId],
+ );
+
+ // ── BEFORE ────────────────────────────────────────────────────────────
+ await page.goto("/");
+ const hero = page.locator('[data-slot="today-hero"]');
+ await expect(hero).toBeVisible({ timeout: 20_000 });
+ await expect(hero.locator('[data-slot="today-hero-just-in"]')).toHaveCount(
+ 0,
+ );
+
+ // ── NEW DATA LANDS ────────────────────────────────────────────────────
+ // Last night's sleep through the real ingest route, on the session the
+ // storage state already carries.
+ const now = new Date();
+ const wokeAt = new Date(now.getTime() - 60 * 60_000);
+
+ const batch = await page.request.post("/api/measurements/batch", {
+ data: {
+ measurements: [
+ {
+ type: "SLEEP_DURATION",
+ value: 7,
+ unit: "h",
+ measuredAt: wokeAt.toISOString(),
+ externalId: `e2e-just-in-${now.getTime()}`,
+ source: "manual",
+ },
+ ],
+ },
+ });
+ expect(
+ batch.ok(),
+ `sleep batch must be accepted (got ${batch.status()})`,
+ ).toBe(true);
+
+ // The marker the spine's worker writes on a salient arrival, plus the
+ // morning-refresh stamp it rides alongside — the two rows that together
+ // make the day final and the arrival news.
+ const localDate = localDayKey(now, timezone);
+ await pool.query(
+ `INSERT INTO arrival_reactions
+ (id, user_id, kind, local_date, occurred_at, created_at)
+ VALUES ($1, $2, 'sleep_night', $3, $4, NOW())
+ ON CONFLICT (user_id, kind, local_date) DO UPDATE
+ SET occurred_at = EXCLUDED.occurred_at`,
+ [`c${now.getTime()}justin000000`, userId, localDate, wokeAt],
+ );
+ await pool.query(
+ "UPDATE users SET morning_digest_refreshed_on = $2 WHERE id = $1",
+ [userId, localDate],
+ );
+
+ // ── THE MOMENT ────────────────────────────────────────────────────────
+ // The tab regains focus — one of the digest hook's existing refetch
+ // triggers. Nothing new is introduced here and the page never reloads.
+ await page.evaluate(() => window.dispatchEvent(new Event("focus")));
+
+ await expect(hero.locator('[data-slot="today-hero-just-in"]')).toBeVisible({
+ timeout: 20_000,
+ });
+ await expect(hero).toHaveAttribute("data-phase", "final", {
+ timeout: 20_000,
+ });
+ // Sleep is in, so the provisional freshness note is gone with it.
+ await expect(
+ hero.locator('[data-slot="today-hero-sleep-pending"]'),
+ ).toHaveCount(0);
+ // The chip names the arrival kind — the stable attribute, not the copy.
+ await expect(
+ hero.locator('[data-slot="today-hero-just-in"]'),
+ ).toHaveAttribute("data-just-in-kind", "sleep_night");
+ } finally {
+ await pool.end();
+ }
+});
diff --git a/src/components/daily/__tests__/today-hero-just-in.test.tsx b/src/components/daily/__tests__/today-hero-just-in.test.tsx
new file mode 100644
index 000000000..afb978cb1
--- /dev/null
+++ b/src/components/daily/__tests__/today-hero-just-in.test.tsx
@@ -0,0 +1,202 @@
+/**
+ * The Today hero's "just in" chip and the reaction line's lead replacement.
+ *
+ * Two invariants, both of which have a specific failure this project has
+ * already paid for:
+ *
+ * 1. HYDRATION. The hero is deliberately NOT mount-gated — it paints from the
+ * server-dehydrated digest on the SSR pass because it is the LCP element
+ * (v1.30.9). So anything inside it that differs between the server and the
+ * browser is a React #418 mismatch. A wall-clock time is exactly that: the
+ * server's locale and timezone are not the reader's. The chip therefore
+ * renders its SLOT on both passes and fills the time in only after mount.
+ * These tests render through `renderToStaticMarkup`, which takes the SERVER
+ * snapshot of `useSyncExternalStore` — i.e. precisely the pass that must
+ * carry no formatted time.
+ *
+ * 2. NO LAYOUT SHIFT. The reaction line REPLACES the lead; it is never a second
+ * paragraph. The hero stays one lead line tall, so nothing below it moves
+ * when the line arrives on a poll.
+ */
+import { describe, it, expect } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { TodayHero } from "../today-hero";
+import type { DailyDigest } from "@/lib/daily/digest";
+
+function render(node: React.ReactNode, locale: "en" | "de" = "en") {
+ const client = new QueryClient();
+ return renderToStaticMarkup(
+
+ {node}
+ ,
+ );
+}
+
+const ARRIVED_AT = "2026-07-16T05:41:00.000Z";
+
+function digest(over: Partial = {}): DailyDigest {
+ return {
+ generatedAt: "2026-07-16T06:00:00.000Z",
+ phase: "final",
+ sleepPending: false,
+ score: { value: 82, band: "green", delta: 3 },
+ topSignal: null,
+ briefingLead: "Your week is trending steady.",
+ line: "Your week is trending steady.",
+ worthALook: [],
+ justIn: null,
+ reactionLine: null,
+ ...over,
+ };
+}
+
+/** Any hh:mm clock face — the thing the server must never emit. */
+const CLOCK_FACE = /\d{1,2}:\d{2}/;
+
+describe("TodayHero — the just-in chip", () => {
+ it("renders no chip when nothing landed", () => {
+ const html = render();
+ expect(html).not.toContain('data-slot="today-hero-just-in"');
+ });
+
+ it("renders the chip slot on the server pass, carrying the kind", () => {
+ const html = render(
+ ,
+ );
+
+ // The slot is present on the SSR pass, so the hydration render — which
+ // takes the same server snapshot — produces identical HTML.
+ expect(html).toContain('data-slot="today-hero-just-in"');
+ expect(html).toContain('data-just-in-kind="sleep_night"');
+ expect(html).toContain("Just in");
+ });
+
+ it("emits NO formatted time and no raw ISO instant on the server pass", () => {
+ const html = render(
+ ,
+ );
+
+ const chip = html.slice(html.indexOf('data-slot="today-hero-just-in"'));
+ // The mismatch itself: a wall clock the browser would render differently.
+ expect(chip).not.toMatch(CLOCK_FACE);
+ // And the raw instant must not leak either — it is machine data, not copy.
+ expect(html).not.toContain(ARRIVED_AT);
+ });
+
+ it("keeps the chip muted meta — never an accent, never an opacity modifier", () => {
+ const html = render(
+ ,
+ );
+
+ const row = html.slice(
+ 0,
+ html.indexOf('data-slot="today-hero-just-in"') + 400,
+ );
+ expect(row).toContain("text-muted-foreground");
+ expect(row).toContain("text-xs");
+ // UI-STANDARDS §text: alpha on muted text drops below AA contrast.
+ expect(html).not.toContain("text-muted-foreground/");
+ expect(html).not.toContain("text-primary");
+ });
+
+ it("shows the chip alongside a still-pending night", () => {
+ // Legitimately co-occurring: a weight landed this morning while last
+ // night's sleep has not arrived yet.
+ const html = render(
+ ,
+ );
+
+ expect(html).toContain('data-slot="today-hero-sleep-pending"');
+ expect(html).toContain('data-slot="today-hero-just-in"');
+ });
+
+ it("localises the chip", () => {
+ const html = render(
+ ,
+ "de",
+ );
+ expect(html).toContain("Gerade eingetroffen");
+ expect(html).not.toContain("Just in");
+ });
+});
+
+describe("TodayHero — the reaction line replaces the lead", () => {
+ const REACTION = "A solid night, deeper than your recent stretch.";
+
+ it("renders the reaction line instead of the briefing lead", () => {
+ const html = render(
+ ,
+ );
+
+ expect(html).toContain(REACTION);
+ // The lead it replaced must be GONE, not pushed down into a second block.
+ expect(html).not.toContain("Your week is trending steady.");
+ });
+
+ it("stays exactly one lead line tall — no second paragraph, no shift", () => {
+ const html = render(
+ ,
+ );
+
+ const leadSlots = html.split('data-slot="today-hero-lead"').length - 1;
+ expect(leadSlots).toBe(1);
+ });
+
+ it("falls back to the briefing lead when no line was generated", () => {
+ const html = render(
+ ,
+ );
+ expect(html).toContain("Your week is trending steady.");
+ });
+
+ it("still renders for an otherwise-bare account when a line exists", () => {
+ // The one moment this surface exists for must not be swallowed by the
+ // empty-account degrade.
+ const html = render(
+ ,
+ );
+
+ expect(html).toContain('data-slot="today-hero"');
+ expect(html).toContain(REACTION);
+ });
+});
diff --git a/src/lib/daily/__tests__/digest-just-in.test.ts b/src/lib/daily/__tests__/digest-just-in.test.ts
new file mode 100644
index 000000000..54a4f401a
--- /dev/null
+++ b/src/lib/daily/__tests__/digest-just-in.test.ts
@@ -0,0 +1,157 @@
+/**
+ * The "just in" fields of the daily digest.
+ *
+ * Two clocks are under test and they are deliberately different, which is the
+ * whole point of this file: the CHIP expires with the news (three hours), the
+ * LINE stands for the rest of the local day. Collapsing them onto one clock is
+ * the most plausible future regression here — it reads like a simplification
+ * and it silently deletes the day's read at lunchtime.
+ */
+import { describe, it, expect } from "vitest";
+
+import { getServerTranslator } from "@/lib/i18n/server-translator";
+import type { MedsTodayBlock } from "@/lib/dashboard/meds-today";
+import {
+ buildDailyDigest,
+ JUST_IN_WINDOW_MS,
+ type DailyDigestArrival,
+ type DailyDigestInput,
+} from "@/lib/daily/digest";
+
+const t = getServerTranslator("en").t;
+const NOW = new Date("2026-07-16T09:00:00.000Z");
+
+function meds(): MedsTodayBlock {
+ return {
+ activeCount: 0,
+ scheduledToday: 0,
+ takenToday: 0,
+ skippedToday: 0,
+ nextDueAt: null,
+ nextDueOverdue: false,
+ nextDueMedicationName: null,
+ nextDueMedicationId: null,
+ };
+}
+
+function input(over: Partial = {}): DailyDigestInput {
+ return {
+ now: NOW,
+ modules: {},
+ score: { value: 82, band: "good", delta: 3 },
+ briefing: null,
+ medsToday: meds(),
+ sleepLastSeenDaysAgo: 0,
+ morningRefreshedToday: false,
+ syncIssues: [],
+ preventiveDue: [],
+ coachPlans: [],
+ tensionWindow: null,
+ todayLocalDate: "2026-07-16",
+ dismissedItemKeys: new Set(),
+ ...over,
+ };
+}
+
+/** An arrival `agoMs` before NOW. */
+function arrival(
+ agoMs: number,
+ over: Partial = {},
+): DailyDigestArrival {
+ return {
+ kind: "sleep_night",
+ occurredAt: new Date(NOW.getTime() - agoMs),
+ line: null,
+ ...over,
+ };
+}
+
+describe("buildDailyDigest — justIn", () => {
+ it("is null when nothing landed today", () => {
+ const digest = buildDailyDigest(input(), t);
+ expect(digest.justIn).toBeNull();
+ expect(digest.reactionLine).toBeNull();
+ });
+
+ it("names the arrival and carries an ISO instant, never a formatted time", () => {
+ const at = new Date(NOW.getTime() - 60_000);
+ const digest = buildDailyDigest(
+ input({ arrivals: [arrival(60_000, { kind: "weight" })] }),
+ t,
+ );
+
+ expect(digest.justIn).toEqual({ kind: "weight", at: at.toISOString() });
+ // The wire value must stay machine-readable. A server-formatted wall clock
+ // here is the React #418 bug in its exact original shape.
+ expect(digest.justIn?.at).toMatch(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/);
+ });
+
+ it("shows the chip inside the window and drops it after", () => {
+ const inside = buildDailyDigest(
+ input({ arrivals: [arrival(JUST_IN_WINDOW_MS - 60_000)] }),
+ t,
+ );
+ expect(inside.justIn).not.toBeNull();
+
+ const outside = buildDailyDigest(
+ input({ arrivals: [arrival(JUST_IN_WINDOW_MS + 60_000)] }),
+ t,
+ );
+ expect(outside.justIn).toBeNull();
+ });
+
+ it("keeps the reaction line after the chip has expired", () => {
+ // The load-bearing asymmetry: past the window the record is no longer
+ // NEWS, but the sentence is still the day's READ and must survive.
+ const digest = buildDailyDigest(
+ input({
+ arrivals: [
+ arrival(JUST_IN_WINDOW_MS + 60 * 60_000, {
+ line: "A solid night, deeper than your recent stretch.",
+ }),
+ ],
+ }),
+ t,
+ );
+
+ expect(digest.justIn).toBeNull();
+ expect(digest.reactionLine).toBe(
+ "A solid night, deeper than your recent stretch.",
+ );
+ });
+
+ it("picks the newest arrival when several landed", () => {
+ const digest = buildDailyDigest(
+ input({
+ arrivals: [
+ arrival(2 * 60 * 60_000, { kind: "weight", line: "older" }),
+ arrival(10 * 60_000, { kind: "workout", line: "newer" }),
+ arrival(60 * 60_000, { kind: "blood_pressure", line: "middle" }),
+ ],
+ }),
+ t,
+ );
+
+ expect(digest.justIn?.kind).toBe("workout");
+ expect(digest.reactionLine).toBe("newer");
+ });
+
+ it("treats a blank generated line as absent rather than shipping an empty lead", () => {
+ const digest = buildDailyDigest(
+ input({ arrivals: [arrival(60_000, { line: " " })] }),
+ t,
+ );
+
+ expect(digest.justIn).not.toBeNull();
+ expect(digest.reactionLine).toBeNull();
+ });
+
+ it("stays pure — the composer never mutates the arrivals it was handed", () => {
+ const arrivals = [arrival(60_000), arrival(30_000, { kind: "weight" })];
+ const snapshot = JSON.stringify(arrivals);
+
+ buildDailyDigest(input({ arrivals }), t);
+
+ expect(JSON.stringify(arrivals)).toBe(snapshot);
+ });
+});
diff --git a/src/lib/jobs/__tests__/reaction-line-degradation.test.ts b/src/lib/jobs/__tests__/reaction-line-degradation.test.ts
new file mode 100644
index 000000000..b58546842
--- /dev/null
+++ b/src/lib/jobs/__tests__/reaction-line-degradation.test.ts
@@ -0,0 +1,301 @@
+/**
+ * Degradation is the headline, not the AI.
+ *
+ * The milestone's actual moment is the STATE CHANGE: something landed, the
+ * record now reads differently, and the open dashboard shows it. The generated
+ * sentence is garnish on top of that. This file exists to keep those two
+ * things separable forever — because the tempting future refactor is to treat
+ * the line as the feature and let a provider-less install fall off the surface
+ * with it.
+ *
+ * So: for every reason a line can fail to exist — no provider, no consent, an
+ * exhausted budget, a dead provider, an unusable output — the row must stay
+ * line-less WITHOUT spending anything it did not reconcile, and the digest must
+ * still surface the chip. The last assertion of each case is the one that
+ * matters: `justIn` survives.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+
+import { getServerTranslator } from "@/lib/i18n/server-translator";
+import { buildDailyDigest, type DailyDigestInput } from "@/lib/daily/digest";
+
+const findUnique = vi.fn();
+const update = vi.fn();
+const userFindUnique = vi.fn();
+
+vi.mock("@/lib/db", () => ({
+ prisma: {
+ arrivalReaction: {
+ findUnique: (...a: unknown[]) => findUnique(...a),
+ update: (...a: unknown[]) => update(...a),
+ },
+ user: { findUnique: (...a: unknown[]) => userFindUnique(...a) },
+ },
+}));
+
+const resolveProviderChain = vi.fn();
+vi.mock("@/lib/ai/provider", () => ({
+ resolveProviderChain: (...a: unknown[]) => resolveProviderChain(...a),
+}));
+
+const chainRequiresServerManagedConsent = vi.fn();
+const hasActiveConsentForSurface = vi.fn();
+vi.mock("@/lib/ai/consent-guard", () => ({
+ chainRequiresServerManagedConsent: (...a: unknown[]) =>
+ chainRequiresServerManagedConsent(...a),
+ hasActiveConsentForSurface: (...a: unknown[]) =>
+ hasActiveConsentForSurface(...a),
+}));
+
+const reserveBudget = vi.fn();
+const reconcileSpend = vi.fn();
+vi.mock("@/lib/ai/coach/budget", () => ({
+ buildDateKey: () => "2026-07-16",
+ reserveBudget: (...a: unknown[]) => reserveBudget(...a),
+ reconcileSpend: (...a: unknown[]) => reconcileSpend(...a),
+ resolveDailyCap: () => 200_000,
+}));
+
+const loadDailyDigest = vi.fn();
+vi.mock("@/lib/daily/load-digest", () => ({
+ loadDailyDigest: (...a: unknown[]) => loadDailyDigest(...a),
+}));
+
+vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() }));
+vi.mock("@/lib/jobs/reminder/shared", () => ({ workerLog: vi.fn() }));
+vi.mock("@/lib/ai/coach/bytes-codec", () => ({
+ encryptToBytes: (s: string) => new TextEncoder().encode(s),
+}));
+
+import { runReactionLine } from "@/lib/jobs/reaction-line";
+
+const JOB = {
+ userId: "u1",
+ kind: "sleep_night" as const,
+ localDate: "2026-07-16",
+};
+
+const t = getServerTranslator("en").t;
+const NOW = new Date("2026-07-16T09:00:00.000Z");
+
+/**
+ * The digest a user sees when the line was never written. Built from the REAL
+ * composer, not a hand-shaped object, so this asserts the actual degrade path
+ * rather than a fixture that agrees with the test.
+ */
+function degradedDigest() {
+ const input: DailyDigestInput = {
+ now: NOW,
+ modules: {},
+ score: { value: 82, band: "good", delta: 3 },
+ briefing: null,
+ medsToday: {
+ activeCount: 0,
+ scheduledToday: 0,
+ takenToday: 0,
+ skippedToday: 0,
+ nextDueAt: null,
+ nextDueOverdue: false,
+ nextDueMedicationName: null,
+ nextDueMedicationId: null,
+ },
+ sleepLastSeenDaysAgo: 0,
+ morningRefreshedToday: true,
+ syncIssues: [],
+ preventiveDue: [],
+ coachPlans: [],
+ tensionWindow: null,
+ todayLocalDate: "2026-07-16",
+ dismissedItemKeys: new Set(),
+ // The marker exists; the line does not. This is the provider-less shape.
+ arrivals: [
+ {
+ kind: "sleep_night",
+ occurredAt: new Date(NOW.getTime() - 60_000),
+ line: null,
+ },
+ ],
+ };
+ return buildDailyDigest(input, t);
+}
+
+/** Every degrade case must leave the surface fully functional. */
+function expectSurfaceStillWorks() {
+ const digest = degradedDigest();
+ // The chip — the moment itself — is untouched by the missing sentence.
+ expect(digest.justIn).toEqual({
+ kind: "sleep_night",
+ at: new Date(NOW.getTime() - 60_000).toISOString(),
+ });
+ // The provisional→final flip is likewise unaffected.
+ expect(digest.phase).toBe("final");
+ expect(digest.sleepPending).toBe(false);
+ // And the hero still has a lead to render: the deterministic floor.
+ expect(digest.reactionLine).toBeNull();
+ expect(digest.line.length).toBeGreaterThan(0);
+}
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ findUnique.mockResolvedValue({ id: "r1", generatedAt: null });
+ userFindUnique.mockResolvedValue({ id: "u1", locale: "en", timezone: "UTC" });
+ chainRequiresServerManagedConsent.mockReturnValue(false);
+ hasActiveConsentForSurface.mockResolvedValue(true);
+ reserveBudget.mockResolvedValue({
+ allowed: true,
+ reserved: 1_400,
+ totalAfter: 1_400,
+ });
+ reconcileSpend.mockResolvedValue(undefined);
+ loadDailyDigest.mockResolvedValue({
+ score: { value: 82, band: "good", delta: 3 },
+ topSignal: null,
+ briefingLead: "Steady week.",
+ });
+});
+
+describe("reaction line — degradation", () => {
+ it("no provider: writes nothing, spends nothing, surface intact", async () => {
+ resolveProviderChain.mockResolvedValue([]);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "no_provider" });
+ expect(reserveBudget).not.toHaveBeenCalled();
+ expect(update).not.toHaveBeenCalled();
+ expectSurfaceStillWorks();
+ });
+
+ it("no consent on a server-managed chain: refuses before reserving", async () => {
+ resolveProviderChain.mockResolvedValue([
+ { providerType: "admin-openai", instance: {} },
+ ]);
+ chainRequiresServerManagedConsent.mockReturnValue(true);
+ hasActiveConsentForSurface.mockResolvedValue(false);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "consent_required" });
+ // A user without a receipt must not spend a token OR a ledger slot.
+ expect(reserveBudget).not.toHaveBeenCalled();
+ expect(update).not.toHaveBeenCalled();
+ expectSurfaceStillWorks();
+ });
+
+ it("exhausted budget: refuses, writes nothing, surface intact", async () => {
+ resolveProviderChain.mockResolvedValue([
+ { providerType: "openai", instance: {} },
+ ]);
+ reserveBudget.mockResolvedValue({
+ allowed: false,
+ reserved: 1_400,
+ totalAfter: 999_999,
+ });
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "budget_exceeded" });
+ expect(update).not.toHaveBeenCalled();
+ expectSurfaceStillWorks();
+ });
+
+ it("provider throws: reconciles the reservation to zero and degrades", async () => {
+ resolveProviderChain.mockResolvedValue([
+ {
+ providerType: "openai",
+ instance: {
+ generateCompletion: vi.fn().mockRejectedValue(new Error("timeout")),
+ },
+ },
+ ]);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "provider_failed" });
+ // The failure path MUST reconcile — an abandoned reservation is a silent
+ // over-charge against the user's own daily ceiling.
+ expect(reconcileSpend).toHaveBeenCalledWith("u1", 1_400, 0, "2026-07-16");
+ expect(update).not.toHaveBeenCalled();
+ expectSurfaceStillWorks();
+ });
+
+ it("unusable output: reconciles the ACTUAL spend and still writes no line", async () => {
+ resolveProviderChain.mockResolvedValue([
+ {
+ providerType: "openai",
+ instance: {
+ generateCompletion: vi.fn().mockResolvedValue({
+ content: " ",
+ tokensUsed: 900,
+ cachedInputTokens: 0,
+ }),
+ },
+ },
+ ]);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "unusable_output" });
+ // The call happened, so the real tokens are billed — not zero.
+ expect(reconcileSpend).toHaveBeenCalledWith(
+ "u1",
+ 1_400,
+ 900,
+ "2026-07-16",
+ 0,
+ );
+ expect(update).not.toHaveBeenCalled();
+ expectSurfaceStillWorks();
+ });
+
+ it("a committed line is never regenerated — the unique row is the throttle", async () => {
+ findUnique.mockResolvedValue({ id: "r1", generatedAt: new Date() });
+ resolveProviderChain.mockResolvedValue([
+ { providerType: "openai", instance: {} },
+ ]);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "skipped", reason: "already_generated" });
+ // The refusal happens before the chain is even resolved: there is no code
+ // path to a second provider call for this kind today.
+ expect(resolveProviderChain).not.toHaveBeenCalled();
+ expect(reserveBudget).not.toHaveBeenCalled();
+ });
+
+ it("the happy path does write, so the degrade cases above are not vacuous", async () => {
+ resolveProviderChain.mockResolvedValue([
+ {
+ providerType: "openai",
+ instance: {
+ generateCompletion: vi.fn().mockResolvedValue({
+ content: "A solid night, deeper than your recent stretch.",
+ tokensUsed: 1_100,
+ cachedInputTokens: 100,
+ }),
+ },
+ },
+ ]);
+
+ const outcome = await runReactionLine(JOB);
+
+ expect(outcome).toEqual({ status: "generated" });
+ expect(update).toHaveBeenCalledTimes(1);
+ const arg = update.mock.calls[0][0] as {
+ data: { generatedAt: Date; lineEncrypted: Uint8Array };
+ };
+ // Ciphertext and the commit stamp land together — `load-digest` requires
+ // both, so a half-written row can never surface a line.
+ expect(arg.data.generatedAt).toBeInstanceOf(Date);
+ expect(new TextDecoder().decode(arg.data.lineEncrypted)).toBe(
+ "A solid night, deeper than your recent stretch.",
+ );
+ expect(reconcileSpend).toHaveBeenCalledWith(
+ "u1",
+ 1_400,
+ 1_100,
+ "2026-07-16",
+ 100,
+ );
+ });
+});