Repository navigation
Conversation
Pin vitest in the root catalog for every workspace, drop the root example.spec.ts placeholder, add root test scripts and document the conventions in TESTING.md (linked from AGENTS.md). Fix @fixr/env's typescript-config dependency so its tsconfig resolves. Refs #102 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
server.ts built Fastify at module scope and called listen() on import. buildApp() now builds the instance without binding a port (docs and logger configurable); server.ts is only the entrypoint. The JWT helper no longer imports the server singleton, and the two competing error handlers are merged into one (the ZodError one was dead code). Refs #103 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Vitest projects for unit and integration, MySQL + Redis containers in globalSetup, truncate/flush between tests, deterministic factories and auth helpers that sign real sessions per role. Refs #104 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
RBAC role x permission matrix with orphan-permission guard, Zod schema specs for every @fixr/schemas file and constants helpers. createAbility() now fails closed to guest for unknown roles. Refs #105 #106 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
authenticate/authenticateEmployee now rethrow AppError unchanged (it matched isFastifyError and every domain error became a 401), and withErrorHandler no longer returns stack traces in production. Refs #107 #108 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Auth, tokens, credentials, account, companies, employees, catalog, uploads and service-orders services, with repositories, mail, R2 and Google mocked. Known security gaps (login/reset enumeration, missing OAuth state, any upload MIME) are pinned with explicit comments. Fix: patch/delete/image operations on models now require the model to belong to the caller's company (any company with devices:update could modify another tenant's models or the shared catalog). Refs #109 #110 #111 #112 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Full login/verify/refresh/sign-out and password-reset flows against real MySQL and Redis. Bugs found and fixed along the way: - Client (non-employee) accounts could not log in: the JWT/account queries return company = NULL, which the schemas rejected (500). - Error envelopes were serialized through the routes' literal response schemas, so any error with a status the route documents under another code (e.g. a 400 validation error, a 401 missing session) became a 500 FST_ERR_FAILED_ERROR_SERIALIZATION. Errors now bypass them. - POST /auth/register answered HTTP 500 with a 501 body; it now throws AUTH_NOT_IMPLEMENTED. Refs #113 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
- RBAC sweep: every route from buildApp() must be allowlisted as public or carry an auth middleware plus requirePermission(); every guarded route is exercised with every role from roleAbilities (403 vs pass), without a session (401) and with a forged token (401). - Companies/employees, catalog, service orders, uploads, cache invalidation and fail-open behaviour against real MySQL/Redis. Fix: the presign route documented a single literal success code, so service-order and model image presigns always failed with a 500 response serialization error; only avatars worked. Refs #114 #115 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Vitest config for apps/workers and packages/mail; specs for the job processor, worker wiring and listeners, every React Email template and the queue helper (retry/backoff contract). The worker now rejects unknown job names with a clear error instead of a TypeError, and closes gracefully on SIGTERM/SIGINT so in-flight jobs finish before the process exits. Refs #116 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
jsdom environment, MSW server with onUnhandledRequest=error and specs for route matching, formatters, parsers, data-table helpers, the API service and the auth client/interceptors. Fixes found by the new tests: - The 401 -> refresh response interceptor was registered on the global axios instance, not on the exported client, so it never ran; it now also retries a request at most once (no refresh loop). - Route rules matched by prefix and the first rule won, so pages like /service-orders/new were gated by serviceOrders:read instead of serviceOrders:create. Exact matches now take precedence. Refs #117 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Session/avatar/media-query/scroll hooks and the sidebar store, the @fixr/permissions React bindings (<Can>, useAbility), the login, forgot-password and new-employee forms (validation, success and API error paths through MSW) and the service order table. Refs #118 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Own jsdom/MSW setup and a Clerk test double (separate from apps/web's JWT helpers). Covers the Clerk middleware route protection, lib/utils and the create-company form (bearer token, validation, API errors). Refs #119 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
New e2e workspace: dedicated MySQL/Redis via docker compose (never the dev DB), API + Next dev servers, per-run reset/seed (employees seeded through the API), storageState per role and hermetic Turnstile stubs. Covers login (success, wrong password, unverified), email verification, protected-route redirect, logout, password reset and silent refresh. Fix: the Turnstile wrapper re-armed its 15s load timeout on every parent render, so it reported a security-check failure (and dropped the token) 15s after the user's last keystroke. The sign-out icon button now has an accessible name. Refs #120 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Employee creation through the UI, one access test per role derived from roleAbilities (UI redirect + API 403), cross-tenant URL/API access, API-down resilience and navigation smoke. Service order and catalog flows are marked fixme: those screens still use mock data / don't exist. Fix: a role without companies:read (guest) was redirected from home to home forever (ERR_TOO_MANY_REDIRECTS); it now lands on the public support page. Covered by a middleware unit spec as well. Refs #121 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Parallel lint/types/unit jobs, a Docker-backed integration job and an e2e job (PRs to main, pushes, manual) that uploads the Playwright report and traces on failure. Unit coverage (V8) is enforced per workspace with low starting thresholds and stricter ones for @fixr/permissions and the server auth/RBAC middlewares; a summary is posted on the PR. deploy.yml is left untouched (infra guardrail); TESTING.md documents the required checks and how deploys relate to CI. Refs #122 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
bun run lint passes --elide-lines, which only works in a terminal, so the hook failed in any non-interactive shell before checking anything. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Keeps this branch's buildApp() split: the API keys route and its OpenAPI tag move from server.ts into app.ts, and the test/e2e envs gain API_KEY_SECRET. Keeps this branch's vitest config (develop's esbuild workaround targeted the same unresolved typescript-config extends that the @fixr/env dependency fix already solves). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
z.date().min(new Date()) froze "now" when the module loaded, so a long-running server accepted expirations already in the past. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
- CookieDialog wrote document.cookie during render, so /auth/login returned 500 whenever the verified/deleted dialog should open. - When the middleware refreshed an expired session it only set the new cookies on the response; the server components of that same request still saw no session and getSession() threw (500). The refreshed cookies are now forwarded to the request too. The e2e tests now assert the pages render, not just the URL. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Schemas, key helpers, both new middlewares (scope narrowing, revoked, expired, usage throttling), the service (no scope escalation, TTL cap, ownership), real-DB flows (revocation beats the Redis lookup cache, keys can't mint keys or reach /account, cross-tenant), the RBAC sweep for authenticateEmployeeOrApiKey, the web form/secret reveal and an e2e create -> use -> revoke flow. The secret copy button gets an accessible name. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Unit test coverage
Thresholds are enforced per workspace in each |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O que mudou
Implementa o épico de testes: stack definida, suítes para todos os apps/pacotes, e2e com Playwright e workflow de CI. Convenções em
TESTING.md(referenciado noAGENTS.md); e2e documentado eme2e/README.md.Fundação
catalog:em todo o monorepo;example.spec.tsremovido; scriptstest,test:unit,test:ci,test:integration,test:e2e,lint:cina raiz.buildApp()extraído deserver.ts(que virou só o entrypoint comlisten()); os doissetErrorHandlerconcorrentes viraram um só.Cobertura
fixme)buildApp()precisa estar na allowlist pública ou ter autenticação +requirePermission(); cada rota protegida é exercitada com cada role deroleAbilities(403 vs. passa), sem sessão (401) e com token forjado (401).storageStatepor role e Turnstile stubado no browser e na API de e2e (sem mudar código de produção).CI (
.github/workflows/ci.yml): lint, types, unit + cobertura (thresholds por workspace, mais rígidos para@fixr/permissionse middlewares de auth/RBAC, resumo comentado no PR), integração (Docker) e e2e (PRs paramain, pushes emdevelop/main, com upload do relatório/traces em falha).deploy.ymlnão foi alterado (guardrail de infra); a relação deploy × CI está documentada noTESTING.md.Merge de
develop: inclui as API keys. O registro das rotas/tag foi movido paraapp.ts; as API keys ganharam testes unitários, de integração, web e e2e.Bugs encontrados pelos testes e corrigidos
devices:updatealterava/excluía models de outra empresa (e do catálogo compartilhado).company: NULLrejeitado pelos schemas).FST_ERR_FAILED_ERROR_SERIALIZATION.authenticatetransformava todoAppErrorem 401 (ex.: conta não-funcionário deveria ser 403)./service-orders/newera liberado comserviceOrders:read; roleguestentrava em loop infinito de redirect; o Turnstile acusava falha 15s após a última tecla;/auth/loginretornava 500 logo após a verificação de e-mail; a primeira página após o refresh silencioso de sessão retornava 500.expiresAtera congelada no boot do servidor (aceitava datas passadas)./auth/registerrespondia HTTP 500 com corpo 501;createAbility()com role desconhecida agora cai emguest; worker de e-mail com shutdown gracioso e erro claro para job desconhecido;@fixr/envagora referencia otypescript-configdo workspace; hook de pre-push usalint:ci(o--elide-linesquebrava fora de terminal).Lacunas mapeadas (não alteradas, decisões de produto/segurança)
0012renomeiauploads.purpose, que nenhuma migration anterior cria. Os testes usamdrizzle-kit push; a cadeia precisa ser regenerada.state(CSRF).account:delete; clientes têm a ability deguest(não leemGET /account); o painel admin não verifica role (qualquer usuário Clerk cria empresas).@fixr/mock(formulário só fazconsole.log), status do web não batem com o enum da API, não há endpoint de mudança de status nem UI de catálogo (fluxos e2e ficaramfixme).api_key_expiration_too_far).Issue relacionada
Closes #95
Closes #102, closes #103, closes #104, closes #105, closes #106, closes #107, closes #108, closes #109, closes #110, closes #111, closes #112, closes #113, closes #114, closes #115, closes #116, closes #117, closes #118, closes #119, closes #120, closes #121, closes #122
Como testar
Depois do merge, marcar
Lint,Types,Unit tests(e idealmenteIntegration tests (server)) como checks obrigatórios na branch protection dedevelop/main.Checklist
bun run check-typespassabun run lintpassa (lint:ci)action(Scope): message🤖 Generated with Claude Code
https://claude.ai/code/session_01JtJ1gJpgdDNQ2xkvhGc5H6
Generated by Claude Code