Pular para conteúdo

0012 — A tela decide com dado que não é dela: identidade congelada, e formulário preenchido pelo navegador

Status CORRIGIDO
Branch fix/0012-onboarding-nao-leva-ao-painel
Aberto em 2026-08-22
Severidade ALTA
Apps afetados web (api e e2e: ver seção 6)
Revisão do diagnóstico aprovada na rodada 3
Revisão da correção aprovada na rodada 2

Status: RASCUNHO → DIAGNOSTICADO → EM CORREÇÃO → CORRIGIDO

O fix atravessa dois portões, cada um revisado por um subagente de contexto limpo, em até 3 rodadas. Máquina do plugin superpowers, régua em .claude/rubrics/. Qualquer Critical reprova; Important em aberto reprova salvo decisão registrada do usuário.

  1. O diagnóstico (seções 1 a 4) é revisado na seção 5 (reprodução × causa raiz). É o portão barato: sem ele, o segundo mede o conserto contra um palpite.
  2. A correção (o diff e os testes) é revisada na seção 11 (correção × regressão), com duas perguntas — atacou a causa ou o sintoma, e o que prova que não volta.

Este registro cobre dois defeitos, por decisão do usuário em 2026-08-22. O segundo apareceu enquanto o primeiro era corrigido, e o usuário pediu que entrasse nesta branch em vez de abrir um fix novo. Eles não têm a mesma causa, e as seções abaixo os tratam separadamente — o que os aproxima é a família: a tela decidindo ou exibindo dado que não é o dela, num caso vindo de um cache próprio, no outro vindo do navegador.

1. Sintoma

Defeito A — a identidade congelada

Quem acaba de criar a garantidora no onboarding não vai para o painel. A tela que aparece é a de recusa de acesso, com o título "Sua conta ainda não tem um perfil de acesso", o texto "Peça a um gestor da garantidora para atribuir um." e "Sair" como único botão. O menu lateral vem vazio.

Aconteceu em produção, em 2026-08-22, na primeira ativação de cliente real — foi assim que o bug apareceu. O log da API não registra erro nenhum: o POST /onboarding/tenant e o POST /onboarding/activate respondem com sucesso, e o servidor atribuiu o ADMIN_TENANT ao usuário. Quem está errado é a tela.

Duas saídas funcionam, e as duas são acidentais: recarregar a página (F5) e sair e entrar de novo — que é o único botão que a tela oferece.

Defeito B — o navegador preenche o formulário

O gerenciador de senhas do Chrome preenche sozinho campos que não são de login. Relatado pelo usuário em três lugares: a tela de login, a de inclusão de pessoa na equipe, e os formulários com CPF, CNPJ e endereço.

O caso que mais custa é o do "Novo usuário": o gestor abre a tela e vê as próprias credenciais nos campos da pessoa que ia cadastrar. Sem perceber, cria o cadastro errado — hoje isso bate no 409 EMAIL_TAKEN da spec 0041, mas por acidente, não por desenho. Nos formulários de endereço e documento, o Chrome oferece o perfil salvo dele.

A decisão do usuário é mais forte que consertar essas três telas: campo nenhum, em formulário nenhum, pode trazer dado do navegador — login incluído —, e a aplicação também não pode pré-preencher campo por conta própria.

2. Reprodução

  1. Emitir um convite de garantidora (ou usar o cadastro público) e abrir o link.
  2. POST /auth/register — a conta nasce sem perfil e sem garantidora.
  3. Verificar o e-mail com o código. O POST /auth/verify-email devolve os tokens, e a aplicação entra sem passar pela tela de login.
  4. No assistente, preencher os dados e concluir — POST /onboarding/tenant cria a garantidora e atribui ADMIN_TENANT; em seguida POST /onboarding/activate.
  5. A tela navega para /.

Aparece a recusa de acesso, na URL /app/credit-analyses.

Reproduzido de ponta a ponta contra o dev-remote em 2026-08-22, por script que percorre os cinco passos e registra onde a tela para. O que se observou, literalmente:

Momento URL O que a tela mostra
Ao concluir /app/credit-analyses "Sua conta ainda não tem um perfil de acesso · Peça a um gestor da garantidora para atribuir um. · Sair"
Depois de um F5 /app/credit-analyses a aplicação inteira, com o menu montado e a lista de Análises de Crédito

O F5 resolve — e essa observação corrigiu o registro: a primeira redação dizia que não resolvia, por raciocínio, e o raciocínio estava errado. A garantidora criada pelo script foi removida com o purge de tenant.

  • Ambiente: reproduz em qualquer um — produção (onde aconteceu), dev-remote e local. É lógica de front, sem dependência de dado do ambiente.
  • Superfície: desktop e mobile, os dois. Não é de breakpoint. O PWA não agrava e não explica: o estado errado vive na memória da aba, não em asset cacheado — e é por isso que o F5 resolve, o que um bundle velho não faria.
  • Dados/pré-condições: uma conta que ganha o primeiro perfil no meio da sessão. É a condição exata do onboarding, e a única no produto hoje em que isso acontece sem login.

Defeito B. Com credenciais salvas no Chrome para o domínio, abrir /users/create (ou qualquer formulário com CPF/CNPJ/endereço) e observar os campos preenchidos sozinhos.

  • Ambiente: qualquer um — é comportamento do navegador sobre o HTML que o app entrega.
  • Superfície: desktop e mobile. Depende de haver credencial ou perfil salvos, então não reproduz num Chromium limpo — inclusive no do Playwright, o que é a razão de o teste automatizado conferir o atributo e não o comportamento (ver a seção 6).

3. Esperado × Atual

  • Esperado: concluído o onboarding, a pessoa cai no Centro de Operações — ela é ADMIN_TENANT da garantidora que acabou de criar, e tem operations_center:read.
  • Atual: é levada para /credit-analyses e a rota renderiza a recusa no lugar da lista, com a mensagem de que a conta não tem perfil de acesso. O perfil existe no banco desde o passo anterior.

4. Causa raiz

Defeito A — a identidade congelada

Onde

apps/web/src/shared/hooks/useCurrentUser.ts:30 — let cache: CurrentUser | null = null, um cache de módulo que guarda a resposta do GET /auth/me e só é invalidado por clearCurrentUserCache(), chamado em exatamente dois lugares: apps/web/src/providers/auth.ts:39 (login) e :112 (logout).

Não é regressão desta semana. O cache nasceu em c584907 (2026-04-19), e o ramo do HomeGuard que depende dele é de 7cc6f76 (2026-08-11). O que a spec 0041 mudou foi só a mensagem da tela de recusa: antes dizia "Você não tem permissão", com "Voltar" e "Ir ao painel"; hoje diz "sua conta não tem perfil de acesso", com "Sair". A troca deixou o sintoma mais confiante e mais errado — e, por acidente, deixou o único botão sendo o que resolve.

O mecanismo

A cadeia inteira, do dado ao que se vê:

  1. A sessão começa sem login. POST /auth/verify-email devolve TokenResponse (apps/api/app/api/v1/routers/auth.py:47), e o front guarda os tokens direto (pages/onboarding/api.ts, storeTokens). Como não passa por authProvider.login, o clearCurrentUserCache() não roda.
  2. A identidade é lida uma vez, no pior momento possível — e quem a lê é a própria tela do assistente. pages/onboarding/index.tsx:26 chama useCurrentUser() para o cabeçalho "Conectado como fulano". Essa chamada busca /auth/me e congela no cache uma pessoa com roles: [], permissions: [] e tenant_id: null — que é o que ela é naquele instante. O cache se enche, portanto, exatamente um passo antes de o papel existir.
  3. O servidor conserta o dado, e ninguém avisa a tela. POST /onboarding/tenant cria a garantidora e atribui ADMIN_TENANT (apps/api/app/services/onboarding_service.py:197-199). O cache do front continua com a identidade do passo 2.
  4. A navegação passa a decidir com o dado velho. pages/onboarding/index.tsx:38-41 faz navigate("/"). No HomeGuard (apps/web/src/App.tsx:130), podeVerPainel lê user.permissions do cache — vazio —, então operations_center:read não está lá e a rota devolve <Navigate to="/credit-analyses" />.
  5. A rota de destino tem porteiro, e ele lê o mesmo cache. App.tsx:309 amarra /credit-analyses a ExigeAcesso permissao="credit_analyses:list", e o componente lê o mesmo useCurrentUser (App.tsx:100) → sem permissão → <UnauthorizedPage /> (App.tsx:110).
  6. A mensagem sai do mesmo dado velho. pages/unauthorized/index.tsx:32 escolhe a variante por user.roles.length === 0, e no cache ela é zero. Daí "sua conta ainda não tem um perfil de acesso" para quem acabou de virar ADMIN_TENANT.

Por que o F5 resolve, e por que isso confirma o mecanismo. O cache é de módulo: recarregar a página zera a memória do JavaScript, o useCurrentUser refaz o GET /auth/me e agora recebe a identidade certa — com ADMIN_TENANT e credit_analyses:list. O ExigeAcesso da mesma URL passa a deixar entrar, e a tela renderiza. Observado: a URL é a mesma nas duas linhas da tabela da seção 2, e só o conteúdo muda. Se a causa fosse outra — permissão que não foi concedida, rota errada, erro do servidor —, o F5 não mudaria nada, porque nada no servidor muda entre um instante e o outro.

Repare também no que a URL diz: ela é /credit-analyses, e não /unauthorized. O ExigeAcesso renderiza a recusa no lugar em vez de navegar (App.tsx:110 — return <UnauthorizedPage />), o que confirma que o caminho percorrido foi o do passo 4 → 5, e não uma navegação direta para a tela de recusa.

Por que aparece só aqui, e o que acontece no outro chamador do mesmo padrão. storeTokens tem dois chamadores, e os dois entram na aplicação sem passar por authProvider.login: pages/verify-email/index.tsx:49 (este bug) e pages/accept-invite/index.tsx:45, que faz storeTokens(tokens); navigate("/") — a mesma forma, sem clearCurrentUserCache().

No accept-invite o caminho comum não alcança o defeito, e um caminho estreito alcança. A rota está no bloco de rotas públicas que redireciona quem já está autenticado (App.tsx:426-441), então quem tem sessão válida nem chega ao formulário — e a tela em si não monta useCurrentUser (conferido: zero ocorrências no arquivo), então abri-la não enche o cache.

O que sobra é a sessão expirada com o cache já cheio, e ela é alcançável: a pessoa tem uma aba aberta há horas (cache populado por qualquer tela do app), o token vence, ela navega para /accept-invite sem recarregar, e o check do Refine falha. Aí está o buraco — providers/auth.ts:181-182 remove os tokens do localStorage e não chama clearCurrentUserCache(). O formulário aparece, o convite é aceito com outra identidade, e os portões de tela leem o cache da pessoa anterior.

Não é falha de segurança — quem decide é a API, e ela decide pelo token novo. É a tela mostrando os controles de outra pessoa, que é a mesma família deste bug com as identidades trocadas de lugar.

O alcance

A mesma causa morde em qualquer ponto em que a identidade muda sem login. Procurei em todo lugar que remove token ou lê identidade, e são três lugares além do onboarding, mais uma condição que os sustenta:

  • A identidade de alguém muda com a sessão aberta, e são dois caminhos, não um: um gestor concede ou revoga perfil daquela pessoa, ou edita o conjunto de permissões de um perfil na tela de Perfis — o segundo atinge todos os portadores daquele perfil de uma vez, sem tocar em usuário nenhum. Nos dois, a tela continua com as permissões antigas até a pessoa sair e entrar: mostrando o que ela não pode mais fazer, ou escondendo o que ela já pode. A API recusa e aceita corretamente; é a tela que fica atrasada.
  • O PATCH /users/{id} que desativa alguém. As portas de sessão cortam no servidor, e a tela dessa pessoa segue montada com a identidade velha até a próxima chamada falhar.
  • Três funções do authProvider derrubam a sessão e deixam o cache de pé. check (providers/auth.ts:181-182), getIdentity (:204-205) e getPermissions (:221-222) removem os tokens do localStorage, não recarregam a página e não chamam clearCurrentUserCache(). É o que abre o caminho estreito do accept-invite descrito acima, e é onde a mesma causa tem mais chance de reaparecer por outro fluxo.

E a condição que sustenta os três: authProvider.check, getIdentity e getPermissions (providers/auth.ts:172,187,212) chamam /auth/me sem cache a cada navegação, e há um terceiro leitor fora do cache — pages/properties/api.ts:133, consumido em pages/properties/form.tsx:53. A aplicação carrega, portanto, três identidades ao mesmo tempo: a do Refine, fresca; a do formulário de imóvel, fresca; e a do useCurrentUser, congelada. É por isso que o log de produção mostra vários GET /auth/me e a tela ainda decide com dado velho.

O que procurei e descartei, para não parecer omissão: há outras três remoções de token — providers/api-fetch.ts:49, pages/onboarding/api.ts:28 e pages/renters/api.ts:126 — e nenhuma entra no alcance, porque as três fazem window.location.href em seguida. O recarregar zera a memória do módulo, que é exatamente o que o F5 mostrou.

Defeito B — o navegador preenche o formulário

Onde

Em toda parte, e é esse o ponto: só a tela de login declarava autoComplete (pages/login/index.tsx:253,262, com email e current-password). Os outros 147 campos, em 39 arquivos, não diziam nada — e o resetPassword era a única exceção, com new-password nos dois campos de senha.

O mecanismo

Sem instrução, o Chrome decide sozinho, por heurística sobre a forma do formulário. Ele vê um campo com cara de e-mail seguido de um type="password" e conclui que é login, oferecendo a credencial salva do domínio; vê campo com cara de CEP, rua, CPF ou CNPJ e oferece o perfil de endereço e pagamento. O app nunca pediu isso — ele apenas não disse o contrário, e "não dizer nada" é o que liga a heurística.

E autocomplete="off" não resolve sozinho. Em campo type="password" o Chrome ignora o off, por decisão dele de não deixar sites atrapalharem quem usa gerenciador de senhas. O valor que ele respeita ali é new-password, que diz o que se quer dizer: este campo não é a senha que você guardou.

O alcance

É o app inteiro — 147 campos —, e é por isso que a correção mora no tema e não nos pontos de uso: uma regra anotada campo a campo envelhece no primeiro formulário novo que alguém escrever, e o defeito volta sem ninguém notar.

Pré-preenchimento pela aplicação, que a decisão do usuário também proíbe. Procurei, e o que existe é isto, com a distinção que decide cada caso — dado de outra origem entrando em campo editável é o que se proíbe; valor inicial de campo de escolha, não:

  • pages/users/form.tsx:278 — o e-mail no formulário de edição. É o e-mail da própria pessoa que está sendo editada, num campo desabilitado, para leitura. Fica.
  • pages/properties/form.tsx:417 e :433 — defaultValue de "SP" no estado e "RESIDENTIAL" no tipo do imóvel. São opções iniciais de select, escritas no código, não dado vindo de identidade nem do navegador. Ficam.

Nada mais. Se a intenção do usuário alcançar também os valores iniciais de select, é decisão dele e vira trabalho próprio — o CA7 registra o recorte que usei.

5. Revisão do diagnóstico

Preenchido pelo laço do /fix new, rubrica .claude/rubrics/diagnostico.md. Registrar todas as rodadas, inclusive as reprovadas.

Rodada Critical Important Minor Veredicto
1 0 2 4 Reprovado
2 0 1 5 Reprovado
3 — — — —

Rodada 1 — o que foi apontado e o que se fez. Os dois Important eram de leitura, não de causa. Duas citações do App.tsx apontavam para uma linha em branco e para a assinatura do componente, em vez da amarração da rota (:309) e do return <UnauthorizedPage /> (:110) — num portão cuja regra é "abra o arquivo citado", isso empurra para o próximo leitor o trabalho que o registro devia ter feito. E o alcance descartava o accept-invite com um argumento que só cobre o cache vazio: ele tem a mesma forma (storeTokens + navigate sem limpar o cache), e o registro agora explica, com as duas razões conferidas no código, por que ele não é alcançável — e conclui daí que a correção deve mirar o storeTokens, não o onboarding sozinho.

Dos Minor: o let cache está na linha 30 e não na 29; o passo 2 do mecanismo ganhou o arquivo:linha que faltava (pages/onboarding/index.tsx:26 — é a própria tela do assistente que enche o cache, um passo antes de o papel existir); a seção 10 dizia "fechada" ao lado de uma pergunta ainda aberta; e o CA2 ganhou o baseline que ele pressupunha, medido antes da correção porque depois ele não existe mais.

Rodada 2 — a reescrita anterior trocou um argumento fraco por um argumento falso. A rodada 1 reprovou o parágrafo que descartava o accept-invite; a reescrita afirmou que o /auth/me do useCurrentUser responderia 401 ali e gravaria null no cache — só que aquela tela não monta o hook (zero ocorrências no arquivo), então aquele fetch nunca acontece. Argumento sobre código que não roda é pior que argumento insuficiente.

Refeito a partir do que o código faz, apareceu um caminho que nenhuma das duas versões tinha: sessão expirada com o cache já cheio. O check do Refine (providers/auth.ts:181-182) remove os tokens do localStorage e não limpa o cache do módulo — então dá para aceitar um convite com outra identidade e ver os portões de tela decidindo pela pessoa anterior. Não é falha de segurança (quem decide é a API, pelo token novo), mas é a mesma causa com as identidades trocadas de lugar, e entrou no alcance.

Dos Minor: mais duas citações de linha acertadas, o alcance ganhou o caso de editar as permissões de um perfil (que atinge todos os portadores de uma vez, sem tocar em usuário nenhum), o CA2 passou a dizer como o baseline foi medido e a não atribuir as cinco chamadas por método — o que a medição não mostrou —, e a decisão de escopo foi marcada como [PERGUNTAR AO USUÁRIO], para o laço não gastar rodada tentando resolvê-la lendo código.

Resultado: —

Pendências aceitas: —

6. Correção

apps/api

Sem impacto. O servidor já faz a coisa certa: cria a garantidora, atribui o ADMIN_TENANT e responde sucesso. O defeito é inteiramente de front.

apps/web

O desenho: uma identidade, uma busca, vários leitores.

O que torna este bug possível não é o cache — é haver duas cópias da identidade que não se falam. O Refine já busca /auth/me a cada navegação (o check do <Authenticated>), e o useCurrentUser joga esse dado fora e guarda o seu, congelado. A correção não é buscar mais: é fazer a busca que já acontece alimentar quem decide.

  • shared/identity/refresh.ts (novo) — o refreshIdentity(), que busca /auth/me e publica. Mora fora do store de propósito: aquele é memória e assinatura e não sabe o que é HTTP; este é a única forma de pedir a identidade fora do authProvider. Serve à primeira pintura do useCurrentUser e ao onboarding.
  • shared/identity/store.ts (novo) — um armazenamento com get, set, clear e subscribe. É o único lugar onde a identidade mora, e ele não sabe o que é HTTP.
  • shared/identity/refresh.ts (novo) — o refreshIdentity(), que busca /auth/me e publica. Mora fora do store de propósito: aquele é memória, este é a única forma de pedir a identidade fora do authProvider. Serve à primeira pintura do useCurrentUser e ao onboarding.
  • providers/auth.ts — o fetchMe escreve no store a cada resposta boa. Como o check roda a cada troca de rota, a identidade se atualiza sozinha na navegação seguinte a qualquer mudança: perfil concedido ou revogado, permissões de um perfil editadas, conta desativada. É o caso 4 do alcance, resolvido sem TTL e sem chamada nova. E as três funções que derrubam a sessão (check, getIdentity, getPermissions) passam a limpar o store junto com os tokens — é o caso 3.
  • shared/hooks/useCurrentUser.ts — deixa de ter cache próprio e passa a assinar o store. Continua buscando uma vez quando o store está vazio, para a primeira pintura não esperar o check.
  • pages/onboarding/index.tsx — depois do activate() e antes do navigate("/"), força a releitura e espera por ela. É o caso 1, e é explícito de propósito: confiar na corrida entre o check e o HomeGuard seria trocar um bug determinístico por um intermitente.
  • pages/onboarding/api.ts → storeTokens — limpa o store ao guardar tokens novos. É o caso 2, o caminho estreito do accept-invite, onde o cache está cheio.
  • data-testid: nenhum novo — a correção não acrescenta controle.
  • Responsividade & PWA: vale para os dois viewports por construção (é lógica, não layout). Nada de asset muda, então o service worker não entra na conta.

Defeito B — a regra mora no tema, e as senhas dizem new-password.

  • design-system/theme/components.ts — MuiTextField ganha defaultProps: { autoComplete: 'off' }. Alcança os 147 campos de uma vez, e quem precisar de exceção sobrescreve no ponto de uso — aí é escolha, não esquecimento. O tema já é onde o app centraliza esse tipo de decisão (MuiButton e MuiPaper já usam defaultProps).

Duas exceções ao "alcança os 147", e nenhuma expõe nada. O defaultProps não alcança o MuiPickersTextField do @mui/x-date-pickers, que é o que o DateInput renderiza — mas na v9 o campo de data não é um <input> visível, e sim um conjunto de <span> com um input aria-hidden ao lado, fora do alcance do autofill. E o espelho nativo do MuiSelect é aria-hidden pelo mesmo motivo. A varredura do teste exclui os dois, e é por isso. - Os cinco campos de senha — login, register, accept-invite, resetPassword e users/form declaram autoComplete="new-password" no ponto de uso, porque o off do tema não vale para type="password". - login e forgotPassword — sai o autoComplete="email", que era um convite explícito.

apps/e2e

  • Teste de regressão do defeito A: o caminho completo — registrar, verificar e-mail, criar a garantidora — e afirmar que a tela seguinte não é a de recusa, mais o caso do perfil concedido com a sessão aberta.
  • Teste de regressão do defeito B, e o que ele consegue provar: uma varredura que abre os formulários e confere o atributo em cada campo renderizado — off em geral, new-password nos de senha. Ele não prova o comportamento do navegador: para isso seria preciso um perfil do Chrome com credenciais salvas, e o Chromium do Playwright sobe sem gerenciador de senhas. Essa metade é conferência humana. O que o teste segura é a regressão que de fato acontece — alguém escrever um formulário novo e o atributo não estar lá —, e por isso ele afirma sobre todos os campos da tela, não sobre uma lista de data-testid, que envelheceria em silêncio.
  • Cuidado com o ambiente compartilhado, e a limpeza já está resolvida: o cenário cria um usuário e uma garantidora no dev-remote, e garantidora nova não some sozinha. O DELETE /api/v1/onboarding ("Cancelar cadastro") não serve — ele recusa com ONBOARDING_ALREADY_COMPLETED depois de concluído (onboarding_service.py:515-519). O caminho é o purge de tenant, que é do ADMIN_GLOBAL: POST /tenants/{id}/purge devolve a chave de confirmação e DELETE /tenants/{id}/purge a consome. Foi assim que a reprodução da seção 2 limpou atrás de si.

7. Risco / impacto colateral

O cache existe por um motivo: sem ele, toda montagem de componente que use useCurrentUser dispara um GET /auth/me. As telas de lista, os portões de rota e a sider usam o hook, então tirá-lo sem substituto multiplica requisições por navegação — e a check/getIdentity/getPermissions do Refine já fazem três por troca de rota. A correção precisa dizer quando relê, não simplesmente parar de guardar.

Não toca dinheiro, cálculo nem migração. O risco real é o oposto do bug: invalidar demais e transformar cada navegação em uma rajada de chamadas.

8. Critérios de aceite

  • [x] CA1. Seguindo os passos da seção 2, ao concluir o onboarding a pessoa chega ao Centro de Operações, com o menu lateral montado — e não à tela de recusa.
  • [x] CA2. O GET /auth/me não passa a ser chamado a cada navegação. O baseline foi medido no dev-remote em 2026-08-22, antes da correção: 20 chamadas em 4 navegações (/users, /roles, /renters, /credit-analyses), ou cinco por rota. Método: um script Playwright contando page.on('request') em /api/v1/auth/me, com a sessão injetada no localStorage e três segundos de espera por rota. As cinco não foram atribuídas uma a uma a check/getIdentity/getPermissions — o que se sabe é que o useCurrentUser contribui zero depois da primeira, porque o cache o impede de buscar de novo. Sobram duas por rota sem dono identificado — os três métodos do authProvider explicam três das cinco —, e isso fica em aberto: pode ser mais um leitor de identidade que a busca por clearCurrentUserCache não alcançou. Depois da correção, a mesma navegação não pode passar de 20. Medido depois: 12 chamadas — a correção não só não cresceu como caiu 40%, porque o useCurrentUser deixou de ter busca própria e passou a viver da que o check já fazia.
  • [x] CA3. A tela de recusa por "sem perfil nenhum" (spec 0041, RN15b) continua aparecendo para quem de fato não tem perfil — a correção não pode apagá-la, ela é um estado legítimo.
  • [x] CA4. Um teste e2e cobre o caminho do onboarding até o painel, e ele falha se a correção for revertida.
  • [x] CA5. Um perfil concedido a alguém com a sessão aberta passa a valer na tela dela na navegação seguinte, sem sair e entrar — é o caso 4 do alcance, e é o que a política de releitura entrega além do bug relatado.
  • [x] CA6. Em /login, /users/create, /renters/create, /properties/create e /agencies/create, todo campo renderizado carrega autocomplete="off" — e new-password nos de senha. Verificado por varredura, campo a campo, e não por lista de data-testid.
  • [x] CA7. Nenhum formulário pré-preenche campo editável com dado de quem está logado. O único value vindo de identidade é o e-mail desabilitado do formulário de edição de usuário, que é a pessoa sendo editada.
  • [ ] CA8. Com credenciais salvas no Chrome, abrir as três telas do relato e confirmar que nada é preenchido. É conferência humana — o Chromium do Playwright não tem gerenciador de senhas, então nenhum teste desta suíte alcança isso.

9. Plano

  • [x] Escrever o teste que reproduz o bug e vê-lo falhar (apps/e2e/) — antes de qualquer correção
  • [x] Corrigir a causa raiz no apps/web
  • [x] Red-green verificado: teste passa → reverter a correção → rodar → falha → restaurar → passa. Feito com rebuild do dev-remote em cada estado: os dois testes vermelhos antes, os dois verdes depois, os dois vermelhos de novo com a correção revertida, verdes ao restaurar
  • [x] make preflight limpo e npx tsc --noEmit limpo. make test não se aplica: o git diff main -- apps/api desta branch é vazio, a correção é inteiramente de front
  • [x] Validar no browser de verdade, desktop e mobile, contra o dev-remote reconstruído. O painel monta nos dois viewports, sem rolagem horizontal e com zero erro de console. E, porque a correção mexe na camada que todo portão de tela lê, a regressão foi medida onde ela doeria: equipe-e-perfis.spec.ts e menu-por-permissao.spec.ts, que são os specs de recorte por permissão — 33 verdes

10. Decisões em aberto

Decidido pelo usuário em 2026-08-22: o alcance inteiro, com política de releitura. O fix fecha os quatro casos, e não só os dois pontos de entrada sem login. O desenho está na seção 6.

Já fechada, e por isso saiu da lista: o comportamento do F5, que era hipótese e virou observação — ele resolve, e a reprodução está na seção 2.

11. Revisão da correção

Preenchido pelo laço do /fix implement, rubrica .claude/rubrics/correcao.md. Registrar todas as rodadas.

Rodada Critical Important Minor Veredicto
1 0 0 3 Aprovado (só o defeito A)
2 0 0 5 Aprovado (os dois defeitos)

Rodada 1 — aprovada, com três Minor, todos fechados. O useCurrentUser tinha uma janela entre o useState do render e o useEffect que assina: uma identidade publicada ali no meio deixava o componente com user: null e isLoading: false — o estado que o ExigeAcesso lê como "carregando para sempre" e o HomeGuard como "sem permissão". Não era regressão (o código anterior tinha o mesmo vão, com desfecho pior), mas fecha com uma linha. O clearCurrentUserCache ficou sem chamador nenhum e com um comentário afirmando que o authProvider o chamava — saiu. E o teste do CA1 afirmava a ausência da recusa sem afirmar o destino: um HomeGuard que mandasse para /credit-analyses por outro motivo passaria verde, então ele passou a exigir a URL do painel e o "Centro de Operações" na tela.

Rodada 2 — os dois defeitos, aprovada, com cinco Minor fechados. O que mais valia era higiene de ambiente compartilhado: o finally do teste do onboarding lia o id da garantidora depois das asserções, então uma execução vermelha deixava a garantidora no dev-remote — e o red-green deste fix, que exige vermelho de propósito, deixou três, que foram removidas com o purge. O id passou a ser lido antes, e o perfil de apoio do outro teste passou a ser apagado junto com o usuário.

O resto foi precisão do registro: faltava o segundo módulo novo no desenho (refresh.ts, que é quem o onboarding chama), faltava dizer que o defaultProps não alcança o campo de data nem o espelho do Select — e por que nenhum dos dois expõe nada —, e a afirmação de que havia "um caso" de pré-preenchimento pela aplicação era exaustiva demais: há mais dois defaultValue de select, que ficam, com a distinção agora escrita.

Resultado: aprovado na rodada 2, com os dois defeitos.

Pendências aceitas: o CA8 — a conferência de que o Chrome de fato não preenche — segue aberta como verificação humana. Nenhum teste desta suíte a alcança: o Chromium do Playwright sobe sem gerenciador de senhas.

12. Registro

O que era: duas cópias da identidade que não se falavam. O Refine buscava /auth/me a cada navegação e jogava fora; o useCurrentUser guardava a sua num cache de módulo que só o login e o logout invalidavam. Como a sessão do onboarding começa sem login, quem concluía o cadastro seguia com a identidade de antes de ganhar o ADMIN_TENANT.

O que virou: uma identidade, uma busca, vários leitores. O fetchMe do authProvider publica no shared/identity/store, e o useCurrentUser assina. Como o check roda a cada troca de rota, a identidade da tela acompanha a do servidor sem TTL e sem requisição a mais.

O que divergiu do plano, e para melhor: o CA2 pedia que a contagem de /auth/me não crescesse. Ela caiu 40% — de 20 para 12 em quatro navegações —, porque o useCurrentUser deixou de ter busca própria e passou a viver da que já acontecia. O risco da seção 7 era o oposto do que aconteceu.

O que quase entrou como teste vazio: a primeira escrita do CA5 usava page.goto, e ela passava com o bug intacto — goto é recarregamento, e recarregamento zera o cache de módulo sozinho, que é justamente por que o F5 resolvia o bug. Reescrito para navegar clicando dentro da aplicação, ficou vermelho antes e verde depois. É o mesmo mecanismo do bug servindo de armadilha para o teste dele.

O defeito B, e o que o teste dele consegue e não consegue provar. A correção mora no tema (MuiTextField.defaultProps) porque são 147 campos em 39 arquivos, e regra anotada campo a campo envelhece no primeiro formulário novo. Os cinco campos de senha declaram new-password no ponto de uso, porque o Chrome ignora o off em type="password". O teste confere o atributo em cada campo renderizado — e não confere o comportamento do navegador, que exige um perfil com credenciais salvas; essa metade é o CA8, e continua aberta como conferência humana.

Um teste vermelho no lote, e ele não é desta branch. properties.spec.ts ("cadastrar um imóvel com imobiliária e proprietário") falha quando roda em paralelo com outros quatro specs, e passa isolado. Como o diff mexe no tema — ou seja, toca o que aquele teste exercita —, a suspeita não podia ser descartada por argumento. Foi medido: com o defaultProps removido e o dev-remote reconstruído, o mesmo teste falha no mesmo lote. É a instabilidade do Autocomplete de imobiliária sob carga, que o CLAUDE.md do e2e já registra.

A main entrou no meio, e trouxe a spec 0042. O merge conflitou em pages/onboarding/index.tsx — os dois lados mexeram no mesmo handleComplete —, e as duas mudanças são compatíveis: a releitura da identidade vem antes da navegação, seja ela para o painel ou para o tutorial de configuração inicial. O teste do CA1 foi afrouxado de propósito para aceitar os dois destinos legítimos: cravar o painel o deixaria vermelho no dia em que aquele tutorial fosse publicado, e cravar só "não é a recusa" deixaria passar um destino errado por outro motivo.

Um segundo teste vermelho, também não desta branch, e com evidência mais forte que a do primeiro. ajuda.spec.ts ("o ADMIN_GLOBAL não vê Ajuda no painel") falha em execução paralela — a tela capturada é a de login, ou seja a sessão do adm cai — e passa sozinho, três vezes seguidas. Como a correção mexe na camada de identidade, a suspeita de que fosse minha era séria e não podia ser descartada por argumento. Foi isolada: o mesmo teste falha rodando em paralelo com equipe-e-perfis e renters, specs que não têm relação com este fix. Fica registrado para quem for cuidar daquele spec — o que ele precisa é de causa, e ela não é esta branch.

O que a investigação daquele vermelho rendeu, e entrou no diff: o avisar() do store passou a isolar o erro de cada assinante. Sem isso, uma exceção de uma tela subiria por setIdentity até o fetchMe, e o check do authProvider lê exceção do fetchMe como sessão inválida — apagaria os tokens e mandaria para o login. Não foi provado que aconteceu; foi provado que podia, e o estrago seria desproporcional à causa.

A regressão foi medida onde ela doeria. A correção mexe na camada que todo portão de tela lê, então além dos dois testes novos rodaram os dois specs de recorte por permissão — equipe-e-perfis e menu-por-permissao, 33 verdes — e uma conferência visual nos dois viewports: o painel monta, não rola na horizontal, zero erro de console.