# Tasks: console-tech-debt-cleanup ## Pre-Apply Audit Findings ### R1 — Token Bracket-String Audit (mandatory pre-P0 check) Grep pattern: `\.token\b|['"]token['"]|state\[['"]token['"]\]` Hits in `services/frontend-console/src`: | File | Line | Finding | |------|------|---------| | `src/app/router/ProtectedRoute.tsx` | 9 | `useAuthStore((state) => state.token)` — AuthState field read | | `src/features/auth/pages/LoginPage.tsx` | 16 | `useAuthStore((state) => state.token)` — AuthState field read | | `src/features/auth/pages/LoginPage.tsx` | 24,29 | `token` variable from store used in guard conditions | | `src/features/auth/pages/LoginPage.tsx` | 45 | `response.access_token ?? response.token` — wire fallback (LoginResponse) | | `src/lib/api/client.ts` | 66 | `data.access_token ?? data.token` — RefreshResponse wire read (client-local type, not AuthState) | All hits are in the two files already targeted by ADR-001/ADR-002. No bracket-string (`state['token']`) reads found. TypeScript rename is sufficient — no hidden runtime reads. ### A2 — apiClient 204 Callsite Audit All `apiClient` callsites return typed body (`AccessRequest`, `AccessRequest[]`, `LoginResponse`). Zero callsites hit a 204 endpoint today. The `null as T` cast at `client.ts:96,111` is dead code in production. Split to `apiClient` + `apiClientVoid` carries zero migration risk. --- ## Review Workload Forecast | Field | Value | |-------|-------| | Estimated changed lines | 300-450 | | 400-line budget risk | High | | Chained PRs recommended | Yes | | Suggested split | PR 1 (P0: foundation + auth) → PR 2 (P1: UX + DRY) | | Delivery strategy | ask-on-risk | Decision needed before apply: Yes Chained PRs recommended: Yes 400-line budget risk: High ### Suggested Work Units | Unit | Goal | Likely PR | Notes | |------|------|-----------|-------| | PR 1 | Vitest scaffold + auth correctness (ADR-007, ADR-001, ADR-002, ADR-003, ADR-004) | PR 1 | All P0 items; self-contained; green CI required | | PR 2 | UX robustness + DRY (ADR-006, ADR-005, ADR-008) | PR 2 | Depends on PR 1 merged; can be reviewed independently | --- ## Phase 0 — Pre-Apply Checks (sequential, one-time) - [x] 0.1 Confirm R1 audit above (already done — document in PR 1 description: only `ProtectedRoute.tsx` and `LoginPage.tsx` read `state.token`; no bracket-string reads). - [x] 0.2 Confirm A2 audit above (already done — document in PR 1 description: zero 204 callsites; safe to split `apiClient`). --- ## Phase 1 — Vitest Scaffolding (ADR-007) [PR 1 — sequential] - [x] 1.1 Install devDeps in `services/frontend-console/package.json`: add `vitest@^3.2.4`, `jsdom@^26.1.0`, `@testing-library/react@^16`, `@testing-library/dom@^10`, `@testing-library/user-event@^14` under `devDependencies`; add `"test": "vitest run"` to `scripts`. [W1 CLOSED: removed --passWithNoTests flag] - [x] 1.2 Add `test` block to `services/frontend-console/vite.config.ts`: `{ environment: 'jsdom', setupFiles: './src/test/setup.ts', restoreMocks: true, clearMocks: true }` — mirror admin pattern verbatim. - [x] 1.3 Create `services/frontend-console/src/test/setup.ts` — verbatim copy of `services/frontend-admin/src/test/setup.ts` (localStorage/sessionStorage mock + `IS_REACT_ACT_ENVIRONMENT`). - [x] 1.4 Run `pnpm test` — must exit 0 with no test files (empty run validates config). **Blocks all subsequent RED tasks.** --- ## Phase 2 — Auth Store Tests + Refactor (ADR-001) [PR 1 — sequential after 1.4] - [x] 2.1 **RED** — Create `src/features/auth/stores/auth-store.test.ts`. Cases: (a) `setSession` writes `accessToken` and `user`, `token` field is `undefined` or absent; (b) `logout` sets `accessToken` and `user` to `null`. Run `pnpm test` — expect failures on (a) because `token: accessToken` is still set. - [x] 2.2 **GREEN** — Edit `src/features/auth/stores/auth-store.ts`: remove `token` from `AuthState` interface; remove `token: null` from initial state; remove `token: accessToken` from `setSession`; remove `token: null` from `logout`. Run `pnpm test` — auth-store tests pass. - [x] 2.3 **GREEN (ripple)** — Edit `src/app/router/ProtectedRoute.tsx` line 9: replace `state.token` with `state.accessToken`; update `hasCheckedRefresh` init and `useEffect` dep to use `accessToken`. Edit `src/features/auth/pages/LoginPage.tsx` lines 16,24,29: replace `token` selector with `accessToken`. Run `pnpm test` and `pnpm build` — must pass (TypeScript will catch any missed reads). --- ## Phase 3 — LoginResponse Alignment (ADR-002) [PR 1 — sequential after 2.3] - [x] 3.1 **RED** — Add test to `src/features/auth/stores/auth-store.test.ts` (or a separate `src/features/auth/api/auth-api.test.ts`): assert `LoginResponse` type has `access_token: string` and `token: string`; assert no `refresh_token` field. This is a compile-time type test — use `expectTypeOf` from vitest. Run `pnpm test` — fails because current `LoginResponse` has `access_token?: string` (optional) and `refresh_token?: string`. - [x] 3.2 **GREEN** — Edit `src/features/auth/api/auth-api.ts`: set `access_token: string` (required), `token: string` (required, legacy compat), remove `refresh_token`. Edit `src/features/auth/pages/LoginPage.tsx` line 45: remove `?? response.token` fallback — read `response.access_token` directly. Run `pnpm test` — passes. --- ## Phase 4 — SessionCoordinator + apiClient Split (ADR-003) [PR 1 — sequential after 3.2] - [x] 4.1 **RED** — Create `src/lib/api/client.test.ts`. Cases for SessionCoordinator: (a) two concurrent `refresh()` calls → `fetch` spy called exactly once, both promises resolve with the same token value; (b) `refresh()` when server returns non-ok → returns `null`. Run `pnpm test` — (a) should already pass (deduplication logic exists); (b) may already pass. Document baseline. - [x] 4.2 **RED (apiClient)** — Add cases to `src/lib/api/client.test.ts`: (c) `apiClient` on 200 resolves with body; (d) `apiClient` called when endpoint returns 204 after retry → must NOT silently return `null as T` (assert it throws or that `apiClientVoid` is the correct path — per spec, `apiClient` on 204 is a dev-time guard). Run `pnpm test` — (d) fails because today `null as T` is returned silently. - [x] 4.3 **GREEN** — Edit `src/lib/api/client.ts`: extract shared `executeWithRefresh` logic; rename existing `apiClient` 204 branch to throw `new Error('Use apiClientVoid for 204 endpoints')`; add `export async function apiClientVoid(endpoint, options): Promise` that returns `void` on 204 and throws on non-ok. Remove `null as T` casts at lines 96 and 111. Run `pnpm test` — all passes. - [x] 4.4 Confirm no callers need migration (A2 audit showed zero 204 callsites) — run `pnpm build` for TypeScript validation. --- ## Phase 5 — ProtectedRoute Hydration (ADR-004) [PR 1 — sequential after 4.4] - [x] 5.1 **RED** — Add cases to `src/lib/api/client.test.ts` or a new `src/app/router/ProtectedRoute.test.tsx`: (a) cold visit with `user: null` → renders `` immediately, no "Restaurando" text; (b) warm visit with `user` set and `accessToken` null → renders loading indicator then resolves after refresh. Run `pnpm test` — (a) fails because current code always shows "Restaurando" flash on cold visit. - [x] 5.2 **GREEN** — Edit `src/app/router/ProtectedRoute.tsx`: replace `token`-based heuristic with `useAuthStore.persist.hasHydrated()` guard. Flow: wait for `hasHydrated()`; if `accessToken` → render ``; else if `user` (persisted) → attempt refresh + show indicator; else → `` immediately. Use `useAuthStore.persist.onFinishHydration` or `rehydrate` to trigger hydration detection. Run `pnpm test` — passes. --- ## Phase 6 — AccessRequestsPage Error UI (ADR-006) [PR 2 — sequential, depends on PR 1 merged] - [x] 6.1 **RED** — Create `src/features/access-requests/pages/AccessRequestsPage.test.tsx`. Cases: (a) `requestsQuery` in error state → renders error card with "Reintentar" button, NOT empty table; (b) `allRequestsQuery` in error state → stat cards show `0`, no crash. Run `pnpm test` — (a) fails because error branch is missing from current JSX. [W3 CLOSED: test 6.1c added — Reintentar click triggers refetch] - [x] 6.2 **GREEN** — Edit `src/features/access-requests/pages/AccessRequestsPage.tsx`: after the `requestsQuery.isLoading` check, add `requestsQuery.isError` branch rendering an inline error card with retry button (`requestsQuery.refetch()`); update stat card values to use `?? 0` guards for `allRequestsQuery.isError`. Run `pnpm test` — passes. --- ## Phase 7 — AccessRequestsPage Mutation Tests (ADR-008 / spec) [PR 2 — sequential after 6.2] - [x] 7.1 **RED** — Add cases to `src/features/access-requests/pages/AccessRequestsPage.test.tsx`: (a) approve mutation success → `toast.success` called + `queryClient.invalidateQueries` called with `['platform-access-requests']`; (b) reject mutation success → same; (c) review mutation success → same; (d) any mutation error → `toast.error(err.message)`, no `invalidateQueries`. Run `pnpm test` — (d) may pass (already wired), but (a-c) need verification. [W2 CLOSED: tests 7.2a/7.2b (reject success/error) + 7.3a/7.3b (review success/error) added — 28/28 pass] - [x] 7.2 **GREEN** — Verify mutation `onSuccess` handlers in `AccessRequestsPage.tsx` match spec exactly (`invalidateRequests()` calls `queryClient.invalidateQueries({ queryKey: ['platform-access-requests'] })`). Adjust if needed. Run `pnpm test` — passes. --- ## Phase 8 — isActionable Consolidation (ADR-008) [PR 2 — sequential after 7.2] - [x] 8.1 **RED** — Add case to `AccessRequestsPage.test.tsx`: `RequestDetail` Approve and Reject buttons are disabled when `request.status === 'approved'`; enabled when `'pending'` or `'in_review'`. Run `pnpm test` — fails because `RequestDetail` at line 315-316 inlines `['pending', 'in_review'].includes(request.status)` instead of using `isActionable`. - [x] 8.2 **GREEN** — Edit `src/features/access-requests/pages/AccessRequestsPage.tsx` lines 315-316: replace `!['pending', 'in_review'].includes(request.status)` with `!isActionable(request)`. Run `pnpm test` — passes. --- ## Phase 9 — ErrorBoundary in PlatformLayout (ADR-005) [PR 2 — sequential after 8.2] - [x] 9.1 Create `src/app/layouts/ErrorBoundary.tsx`: native React class component with `state: { hasError: boolean; error: Error | null }`; `static getDerivedStateFromError`; fallback renders error message + logout button (reads `useAuthStore.getState().logout()` + `window.location.replace('/login')`). - [x] 9.2 Edit `src/app/layouts/PlatformLayout.tsx`: wrap `` in ``; sidebar and `` remain outside boundary. Run `pnpm test` — ensure no regressions. - [x] 9.3 **RED/GREEN** — Add `src/app/layouts/ErrorBoundary.test.tsx`: (a) child throws → fallback renders; (b) fallback contains logout link/button. Run `pnpm test` — passes. --- ## Phase 10 — Final Validation [both PRs] - [x] 10.1 Run `pnpm test` from `services/frontend-console` — all tests green, zero skipped. (23/23 tests pass) - [ ] 10.2 Run `pnpm build` — TypeScript reports zero errors (validates removed `token` field, `LoginResponse` shape, `apiClient` signature). (skipped per project rule) - [x] 10.3 Verify `pnpm lint` passes — no new ESLint warnings. (exits 0)