Files
OmniOilPersonal/openspec/changes/archive/2026-05-07-console-tech-debt-cleanup/tasks.md
henryor af5535dfcc docs(openspec): archiva el change console-tech-debt-cleanup
Persiste proposal, specs delta (auth, api-client, access-requests,
platform-shell, testing), design con 8 ADRs, tasks (27 items en 10 fases),
verify-report (28 tests, 0 críticos, 0 warnings tras el cierre) y
archive-report final.

Cierra el ciclo SDD del cleanup de deuda técnica del console.
2026-05-07 21:14:56 -05:00

11 KiB

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<T> 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<T> + 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)

  • 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).
  • 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]

  • 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]
  • 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.
  • 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).
  • 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]

  • 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.
  • 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.
  • 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]

  • 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.
  • 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]

  • 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.
  • 4.2 RED (apiClient) — Add cases to src/lib/api/client.test.ts: (c) apiClient<T> on 200 resolves with body; (d) apiClient<T> 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<T> on 204 is a dev-time guard). Run pnpm test — (d) fails because today null as T is returned silently.
  • 4.3 GREEN — Edit src/lib/api/client.ts: extract shared executeWithRefresh logic; rename existing apiClient<T> 204 branch to throw new Error('Use apiClientVoid for 204 endpoints'); add export async function apiClientVoid(endpoint, options): Promise<void> 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.
  • 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]

  • 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 <Navigate to="/login"/> 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.
  • 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 <Outlet/>; else if user (persisted) → attempt refresh + show indicator; else → <Navigate to="/login"/> 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]

  • 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]
  • 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]

  • 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]
  • 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]

  • 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.
  • 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]

  • 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')).
  • 9.2 Edit src/app/layouts/PlatformLayout.tsx: wrap <Outlet /> in <ErrorBoundary>; sidebar and <Toaster/> remain outside boundary. Run pnpm test — ensure no regressions.
  • 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]

  • 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)
  • 10.3 Verify pnpm lint passes — no new ESLint warnings. (exits 0)