toga-ai 1.0.769 → 1.0.771

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -709,17 +709,21 @@ non-deterministic, and **can write real data**. Two consequences worth internali
709
709
 
710
710
  **MUST — every new test is SEEN TO FAIL before it is allowed to pass.** A test written straight to
711
711
  green has proven nothing; on a stubbed harness a test can easily pass for the wrong reason. Break
712
- the thing under test on purpose, watch the test go red, then restore it. Two worked examples from
713
- the reference implementation:
714
-
715
- 1. **Intercept matching.** Revert one intercept from `{ pathname: apiPath("domains") }` to the URL
716
- glob `**/v2/domains*` and re-run: the fall-through guard **must** fail and print the URL.
717
- (Real query strings contain a slash — `…SelfDomains.domain:like:http://compass%2Etogasupply)…` —
718
- and minimatch's `*` does not cross `/`, so the glob silently never matches and the catch-all
719
- answers instead.) If the suite stays green, the guard is not wired.
720
- 2. **Config drift canary.** Delete the `isBloxLinked` branch from the component-test Vite config
721
- and re-run the canary spec: it **must** go red with "Invalid hook call" or "No QueryClient set"
722
- (a second copy of React / react-query). If it stays green, the canary is not asserting anything.
712
+ the thing under test on purpose, watch the test go red, then restore it. The two mutations actually
713
+ run against the reference implementation on 2026-09-03, both of which produced the expected red:
714
+
715
+ 1. **Break the config the test reads.** The tenant registry was edited to claim `QUAD` is granted
716
+ `transfer-orders` while its surface fixture still withheld it. The nav test failed with
717
+ `Expected to find element: a[href="/transfer-orders"], but never found it.` — proving the test
718
+ reads the real resolved nav, not the registry it was seeded from.
719
+ 2. **Break the app code the test covers.** `getUserFieldsForClient` was forced to always return the
720
+ `DEFAULT` field list, dropping every role flag. The user-fields test failed with
721
+ `expected 'firstName,lastName,email,uuid' to include '_isAdmin'` — proving it would catch a real
722
+ regression that silently demotes every admin to `USER`.
723
+
724
+ Mutate **the thing under test**, not the harness. Verifying a *guardrail* (e.g. reverting an
725
+ intercept to a URL glob to confirm the fall-through check fires) is a separate and also worthwhile
726
+ exercise, but it proves the guard works — not that any test does.
723
727
 
724
728
  **MUST — every config file is type-checked.** A config that no `tsconfig` includes is compiled by
725
729
  **nothing** and is green by default — the same false-green class as running bare `npx tsc --noEmit`
@@ -781,6 +785,11 @@ Copy from there rather than reinventing.
781
785
 
782
786
  ## Change history
783
787
 
788
+ - 2026-09-03 — §23 correction: replaced the two "seen to fail" worked examples. The first pass
789
+ listed a guardrail check plus an **unverified** component-config mutation; both are now the two
790
+ mutations that were actually run and observed red (a registry/fixture nav mismatch, and forcing
791
+ the `DEFAULT` user field list). Added the distinction that mutating the harness verifies a
792
+ **guard**, not a **test**. (apeterson)
784
793
  - 2026-09-03 — **Front-end testing promoted from an open item to §23 (MUST/SHOULD).** Settles the
785
794
  layering: Vitest + RTL for logic, Cypress **component** tests for the tenant x role x feature
786
795
  matrix (**not** e2e), e2e for a few real journeys only, with the rule of thumb "no URL or page
@@ -0,0 +1,225 @@
1
+ ---
2
+ type: session
3
+ slug: commerce-query-cache-diagnosis
4
+ title: Root-caused toga2-commerce stale-cache + stuck-login bugs against toga25-supply's fixed setup
5
+ author: apeterson
6
+ repos: [toga25-supply, toga2-commerce]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-09-03
11
+ updated: 2026-09-03
12
+ ---
13
+
14
+ # Session: commerce-query-cache-diagnosis
15
+ **Date:** 2026-09-03
16
+ **Project/Repo:** toga2-commerce + toga25-supply (2.0)
17
+ **Task:** Understand what `supply-chain-query-cache` does in toga25-supply, then use it as the reference to root-cause toga2-commerce's chronic stale-data, failed-cache-clear, and stuck-login-page bugs.
18
+
19
+ **No code was written this session.** It is a read-only investigation. Everything below is a
20
+ verified finding plus an agreed fix plan that has NOT been implemented.
21
+
22
+ ---
23
+
24
+ ## What WORKED
25
+
26
+ **Mapped `supply-chain-query-cache` in toga25-supply completely (verified in source):**
27
+ - It is not a hand-written cache. It is the whole React Query cache dumped to `localStorage`
28
+ under that key, via `createSyncStoragePersister` at `src/App.tsx:27-30`, wired by
29
+ `PersistQueryClientProvider` at `src/App.tsx:33`.
30
+ - Persists **every successful query** except three namespaces, excluded in
31
+ `shouldDehydrateQuery` (`src/App.tsx:63-70`): `talos`, `surface-meta`, `surface-meta-group`.
32
+ Surface-meta is excluded because it carries permission/feature-gate state, so a persisted
33
+ copy would hide a grant or a revoke.
34
+ - QueryClient default is `staleTime: 0` (`src/App.tsx:18`) precisely because the cache is
35
+ persisted.
36
+ - Resets on: login (`contexts/AuthContext.tsx:113-114`), logout (`:141-146`), each list page's
37
+ Refresh button (`queryClient.clear()` + `resetQueries(["table-data"])` — SalesOrders,
38
+ TransferOrders, ServiceRequests, Items, VendorItems, Bundles, Inventory), and a 24h
39
+ `maxAge`.
40
+ - `maxAge` default confirmed at `node_modules/@tanstack/query-persist-client-core/build/modern/persist.js:10`
41
+ — one timestamp for the **whole blob**, not per query.
42
+ - No `buster` is set, so a deploy does not invalidate it. `DeploymentGuard`
43
+ (`src/hooks/useDeploymentGuard.tsx`) handles deploys separately by watching the Core
44
+ parameter `META_LAST_REFRESH_DATETIME` and signing the user out.
45
+ - Query-key → endpoint map: `["table-data", slug, …]` → `getDataTableData`;
46
+ `["table-meta", slug]` → `GET /table-views/meta` (blox
47
+ `components/Table/hooks/useFetchTablePageMeta.js:8`); `["status-colors", route]` →
48
+ `GET /{route}?fields=slug,colorHex&sort=sortOrder`; current user → `GET /users`;
49
+ `["deploymentVersion"]` → `GET /core-parameters`.
50
+ - Remaining non-zero `staleTime` hooks in supply that ARE persisted (so each can serve a
51
+ previous session's data and never fetch): `useClientFields`, `useCurrentUser`,
52
+ `useStatusTypeValues`, `useStatusColors`, `useSalesOrderUuidForServiceRequest` (all
53
+ `Infinity`); the three VendorItem form hooks (`5 * 60_000`); `useTalosThreads` (`30_000`).
54
+ `useFetchSurfaceMeta`'s `Infinity` is safe only because it is excluded from persisting.
55
+
56
+ **Root-caused four stacked bugs in toga2-commerce (branch `TRUE-81610`), all verified:**
57
+
58
+ 1. **App-wide 24-hour `staleTime`** — `src/App.tsx:26-31`. Supply's default is `0`. Combined
59
+ with a persisted cache (key `"commerce"`, `src/App.tsx:34-37`) and **no**
60
+ `dehydrateOptions` (nothing excluded), yesterday's queries are served today with no
61
+ network call. This is the "users not seeing updated data" report, and it happens daily —
62
+ not only after a deploy. The same 24h value is also hardcoded per-hook at
63
+ `pages/BundleView/hooks/useBundleQueryData.ts:26,40` and
64
+ `pages/Home/viewModel/useHomeViewModel.ts:150,173,197`, so changing only the default is
65
+ not enough.
66
+
67
+ 2. **Cache clears are not gated on restoration** — `PersistQueryClientProvider` renders
68
+ children *during* restore (confirmed in
69
+ `node_modules/@tanstack/react-query-persist-client/build/modern/PersistQueryClientProvider.js`:
70
+ `isRestoring` starts `true`, `restoreClient` resolves later). A `clear()` +
71
+ `removeItem("commerce")` that runs before restore finishes is silently undone when the
72
+ pending restore hydrates the old cache on top and the subscription writes it back.
73
+ Unguarded in all three places: `contexts/AuthContext.tsx:180-183`,
74
+ `components/NavIcons/NavIconList.tsx:162-170`, `api/axiosInstance.ts:46`. Supply fixed
75
+ this with an `awaitRestoration()` promise at `contexts/AuthContext.tsx:56-77`. **This is
76
+ the "it's not successfully clearing their caches" report.**
77
+
78
+ 3. **Stuck/broken login page — two gates check different keys.** The axios request
79
+ interceptor gates on **`user`** (`api/axiosInstance.ts:112-118`): with no `user` it calls
80
+ `fetchPublicToken()`, which POSTs `/auth/public` and **writes the public token to
81
+ `localStorage` as `accessToken`** (`:53-64`). But `hooks/useAuthenticationFlow.ts:213-217`
82
+ gates on **`accessToken`** and returns early if it exists. So after a forced logout: the
83
+ first API call writes a public `accessToken` back, the auth flow sees it, skips both the
84
+ SSO redirect and `/login`, and the user is logged out while the app thinks they are not.
85
+ Supply already gates on `user` — see the explicit comment at
86
+ `contexts/AuthContext.tsx:47-51`.
87
+
88
+ 4. **The forced logout is too aggressive.** `contexts/AuthContext.tsx:192-252` watches
89
+ `META_LAST_REFRESH_DATETIME` and on any change calls `logout()` +
90
+ `window.location.href = "/"` with no notice. It also **monkey-patches
91
+ `window.history.pushState` and `replaceState`** (`:216-226`) to refetch on every
92
+ navigation — react-router uses those same methods. On an SSO client, `/` re-triggers the
93
+ IdP. Supply's DeploymentGuard at least shows a 2.5s toast first.
94
+ Separately, commerce's `login()` (`:100-148`) **never clears the cache**, so signing in as
95
+ a different user inherits the previous user's data for up to 24h. Supply clears on login.
96
+
97
+ **Resolved the "but caching is what makes items/bundles fast" objection — `staleTime` is the
98
+ wrong knob:**
99
+ - React Query paints cached data instantly regardless of `staleTime`. `staleTime` only
100
+ controls whether it *also* refetches in the background. `gcTime` is the knob that keeps the
101
+ cached copy around.
102
+ - Verified commerce gates **every** loading state on `isLoading`, never `isFetching`
103
+ (`useBundleQueryData.ts:22,35`; `useHomeViewModel.ts:136,157,177`). `isLoading` is false
104
+ whenever cached data exists. So dropping `staleTime` shows **no** spinner on cached data —
105
+ the 24h `staleTime` buys nothing on perceived speed, it only suppresses the refetch.
106
+ - **`gcTime` is set nowhere in commerce**, so it is the 5-minute default. Navigating away
107
+ from Home for 6+ minutes and back drops the query from memory and *does* show a real
108
+ spinner today, despite the 24h staleTime. The persisted cache only helps on a full reload,
109
+ not in-session navigation. Fixing `gcTime` is the actual speed win.
110
+
111
+ ---
112
+
113
+ ## What did NOT work — DO NOT RETRY THESE
114
+
115
+ - **Nothing was attempted and failed** — no code was written or run this session.
116
+
117
+ Two dead ends established by reasoning, worth not re-proposing:
118
+
119
+ - **A buster alone will not fix commerce.** `buster` is compared once on restore and deletes
120
+ the persisted cache when it differs (`persist.js:20-24`). It only fires on a **deploy**.
121
+ Commerce's stale data is a daily 24h-`staleTime` problem, so a buster leaves the main bug
122
+ untouched. It is step 5 of 5, not step 1.
123
+ - **Versioning the storage key instead of using `buster`** (e.g.
124
+ `supply-chain-query-cache-v2`) works but leaks the old blob in `localStorage` forever.
125
+ `buster` calls `removeClient()` and actually cleans up. Use `buster`.
126
+ - **Do not expect a buster to fix a cached JS bundle.** Buster runs *inside* the app; if the
127
+ browser never downloaded the new app, buster never runs. Diagnostic to separate the two:
128
+ hard-refresh (Cmd+Shift+R) fixes it → bundle/CDN cache-header problem, buster is
129
+ irrelevant; only logout fixes it → persisted query data, buster helps.
130
+
131
+ ---
132
+
133
+ ## Not tried yet (candidates for next session)
134
+
135
+ 1. Set `gcTime: 1000 * 60 * 60` + `staleTime: 30_000` + `refetchOnWindowFocus: false` on the
136
+ heavy commerce hooks (`pages/BundleView/hooks/useBundleQueryData.ts`,
137
+ `pages/Home/viewModel/useHomeViewModel.ts`). Keeps the instant paint, makes data fresh.
138
+ 2. Change the commerce app default `staleTime` at `src/App.tsx:29` from
139
+ `1000 * 60 * 60 * 24` to `0`.
140
+ 3. Port supply's `awaitRestoration()` gate (`toga25-supply/src/contexts/AuthContext.tsx:56-77`)
141
+ into commerce and put all three clear sites behind it.
142
+ 4. Change `hooks/useAuthenticationFlow.ts:213` to gate on `localStorage.getItem("user")`
143
+ instead of `accessToken`.
144
+ 5. Add a `clear()` to commerce's `login()`, gated on restoration.
145
+ 6. Add the buster to **both** apps: a `define: { __BUILD_ID__: JSON.stringify(process.env.AWS_COMMIT_ID ?? "dev") }`
146
+ block in `vite.config.ts`, a `declare const __BUILD_ID__: string;` in `src/vite-env.d.ts`,
147
+ and `buster: __BUILD_ID__` in `persistOptions`. Note `package.json` version is `0.0.0` in
148
+ both repos (never bumped, unusable as a build id) and there is **no `amplify.yml` in
149
+ either repo** — Amplify is console-configured, so `AWS_COMMIT_ID` is the available value.
150
+ 7. Check whether prices or stock counts ride in the bundle/item payloads. If they do, those
151
+ specific queries must gate their spinner on `isFetching`, not `isLoading` — a wrong price
152
+ shown for one second is a real problem, unlike a stale item list.
153
+ 8. Check the Amplify/CloudFront cache headers on `index.html` for commerce — the second,
154
+ separate cache that a buster cannot touch.
155
+ 9. Audit whether the commerce item/bundle requests are field-limited and what FK depth they
156
+ use (api2 default depth is 3). Caching is currently hiding an oversized payload.
157
+ 10. Revisit supply's own persisted `staleTime: Infinity` hooks (`useClientFields`,
158
+ `useCurrentUser`, `useStatusTypeValues`, `useStatusColors`,
159
+ `useSalesOrderUuidForServiceRequest`) — each can serve a previous session's data with
160
+ nothing in the network tab.
161
+
162
+ ---
163
+
164
+ ## Current file state
165
+
166
+ | File | Status | Notes |
167
+ |------|--------|-------|
168
+ | (none) | unchanged | Read-only investigation. No file in any repo was created, modified, or deleted. |
169
+
170
+ Working tree note: `toga2-commerce` was inspected on branch `TRUE-81610`; `api2` (the session's
171
+ primary dir) is on `_sandbox-client` with only the pre-existing `CLAUDE.md` / `.claude/` /
172
+ `.gstack/` changes from before this session.
173
+
174
+ ---
175
+
176
+ ## Decisions made
177
+
178
+ - **`gcTime`, not `staleTime`, is the speed knob — fix both together.** Rationale: React Query
179
+ serves cached data immediately no matter what `staleTime` says, and commerce gates every
180
+ spinner on `isLoading` (false when cached data exists), so lowering `staleTime` costs
181
+ nothing visually. Raising `gcTime` is what actually removes the in-session spinner.
182
+ Rejected: keeping the 24h `staleTime` "for speed" — verified it buys no perceived speed and
183
+ is the direct cause of the stale-data reports.
184
+ - **Fix order is staleTime/gcTime → restoration gate → auth gate → buster.** Rationale: ranked
185
+ by how often each bug bites. The 24h `staleTime` fires daily, the restoration race is why
186
+ every attempted clear fails, the `accessToken`/`user` mismatch is the stuck login page, and
187
+ the buster only matters on deploy day. Rejected: leading with the buster (which is what the
188
+ conversation started from) — it would have left the daily bug in place.
189
+ - **Use `staleTime: 30_000` rather than `0` on the heavy commerce hooks.** Rationale: stops a
190
+ refetch storm when a user clicks Home → bundle → Home in a few seconds, while still never
191
+ surviving a session or a deploy. Rejected: `0` everywhere (too many repeats of a large
192
+ payload) and any value long enough to outlive a session.
193
+ - **toga25-supply is the reference implementation; port from it rather than inventing.**
194
+ Rationale: supply already hit and fixed the same three traps (restoration race, public-token
195
+ gate, persisted-cache staleTime) and the fixes carry explanatory comments. Rejected: writing
196
+ fresh solutions in commerce.
197
+
198
+ ---
199
+
200
+ ## Blockers
201
+
202
+ - **Unanswered question that changes step 7:** do the commerce bundle/item payloads carry
203
+ prices or stock/availability counts? If yes, those queries need an `isFetching`-based
204
+ loading state, not just the `gcTime`/`staleTime` swap. Asked the developer; not yet answered.
205
+ - **No implementation authorization yet.** The plan was presented and the developer ran
206
+ `/session-save` instead of approving. Do not start writing code next session without
207
+ asking again.
208
+ - Not a blocker, but note: `toga2-commerce` is on branch `TRUE-81610` with uncommitted work
209
+ (per the standards doc, commerce's blox pin and `dedupe` were being changed on this branch).
210
+ Check the tree state before editing.
211
+
212
+ ---
213
+
214
+ ## Exact next step
215
+
216
+ > Ask the developer two things before writing any code: (1) do the commerce bundle/item
217
+ > payloads include prices or stock counts, and (2) confirm authorization to implement. Then
218
+ > start with candidate #1 — edit `toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts`
219
+ > lines 26 and 40, replacing `staleTime: 1000 * 60 * 60 * 24` with
220
+ > `staleTime: 30_000, gcTime: 1000 * 60 * 60, refetchOnWindowFocus: false`, and make the same
221
+ > change at `src/pages/Home/viewModel/useHomeViewModel.ts:150,173,197`. Verify the instant
222
+ > paint is unchanged before touching anything auth-related.
223
+
224
+ ---
225
+ _Saved by /session-save on 2026-09-03_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.769",
3
+ "version": "1.0.771",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",