Merge pull request #638 from fsolla/OPS35+-restaurar-pin-de-visibilidade-css-das-colunas-de-rede

test(OPS35+): restaurar pin de browser da visibilidade CSS das colunas de rede em Territórios
This commit is contained in:
Francisco Solla 2026-08-11 01:23:44 +00:00 committed by GitHub
commit 02a33f1bc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 120 additions and 1 deletions

View file

@ -2,6 +2,8 @@
**Recently resolved (2026-08-10):** **OPS37** (Issue #605) — o container Postgres compartilhado **não é mais recriado** por um worktree vizinho: `pnpm db:start` virou `scripts/db-start.mjs` (CLI) sobre `scripts/lib/db-start.mjs` — preflight "container `teqo-postgres-1` Up + `healthy`/`starting` → **no-op** (compose nem roda); senão, `compose -p teqo up -d --wait --wait-timeout 120 postgres` como antes. **Causa raiz provada por medição:** o bind mount **relativo** `./docker/postgres/init` do compose resolve um path absoluto por worktree → `config_hash` diferente por worktree (OPS37 `39e89956…` / B195 `cf3cb522…` / C101 `917e11bb…`; diff do `config` = só o `source:` do bind) → qualquer `compose up` cru de outro worktree fazia `replace` e matava dev+e2e de todos (`terminating connection due to administrator command`) — o incidente se repetiu **ao vivo durante o gate**: o worktree `work/4` recriou o container no meio da suíte int (17 testes mortos; os 3 arquivos re-rodaram verdes e a suíte completa 691/691 passou em seguida). **Verificação do aceite:** `StartedAt` inalterado após `db:start` repetido, inclusive rodado de **outro worktree** (D9, hash diferente) — o caso que antes recriava. Recriação legítima (bump de imagem/config) segue por `pnpm db:start --force-recreate` (explícito; não testado live para não matar a run e2e do D9 em voo — flag é pass-through de 1 arg). `worktree.mjs` provisionamento delega para a mesma lib (mesmo risco no `worktree next`; `dockerComposeUp`/`SHARED_COMPOSE_PROJECT` órfãos removidos). Skip deliberado em `starting` (container que um peer acabou de criar): **espera** o boot ficar `healthy` (60s, polling 2s) em vez de recriar — e cai no compose up se travou; **não** skip em `unhealthy`/sem healthcheck (volta à config atual exatamente uma vez); `db:stop`/`db:doctor`/compose file/CI intactos. Unit novos (`dbStart.unit.spec.ts`, 17: matrix running×health + parse do inspect + wrapper mockado — skip não chama compose, argv do up/force-recreate, espera de `starting`). Sem migration/access/Consent/UI. Intenção [`container-postgres-compartilhado-compose-replace.md`](plans/container-postgres-compartilhado-compose-replace.md); impl [`container-postgres-compartilhado-compose-replace-impl.md`](plans/container-postgres-compartilhado-compose-replace-impl.md).
**Recently resolved (2026-08-10):** **OPS35+** (Issue #616) — restaurado o pin de browser que o OPS35 documentou como perdido: a **visibilidade CSS real** das colunas de rede (Assessor / Liderança / Dobradinha) e dos sortables de `/campanha/territorios` a 2200px. Spec novo mínimo `tests/e2e/campaignTerritoriesColumns.e2e.spec.ts` (projeto `campaign`, corpo exato do teste 3 do spec deletado — `getByRole('columnheader')` visível por rung container-query `@min-[Nrem]/territory-list`, Cobertura `count 0`); divisão de trabalho com o HTTP preservada (presença + classes de rung continuam no `campaignTerritoriesHttp`). Manifesto `e2e-affected`: entrada territórios agora mapeia os 2 specs **e** o arquivo dono das rungs `TerritoryListColumns.tsx` (uma mudança de rung acorda o pin; o seletor faz união, specs de municípios continuam acordando). Prova de não-vazio por mutação (rung `advisor` → `@min-[200rem]` fez o spec falhar em "Assessor"; revertido). Sem migration/access/Consent/UI/CSS. Intenção [`restaurar-pin-visibilidade-colunas-territorios.md`](plans/restaurar-pin-visibilidade-colunas-territorios.md); impl [`restaurar-pin-visibilidade-colunas-territorios-impl.md`](plans/restaurar-pin-visibilidade-colunas-territorios-impl.md).
**Recently resolved (2026-08-10):** **B195** (Issue #591) — wizard "Registrar pedido" completo (A5, adiado no B85): o passo final saiu do stub e virou formulário funcional no shell do wizard — município no header do app (chrome `municipalityLabel`), **sem título grande de passo** (`stepTitle={null}` + `contentFocus="none"`), Tipo → Atividade (opcional) → **campo único** "O que você precisa?"; salvar ("Abrir demanda") → toast + retorno à origem (`returnPath`). **Mudança de modelo da demanda:** o cadastro (wizard e `/demandas/nova`, sem divergência) tem UM campo de texto (`description` required, min 2, max 4000 — constante `CAMPAIGN_DEMAND_BODY_MAX_LENGTH`); o **título virou derivado por IA** (`src/utilities/ai/campaignDemandTitle.ts` — `generateText` DeepSeek `deepseek-v4-flash` como o Sollinha, timeout 4s, sem key → `null`, prompt com rótulo do tipo) com **fallback = texto truncado** (`src/lib/demandTitle.ts`, puro: colapsa whitespace, ≤160) no create e **keep-previous na edição** (novo `updateCampaignDemandRecord` + editor inline no detalhe — recalc só quando a descrição muda; slug preservado pela invariante canônica); `campaignDemandCreateSchema` perdeu `title` (só os drafts de demanda da atividade mantêm título digitado — vertical fora de escopo, schema desacoplado). **Unicidade de slug defensiva só no create das forms** (`ensureUniqueDemandSlug`, opt-in `context.campaignDemandUniqueSlug` fail-closed em transação ativa — precedente `skipContactPhoneInvariant`): probe único da janela `-2…-20` + advisory lock `campaign-demand-slug:<base>` contra race; activity drafts/admin mantêm a constraint estrita (contrato de rollback C90 pinado em teste). Page: branch `register-demand` no router + **staff gate ANTES do load do escopo** (`gate: 'staff'` → líder vai a `/campanha/contatos`, B43 — o 403 do scope load pré-existente virava erro não determinístico; stub `WizardMunicipalitySelectedStub` removido, placeholder "em breve" apagado). **Bug latente corrigido no hook compartilhado** `useCampaignFormSuccessToast` (dependência `[state.message, state.status]` → `[state]`: segundo save com a mesma mensagem — caso do editor — não disparava toast/refresh). Copy compartilhada em `schemas/campaignDemand.ts`; parser único `parseCampaignDemandCreateFormData` (`utilities/demand/`). **Sem migration** (título continua required, preenchido pelo record). Testes: unit do fallback/limites exatos; int (create deriva, colisão `-2`/`-3`, min 2, update keep-previous/skip, leader edit bloqueado, pin "sem `DEEPSEEK_API_KEY`" no `beforeAll`); e2e novo `campaignRegisterDemand` (fluxo completo com título na lista, back-nav à busca, lockdown de líder). Intenção [`registrar-pedido-wizard.md`](plans/registrar-pedido-wizard.md); impl [`registrar-pedido-wizard-impl.md`](plans/registrar-pedido-wizard-impl.md).
**Recently resolved (2026-08-10):** **OPS33** (Issue #595) — `worktree next` **claima deterministicamente** antes de criar o worktree: mesma fila/ordem e lock otimista do `pnpm agent:claim`, com o claim extraído para `scripts/lib/agent-github.mjs` (`claimIssue` — lock + flip + comentário; `claimBriefLines` — brief compartilhado; `claimQueueEntry` — shape para o reopen; `claimTargetVerdict` — decisão pura testada) e o `agent-claim.mjs` virou CLI fino com a **mesma saída**; claim falhou (fila vazia/race/`--issue` inválido) → motivo e saída **sem worktree órfão** (branch validada antes do claim; hint de falha pós-claim). `--issue <N>` claima a Issue direcionada (`ready`) ou **reabre** uma já claimada (`in-progress` — sem re-claim, idempotente; recupera também falha pós-claim); a saída imprime o brief e avisa "já claimada — NÃO rodar `pnpm agent:claim`". O **launch** do terminal agora envia `/work-issue --issue <N>` (prompt citado na diretiva) e a função `worktree()` de `.agents/shell/worktree.sh` passou a **tokenizar a diretiva via xargs** (quote-aware, nunca eval) — o valor do `--prompt` carrega espaço. `--stay` suprime cd+launch mas o claim acontece; o comando `/worktree` roda o mesmo script (claim incluso, sem launch). Guard `-B` contra reset de commits de sessão no reopen. Pool intacto (`claimIssueForPool` coordenado). Docs: AGENTS.md ("Per-worktree environments"), header/uso do script, `.opencode/commands/worktree.md`, skill `worktree-next-issue` (sai "read-only"), AGENT-OPS. Sem migration/dados/access/UI. Intenção [`worktree-next-claima-issue-deterministicamente.md`](plans/worktree-next-claima-issue-deterministicamente.md); impl [`worktree-next-claima-issue-deterministicamente-impl.md`](plans/worktree-next-claima-issue-deterministicamente-impl.md).

View file

@ -0,0 +1,61 @@
# Impl: Restaurar pin de visibilidade CSS das colunas de rede em Territórios
Status: aprovado
Atualizado em: 2026-08-10
Issue: #616
Intenção: docs/plans/restaurar-pin-visibilidade-colunas-territorios.md
Appetite restante: herdado (~0,25 dia eng)
## Leitura da intenção
- **Outcome:** um spec de browser mínimo pina que, a 2200px, os columnheaders de rede (Assessor / Liderança / Dobradinha) e o restante do contrato (sortables) de `/campanha/territorios` estão **visíveis** — visibilidade CSS real via container queries (`@min-[Nrem]/territory-list:table-cell`) — e Cobertura permanece oculta; verde no job e2e existente.
- **O que NÃO negociar:** nada de re-migrar a família; não duplicar asserções do `campaignTerritoriesHttp` (presença no HTML + classes de rung continuam lá); zero mudança de CSS/UI; spec mínimo (só este comportamento).
- **O que reavaliar:** a letra do manifesto `e2e-affected` da intenção ("rota `territorios` → specs `['campaignTerritoriesHttp', 'campaignTerritoriesColumns']`") — o prefixo de rota não acorda o spec quando o **arquivo das rungs** muda (ver D2; as rungs vivem em `src/components/campaign/municipality/TerritoryListColumns.tsx`, hoje mapeado para specs de municípios).
## Abordagem recomendada
```mermaid
flowchart LR
Spec[campaignTerritoriesColumns.e2e.spec.ts<br/>1 teste — corpo do antigo teste 3] --> Login[campaign.login<br/>viewport 2200x1000 antes do login]
Login --> Goto[goto /campanha/territorios]
Goto --> Sort[sortables visíveis<br/>Ordenar por 2022/Captura/2026/Classe/Assessoria]
Sort --> Net[rede visível<br/>Assessor/Liderança/Dobradinha exact]
Net --> Cobertura[Cobertura count 0]
Manifest[e2e-affected: territórios<br/>+campaignTerritoriesColumns<br/>+prefixo TerritoryListColumns] --> Spec
```
**Opções consideradas:** D1 (corpo do spec) A | B · D2 (manifesto) A | B
**Recomendação:** corpo **exato** do antigo teste 3 (D1-A) + manifesto com o spec **e** o prefixo do arquivo das rungs (D2-B) — porque o corpo restaurado já foi aceito por produto (B175) e o prefixo extra é o que faz o pin acordar exatamente quando o CSS que ele protege muda.
**Rejeitadas:** D1-B (versão enxuta só com as 3 colunas de rede) porque perde o pin de "Cobertura oculta" e dos sortables sem ganho de custo; D2-A (só a letra da intenção) porque deixa o spec mudo exatamente para a regressão que o motivou.
### Componentes / mudanças
- **`tests/e2e/campaignTerritoriesColumns.e2e.spec.ts`** (novo, projeto `campaign` — casa `/campaign.*\.e2e\.spec\.ts/`): um teste que restaura o corpo do teste 3 do spec deletado (`campaignTerritories.e2e.spec.ts` em `1f31d1b6^`), com o mesmo comentário B175 e a mesma ordem: `page.setViewportSize({ width: 2200, height: 1000 })` → `campaign.login(page, email, password)` → `page.goto('/campanha/territorios')` → loop de sortables (`getByRole('columnheader', { name: new RegExp(\`^Ordenar por ${header}\`) })`visível para`['2022', 'Captura', '2026', 'Classe', 'Assessoria']`) → loop da rede (`columnheader`exact`['Assessor', 'Liderança', 'Dobradinha']`visível) →`columnheader` `'Cobertura'`com`toHaveCount(0)`. Cabeçalho do arquivo documenta a divisão de trabalho com o spec HTTP (presença + rungs no HTML são do HTTP; visibilidade em viewport é browser) e por que ele existe (OPS35+).
- **`scripts/lib/e2e-affected-manifest.mjs`** (entrada territórios): `specs: ['campaignTerritoriesHttp']` → `['campaignTerritoriesHttp', 'campaignTerritoriesColumns']` e `prefixes` ganha `src/components/campaign/municipality/TerritoryListColumns` (arquivo das rungs; startsWith prefixo-de-arquivo é válido no contrato do manifesto — mesmos checks unit de `e2eAffectedManifest.unit.spec.ts` seguem passando).
- **Migration:** sem migration.
- **Access / Consent:** sem mudança.
- **UI:** Impeccable A — N/A (infra de testes; sem UI nova).
## Fases verificáveis
1. **Spec + manifesto** — criar o spec novo e a entrada do manifesto. Prova de não-vazio (muta): com `@min-[60rem]/territory-list` de `advisor` temporariamente alterado para `@min-[200rem]`, o spec deve falhar em `Assessor`; reverter. Depois rodar os pins unit afetados: `pnpm test:unit tests/unit/e2eAffectedManifest.unit.spec.ts tests/unit/ciSkipInvariants.unit.spec.ts tests/unit/testAffected.unit.spec.ts`.
2. **E2e local** — `pnpm test:e2e tests/e2e/campaignTerritoriesColumns.e2e.spec.ts` (dev, worktree: `teqo_wt35_test`, porta 3135) verde; conferir que o job CI continua verde no PR (o spec novo entra no `campaign` project).
3. **Gates** — `pnpm gate:fast` (lint + tsc + unit), `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`; entrega via `pnpm push`.
## Rabbit holes / Não escopo (engenharia)
- **Re-migrar famílias ou tocar o `campaignTerritoriesHttp`** — fora de escopo; a equivalência HTTP continua como está.
- **Pinar rungs das outras listas** (municípios, lideranças…) — rabbit hole de produto já catalogado na intenção; o pin de municípios já existe (`campaignMunicipalityResponsiveColumns`).
- **Cobrir mudanças de `tailwind.config`/`globals.css` no manifesto** — gap do OPS5 conhecido, não desta issue; uma regressão de container-query Tailwind que quebre a rung também quebra o pin via `TerritoryListColumns.tsx` quando o arquivo mudar.
## Riscos e mitigação
- **Flake de viewport/container:** o corpo é exatamente o que passou em CI por semanas antes do OPS35 (2200px com sidebar expandida deixa o container acima de 72rem). Sem estado novo, sem interação — risco baixo.
- **Custo do job e2e:** +1 teste browser (~5–10 s dev; <1 s prod) — o trade do OPS35 foi explícito: viewports continuam em browser.
- **Prefixo novo no manifesto muda o alcance do `selectE2eSpecs`:** mudanças em `TerritoryListColumns.tsx` passam a rodar os 2 specs de territórios **além dos** specs de municípios (o seletor faz união de entradas) — é o comportamento desejado (a regressão de CSS de territórios acorda o pin de territórios).
## Aceite de engenharia
- [x] Aceite de produto da intenção ainda coberto (corpo do teste 3 restaurado; nada de CSS/HTTP tocado)
- [x] Invariantes AGENTS/engineering-standards (spec novo mapeado no manifesto; unit pins verdes; sem twin de fixtures — `campaign.login` reusado)
- [x] Testes de domínio previstos: spec e2e novo + prova de não-vazio (mutação revertida) + pins unit do manifesto

View file

@ -67,8 +67,11 @@ export const E2E_AFFECTED_MANIFEST = [
`${CAMPAIGN_APP}/territorios`,
'src/components/campaign/tour',
'src/utilities/territory',
// OPS35+ — the network-column rungs live in this file; a rung regression
// must wake the CSS-visibility browser pin, not only the municipios specs.
'src/components/campaign/municipality/TerritoryListColumns',
],
specs: ['campaignTerritoriesHttp'],
specs: ['campaignTerritoriesHttp', 'campaignTerritoriesColumns'],
},
{
prefixes: [`${CAMPAIGN_APP}/conceitos`],

View file

@ -0,0 +1,53 @@
import { expect, test } from './fixtures/campaignE2EFixtures.js'
/**
* OPS35+ — restores the browser pin the HTTP migration dropped. OPS35 migrated
* `campaignTerritories` to the browserless HTTP mode (`campaignTerritoriesHttp`)
* because its assertions were server-rendered content; its one browser-only
* concern — CSS rung visibility at a viewport — was documented as lost at the
* time. That loss is a real hole: the network columns hide behind container
* queries (`@min-[Nrem]/territory-list:table-cell`, TerritoryListColumns.tsx),
* and the HTTP spec only pins presence + rung classes in the HTML, never the
* visual effect. This spec is that pin back: at a wide viewport the read-only
* network columns and every rung-gated sortable are actually visible, and
* Cobertura is not rendered at this width. It carries the exact body of the
* deleted spec's test 3 (accepted product behavior since B175) — nothing else,
* no CSS, no HTTP duplication.
*/
test.describe('Territórios de Identidade — colunas de rede (browser)', () => {
test('a wide panel surfaces the read-only network columns; Cobertura stays hidden by default', async ({
campaign,
page,
}) => {
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Rede Larga'),
})
const password = coordinator.password
const email = coordinator.email!
await page.setViewportSize({ width: 2200, height: 1000 })
await campaign.login(page, email, password)
await page.goto('/campanha/territorios')
// B175 — the responsive rungs are all visible at a wide panel (P0 + Captura
// + network + Classe + Assessoria). Cobertura is P3 AND hidden in the picker.
// Sortable headers are announced "Ordenar por <label> (…)" — match by prefix;
// the read-only network headers are plain, so match them exactly (never by
// substring, since "Assessor" is a prefix of "Assessoria").
for (const header of ['2022', 'Captura', '2026', 'Classe', 'Assessoria']) {
await expect(
page.getByRole('columnheader', { name: new RegExp(`^Ordenar por ${header}`) }),
`header ${header}`,
).toBeVisible()
}
for (const header of ['Assessor', 'Liderança', 'Dobradinha']) {
await expect(
page.getByRole('columnheader', { name: header, exact: true }),
`header ${header}`,
).toBeVisible()
}
await expect(page.getByRole('columnheader', { name: 'Cobertura' })).toHaveCount(0)
})
})