# PR #22993 (GitCafe #22990) widget fixture matrix (proof audit, pass 1)

- Head under test: `d05935e9f9` (origin/main `8d3b061121` merged in). Before = `origin/main:apps/mobile/src/widgets/subscriptionUsageSnapshot.ts`. After = head.
- Frozen clock: **T = 2026-10-21T16:45:00Z**. All offsets below are minutes from T. `checkedAt` = the account's `usageLimits.checkedAt`.
- The before/after columns were **computed, not inferred**. A probe (`/tmp/ev-22993/probe/probe.ts`, run with bun) runs both module versions on each fixture and re-implements the render logic of `SubscriptionUsage.android.tsx` (`limit = 2`, `stale = windows>0 && expiresAt>0 && Date.now() >= expiresAt`) at `Date.now() = T`. The "after +16m" column is the same render at T+16m.
- The head unit tests pass: `vp test run src/widgets/subscriptionUsageSnapshot.test.ts` → 14/14.
- `buildSubscriptionUsageSnapshot` passes `now = 0` into `subscriptionUsageProps`. That means `fresh` is always true at build time, and expiry is decided by the widget clock alone: Android reads `Date.now()`, iOS reads the timeline entry date.

## Matrix

Codex windows are `5 hours` (session, 0% used) and `Weekly` (30% used) unless noted. "Open T3 to refresh" = expired.

| Row | Input (resets relative to T) | Before (main) | After (PR) | After at T+16m | Seed on Android |
|---|---|---|---|---|---|
| **F1 not yet reset** | 1 Codex acct, checkedAt T; session +45m, weekly +7d | exp T+15m; `Weekly 70% · Next reset`, `5 hours 100% · Next reset` | identical | expired | fixture `F1` |
| **F2 window already reset** (issue #22973) | checkedAt T; session **−15m**, weekly +7d | **exp T−15m → "Open T3 to refresh" immediately** | exp T+15m; `Weekly 70% · Next reset`, `5 hours 100% · **Reset** <time>` | expired | fixture `F2` |
| **F3 all windows reset** | checkedAt T; session −15m, weekly −1m | **expired immediately** | exp T+15m; both rows labelled `Reset <time>` | expired | fixture `F3` |
| F4 reset == checkedAt (boundary) | checkedAt T; session **0m**, weekly +7d | **expired immediately** (exp = T) | exp T+15m; session `Reset`, weekly `Next reset` | expired | fixture `F4` |
| F5 future reset < 15 min | checkedAt T; session +5m | exp T+5m; both `Next reset` | identical (exp T+5m) | expired (flips at +5m via the `T3WidgetExpiry` alarm) | fixture `F5` |
| F6 snapshot older than 15 min | checkedAt **T−16m**; session +45m | exp T−1m → expired | identical | expired | fixture `F6` |
| **F7a pooled, reset on newer acct** | 2 Codex accts (a@ checkedAt T−5m, session +45m; b@ checkedAt T, session −3m); weekly +7d | **expired immediately** (exp T−3m) | exp T+10m (oldest checkedAt + 15m); detail `2 accounts · pooled`; session `Reset` (earliest reset is b's past one) | expired | fixture `F7a` (unit test covers the pooled case too) |
| F7b pooled, reset on older acct | a@ checkedAt T−5m, session −3m (future when observed); b@ checkedAt T, session +45m | expired (exp T−3m) | **identical: still expired**. This is correct, because the reset happened after a@'s reading | expired | fixture `F7b` |
| **F8 long labels + >2 windows** (Android `limit = 2`) | 1 Claude acct, checkedAt T; Session 10% used, reset −20m; Weekly 50% +6d; `Weekly · Claude Opus 4.7 Extended Thinking Preview` 95% +6d | **expired immediately** | exp T+15m; shows `Weekly · Claude Opus 4.7 Extended Thinking Preview · 5% left` (red, ≤10%, `maxLines=1` truncation) and `Weekly 50%`, then `1 more in T3`; the past Session row is hidden by the limit | expired | fixture `F8` |
| F9 both providers, one reset | Codex as F2 + Claude Session +120m | Codex **expired**, Claude fresh | both fresh; Codex session `Reset` | both expired | fixture `F9` |
| L/D light & dark | F2 under Settings → Appearance = Light, then Dark (and System + device dark mode) | n/a (change does not touch theme) | same rows; palette from `props.theme[appearance]` | — | toggle the app appearance while on F2; the coordinator republishes on `androidAppearance` |

Notes on the brief's dimensions:
- **Long provider names cannot occur.** The widget's provider `name` is hard-coded to `"Codex"` / `"Claude"` (`subscriptionUsageSnapshot.ts`), and account names and emails are never rendered. The long-text risk is window labels, specifically Claude model-scoped weeklies named `Weekly · <model display_name>`; that is row F8. `"Reset …"` is shorter than `"Next reset …"`, so the new label adds no overflow risk.
- **Window count vs `limit = 2`.** Android publishes every window (`maxWindowsPerProvider = Infinity`) and shows the first 2 after sorting by remaining percent. The PR does not change selection. The only interaction: before the PR, any past reset expired the whole provider; after it, past-reset windows still render and can take one of the two slots (F2/F3, the 5-hour row at 100%).
- **The "Reset" vs "Next reset" label** keys off `window.resets[0]`, the earliest reset across pooled members, compared with *that member's* checkedAt (F7a).

## Real-app seeding (Android)

- **No dev hook, deep link, env var, or fixture mode exists** for the widget. I grepped `apps/mobile` for widget fixture/mock/sample/demo and found none. Data flow: `SubscriptionUsageCoordinator` → `buildSubscriptionUsageSnapshot(environmentPresentations.presentationsWithProvidersAtom)` → `publishSubscriptionUsage.android.ts` → `widget.updateSnapshot(snapshot)`. It then calls `T3WidgetExpiry.schedule(name, expiresAt[])`, a native alarm that re-renders the stored snapshot at each deadline.
- **Cheapest path: a throwaway, uncommitted patch on a dev client served by Metro.** In `SubscriptionUsageCoordinator.tsx`, when `process.env.EXPO_PUBLIC_WIDGET_FIXTURE` is set, swap `get(environmentPresentations.presentationsWithProvidersAtom)` for a fixture `Map` built **relative to `Date.now()`** (the probe's fixture builders can be reused). The real build → publish → `updateSnapshot` → expiry-alarm path still runs, with no T3 server, no pairing, and no tunnel back to this VM. Before = the same patch with `git show origin/main:…/subscriptionUsageSnapshot.ts` swapped in. Restart Metro for each fixture (`EXPO_PUBLIC_*` is inlined at bundle time).
- **Clock:** relative fixtures need no frozen clock. The widget's `Date.now()` and the fixture both read device time, so `adb shell date` and `settings put global auto_time 0` are unnecessary. That avoids the unknown of whether the EAS emulator grants `adb root`. "After +16m" can be checked by waiting, or with F5, which flips at +5m.
- **Device:** an EAS cloud Android emulator (`eas-simulator`, Mode C: dev build + `EXPO_UNSTABLE_TUNNEL_V2=1 expo start --tunnel`). A `preview:dev` APK built from `9e0a6e2458` finished on 2026-10-22, but that commit is not in this worktree's history, so its native fingerprint against head is unverified. Run `node scripts/mobile-native-client.ts`, or compare fingerprints, before reusing it. Otherwise build `preview:dev` for Android on EAS. Add the widget to the home screen through the launcher's long-press → Widgets.
- **Real-data alternative (no app patch):** a fake cliproxy hub serving `GET v0/management/auth-files` and `POST v0/management/api-call` with crafted Codex `reset_at` epoch seconds relative to request time, configured as the server setting `usageLimitSources`. This needs a T3 server that the cloud emulator can reach (T3 Connect), and per-account checkedAt values cannot be skewed for F7. It costs more than the patch.

## iOS

**Confirmed: no static fixture mode.** The iOS widget is native WidgetKit via expo-widgets, `publishSubscriptionUsage.ios.ts` → `widget.updateTimeline(subscriptionUsageTimeline(snapshot, Date.now()))`, and the repo has no fixture or preview injection. Its render uses `max(widgetEnvironment.date, Date.now())` and a family-dependent `limit` (2/4/6). The timeline behaviour (an entry at each future `expiresAt`) is covered by the PR's unit tests (`subscriptionUsageTimeline` cases). **iOS on-device is unverified**: this Linux VM has no Xcode or simulator, and iOS on EAS would need the same throwaway patch plus an iOS dev build.
