mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
refactor(OPS30): fixes do /simplify — pessoas no prewarm, login com sub-budgets, retry da demanda via existência, B24 no helper do arquivo
Review 3× read-only (0 P1): - setup: /campanha/pessoas faltava (campaignPeople); log do 1º hit quando o retry salva; comentário do budget ancorado no medido. - login: sub-budgets 15+5+30 (antes 30+5+30 estourava o test timeout e credencial errada morria de timeout genérico); retry bifurca pela URL real (dashboard/fora do login/re-submit) — re-submit cego contra página que saiu do login falhava com fill na dashboard. - :713: retry cego criaria duplicata que o UNIQUE do slug refuta — o retry consulta por título e navega ao detail quando o 1º POST criou mas o redirect se perdeu. - B24: locator reusa visibleMunicipalityButton (escapeRegExp + fronteira de nome, 23/435 com prefixo) e ensureWideMunicipalityList re-rodado pós-reload (padrão do irmão dobradinhas). - :1063: waitForResponse com timeout 5s para o toPass decidir. Re-prova: campaignMunicipalities 22/22 (arquivo atual, B193+OPS29 do main) ×3 consecutivos em dev a 2 workers (load 5-17).
This commit is contained in:
parent
02beea808b
commit
48088d8162
4 changed files with 76 additions and 35 deletions
|
|
@ -32,14 +32,14 @@ flowchart LR
|
|||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`tests/e2e/setup.e2e.spec.ts`**: lista GET ganha `/campanha/municipios/e2e-prewarm/editar` + os módulos de rota faltantes do audit (ver Fases 1). Lista POST inalterada. Comentário do mecanismo já existe — sem novas cerimônias.
|
||||
- **`tests/e2e/setup.e2e.spec.ts`**: lista GET ganha `/campanha/municipios/e2e-prewarm/editar` + os módulos de rota faltantes do audit (ver Fases 1) + os ajustes pós-gate (POSTs novos, PWA/chat, `/campanha/pessoas`). `prewarmGet` com retry-único e budget explícito (ver ajustes pós-gate).
|
||||
- **`tests/e2e/fixtures/campaignE2EFixtures.ts`** (`campaign.login`, ~546–555): extrai `submitLogin` (fill+click); primeira `waitForURL` no budget padrão; no timeout, se `page.url()` já é `/campanha` → sucesso (idempotência contra double-submit); senão re-fill + re-click + segunda `waitForURL`; segunda falha sobe naturalmente (credencial errada falha duas vezes — retry não mascara defeito real). Comentário documentando o mecanismo do compile-frio.
|
||||
|
||||
### Ajustes pós-gate (evidência de execução, 2026-08-10 — mesma sessão)
|
||||
|
||||
Sob carga real de worktrees paralelas (load ~50, 16 núcleos — gate do OPS22 rodando em paralelo), a evidência mostrou que **o próprio setup original falhava** (`socket hang up` no 2º GET — a classe de abort do dev server atinge o prewarm quando o compile frio aborta a conexão em voo; reproduzido com a lista original via stash). Dois ajustes no escopo aprovado:
|
||||
|
||||
1. **`setup.e2e.spec.ts` — `prewarmGet` com retry-único por GET** (a falha aborta a conexão, mas o compile acontece; o retry cai quente; a asserção `ok()` roda no retry — segunda falha é regressão real). Substituiu `test.slow()` (180 s) por `test.setTimeout(420_000)`: 23 módulos × até ~60 s de compile frio sob carga (medido: `/campanha/contatos` = 57 s) + headroom do retry; no CI (build de produção quente) termina em segundos, o budget não morde.
|
||||
1. **`setup.e2e.spec.ts` — `prewarmGet` com retry-único por GET** (a falha aborta a conexão, mas o compile acontece; o retry cai quente; a asserção `ok()` roda no retry — segunda falha é regressão real). Substituiu `test.slow()` (180 s) por `test.setTimeout(420_000)`: 27 módulos, total medido ~3–6 min de compile frio sob carga (medido: `/campanha/contatos` = 57 s a load ~50) + headroom do retry; no CI (build de produção quente) termina em segundos, o budget não morde.
|
||||
2. **`campaign.login` — guard de idempotência refinado**: o check `page.url().startsWith(baseURL/campanha)` casava também `/campanha/login` (prefixo) — trocado por grace-wait de 5 s do destino exato antes do re-submit (cobre a navegação que caiu logo após o timeout do 1º wait; re-fill no dashboard seria erro falso).
|
||||
3. **Prewarm de alvos de redirect + suggest** (a classe compile-frio não é só `page.goto`): o submit "Abrir demanda" redireciona para `/campanha/demandas/[slug]` — módulo fora da lista (audit de goto não vê redirects) → adicionado `e2e-prewarm`; e o suggest do overlay FAB POSTa `/campanha/home-search` — fora da lista POST → adicionado.
|
||||
4. **Refator mínimo do spec `campaignMunicipalities` (evidência de execução)**: três testes falhavam deterministicamente em dev — todos da classe de abort do compile-frio no cliente:
|
||||
|
|
@ -50,9 +50,18 @@ Sob carga real de worktrees paralelas (load ~50, 16 núcleos — gate do OPS22 r
|
|||
|
||||
### Evidência (2026-08-10, máquina compartilhada com 4–5 worktrees ativos, load 5–70)
|
||||
|
||||
- **Aceite — `campaignMunicipalities` 17/17 ×3 consecutivos em dev a 2 workers** (runs 16:00–16:03, load 5–18; incl. `:78` e `:518`). Runs sob load 40–70 continuam caindo por starvation de CPU (renders RSC > 60 s) — classe fora do escopo (não é compile-frio); a evidência foi coletada nas janelas quietas.
|
||||
- **Aceite — `campaignMunicipalities` 22/22 (arquivo atual, incl. B193 ×4 + OPS29) ×3 consecutivos em dev a 2 workers** (runs 16:28–16:39, load 5–17; incl. `:78` e `:518`; as 3 runs verdes anteriores com 17 testes foram absorvidas pelo rebase que trouxe B193/OPS29 do main — re-prova com o arquivo atual). Runs sob load 40–70 continuam caindo por starvation de CPU (renders RSC > 60 s) — classe fora do escopo (não é compile-frio); a evidência foi coletada nas janelas quietas.
|
||||
- **P4-L — `campaignLeaderships` 3/3 em dev a 2 workers (load 4–10) + 1/1 com `E2E_PROD=1`** (build local em `.next/e2e`; 1.6 s/1.9 s). TECH-DEBT row 59 fechado com a evidência; TESTING.md caveat atualizado.
|
||||
- **Setup**: prewarm completo (26 GETs + 16 POSTs) passando em dev e prod; retry-único absorveu aborts reais (socket hang up sob load).
|
||||
- **Setup**: prewarm completo (27 GETs + 16 POSTs) passando em dev e prod; retry-único absorveu aborts reais (socket hang up sob load).
|
||||
|
||||
### Ajustes do /simplify (3 revisores read-only, 2026-08-10 — 0 P1, P2/P3 aplicados)
|
||||
|
||||
- `/campanha/pessoas` (campaignPeople) entrou no prewarm GET — o audit por grep de goto tinha passado batido (audit refeito por grep completo).
|
||||
- `prewarmGet` loga o status do 1º hit quando o retry salva (rastro do masking residual).
|
||||
- Login: sub-budgets explícitos somando <60 s (15+5+30) — antes o 2º wait nunca completava dentro do test timeout e credencial errada morria de "Test timeout" genérico; retry agora bifurca pela URL real (dashboard → return; fora do login → throw com a URL; login → re-submit) — o re-submit cego contra página que saiu do login falhava com fill na dashboard.
|
||||
- B24: locator reusa `visibleMunicipalityButton` (escapeRegExp + fronteira — nomes com prefixo compartilhado, 23/435) e `ensureWideMunicipalityList` re-rodado após o reload (padrão do irmão dobradinhas).
|
||||
- `:713`: o retry cego do submit criaria duplicata que o UNIQUE do `campaignDemand.slug` refuta — o retry agora consulta por título e navega direto ao detail quando o 1º POST já criou (redirect perdido).
|
||||
- `:1063`: `waitForResponse` com timeout curto (5 s) — sem ele o budget do toPass queimava no wait e o erro apontava a classe errada.
|
||||
- **`docs/TECH-DEBT.md`** (row 59, P4-L): status `open — Pass 4…` → `closed <data> (evidência: spec B34 ×3 dev 2 workers + 1× E2E_PROD=1 verdes, <data>)` — só após a evidência coletada.
|
||||
- **`docs/TESTING.md`** (linha 15): caveat "chip-cell spec currently fails deterministically under `pnpm dev`" vira referência ao fechamento (o ledger é a fonte; doc não pode contradizer o ledger fechado).
|
||||
- **`docs/plans/hardening-e2e-dev-compile-frio-impl.md`**: este arquivo (commitado na entrega).
|
||||
|
|
|
|||
|
|
@ -545,13 +545,13 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
await ensureWideMunicipalityList(page)
|
||||
|
||||
// The trend control renders in both the desktop cell and the always-mounted
|
||||
// mobile-card tree (B158); scope to the list container and the visible copy
|
||||
// after the wide stage settles, the way the sibling journeys do.
|
||||
const trendButton = municipalityContainer(page)
|
||||
.getByRole('button', {
|
||||
name: new RegExp(`^Editar tendência política em ${municipality.name}`),
|
||||
})
|
||||
.filter({ visible: true })
|
||||
// mobile-card tree (B158); `visibleMunicipalityButton` scopes to the list
|
||||
// container and the visible copy after the wide stage settles, the way the
|
||||
// sibling journeys do.
|
||||
const trendButton = visibleMunicipalityButton(
|
||||
page,
|
||||
`Editar tendência política em ${municipality.name}`,
|
||||
)
|
||||
await expect(trendButton).toBeVisible()
|
||||
await trendButton.click()
|
||||
const trendPopover = page.locator('[data-slot="popover-content"]')
|
||||
|
|
@ -569,14 +569,15 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.reload()
|
||||
// The reload restarts the RSC stream: the filtered row re-renders in waves
|
||||
// (each one moves the button, so a click keeps waiting for stability) and
|
||||
// hydration lags SSR paint (a pre-hydration click is a silent no-op, P3-C
|
||||
// class). Settle the stream first, then click only while the popover is
|
||||
// closed — the trigger TOGGLES, so a blind click-per-iteration would close
|
||||
// an open popover forever. The reopened popover proves the persisted save
|
||||
// (server round trip), not just the optimistic chip.
|
||||
await page.waitForLoadState('networkidle')
|
||||
// The reload restarts the RSC stream and the B158 stage: the filtered row
|
||||
// re-renders in waves (each one moves the button, so a click keeps waiting
|
||||
// for stability) and hydration lags SSR paint (a pre-hydration click is a
|
||||
// silent no-op, P3-C class). Re-settle the wide stage first, then click
|
||||
// only while the popover is closed — the trigger TOGGLES, so a blind
|
||||
// click-per-iteration would close an open popover forever. The reopened
|
||||
// popover proves the persisted save (server round trip), not just the
|
||||
// optimistic chip.
|
||||
await ensureWideMunicipalityList(page)
|
||||
const reopened = page.locator('[data-slot="popover-content"]')
|
||||
await expect(async () => {
|
||||
if (!(await reopened.first().isVisible())) await trendButton.click()
|
||||
|
|
@ -759,12 +760,25 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
await page.getByLabel('O que você precisa?').fill(demandText)
|
||||
// The create action is a server action — not prewarmable — and its first
|
||||
// dev-mode invocation can be aborted by the compile full-page reload, which
|
||||
// leaves the form filled but never navigates. The retry re-submits once the
|
||||
// action is warm; an aborted first POST created nothing, and if the POST
|
||||
// landed but the redirect was lost, a duplicated create is harmless (same
|
||||
// title, fixture cleanup owns both).
|
||||
// leaves the form filled but never navigates. On the retry: if the demand
|
||||
// already exists (the POST landed but the redirect was lost — `slug` is
|
||||
// unique, a second submit would fail the constraint), go straight to its
|
||||
// detail; otherwise re-submit against the now-warm action.
|
||||
await expect(async () => {
|
||||
await page.getByRole('button', { name: 'Abrir demanda' }).click()
|
||||
if (page.url().includes('/campanha/demandas/nova')) {
|
||||
const existing = await campaign.payload.find({
|
||||
collection: 'campaignDemand',
|
||||
where: { title: { equals: demandTitle } },
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
if (existing.docs.length > 0) {
|
||||
await page.goto(`${campaign.baseURL}/campanha/demandas/${existing.docs[0].slug}`)
|
||||
} else {
|
||||
await page.getByRole('button', { name: 'Abrir demanda' }).click()
|
||||
}
|
||||
}
|
||||
await expect(campaignPageChrome(page, demandTitle)).toBeVisible({ timeout: 10_000 })
|
||||
}).toPass({ timeout: 30_000 })
|
||||
await expect(page.getByRole('button', { name: 'Aprovar' })).toBeVisible()
|
||||
|
|
@ -1530,6 +1544,7 @@ test.describe('Municípios — FAB overlay polish (B126)', () => {
|
|||
resp.url().includes('/campanha/home-search') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.request().postDataJSON()?.mode === 'suggest',
|
||||
{ timeout: 5_000 },
|
||||
)
|
||||
await search.blur()
|
||||
await search.focus()
|
||||
|
|
|
|||
|
|
@ -626,10 +626,15 @@ export const campaignFixture: CampaignE2EFixtureValue = async ({}, runFixture) =
|
|||
// aborting the login POST before it lands (the mechanism documented
|
||||
// in setup.e2e.spec.ts). A budget-only wait cannot recover an aborted
|
||||
// submit, so on a navigation timeout we re-submit ONCE: the action is
|
||||
// warm by then. The short grace wait before re-submitting keeps the
|
||||
// retry idempotent when the first navigation landed just past the
|
||||
// wait (re-filling on the dashboard would throw a bogus error). A
|
||||
// second failure surfaces — wrong credentials fail both attempts.
|
||||
// warm by then. The grace wait absorbs a navigation that landed just
|
||||
// past the first wait, and the URL branch refuses a blind re-submit
|
||||
// against a page that left the login screen. Sub-budgets sum below
|
||||
// the 60 s test timeout, so a real credential failure surfaces as a
|
||||
// crisp wait error, not a generic test timeout. A second failure
|
||||
// surfaces — wrong credentials fail both attempts.
|
||||
const dashboardURL = `${baseURL}/campanha`
|
||||
const onDashboard = (url: string) =>
|
||||
url === dashboardURL || url === `${dashboardURL}/`
|
||||
const submitLogin = async () => {
|
||||
await page.getByLabel('E-mail ou celular').fill(identifier)
|
||||
await page.getByLabel('Senha').fill(password)
|
||||
|
|
@ -640,13 +645,20 @@ export const campaignFixture: CampaignE2EFixtureValue = async ({}, runFixture) =
|
|||
}
|
||||
await submitLogin()
|
||||
try {
|
||||
await page.waitForURL(`${baseURL}/campanha`)
|
||||
await page.waitForURL(dashboardURL, { timeout: 15_000 })
|
||||
} catch {
|
||||
try {
|
||||
await page.waitForURL(`${baseURL}/campanha`, { timeout: 5_000 })
|
||||
await page.waitForURL(dashboardURL, { timeout: 5_000 })
|
||||
} catch {
|
||||
const current = page.url()
|
||||
if (onDashboard(current)) return
|
||||
if (!current.startsWith(`${baseURL}/campanha/login`)) {
|
||||
throw new Error(
|
||||
`Login retry aborted: page left the login screen (${current}).`,
|
||||
)
|
||||
}
|
||||
await submitLogin()
|
||||
await page.waitForURL(`${baseURL}/campanha`)
|
||||
await page.waitForURL(dashboardURL, { timeout: 30_000 })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { expect, test } from '@playwright/test'
|
|||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
||||
// 26 route modules compiled cold, each up to ~60 s under parallel-worktree
|
||||
// load (measured 2026-08-10, load ~50: /campanha/contatos took 57 s), plus a
|
||||
// one-shot retry per GET for the dev-server abort class below. On CI the
|
||||
// production build answers warm in seconds, so the budget never bites there.
|
||||
// 27 route modules compiled cold; measured ~3-6 min total under
|
||||
// parallel-worktree load (load ~50, 2026-08-10: /campanha/contatos alone
|
||||
// took 57 s), plus a one-shot retry per GET for the dev-server abort class
|
||||
// below. On CI the production build answers warm in seconds, so the budget
|
||||
// never bites there.
|
||||
test.setTimeout(420_000)
|
||||
// A cold compile can make Next dev abort an in-flight request (socket hang
|
||||
// up, measured on the original list under load) — the route compiles anyway,
|
||||
|
|
@ -15,6 +16,9 @@ test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
|||
const prewarmGet = async (path: string) => {
|
||||
const first = await request.get(`${baseURL}${path}`).catch(() => undefined)
|
||||
if (first?.ok()) return first
|
||||
if (first) {
|
||||
console.info(`[prewarm] first attempt ${path} answered ${first.status()} — retrying once`)
|
||||
}
|
||||
const retry = await request.get(`${baseURL}${path}`).catch(() => undefined)
|
||||
expect(retry?.ok(), `Failed to prewarm ${path}`).toBe(true)
|
||||
return retry
|
||||
|
|
@ -47,6 +51,7 @@ test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
|||
'/campanha/atividades/nova',
|
||||
'/campanha/atualizacoes',
|
||||
'/campanha/offline',
|
||||
'/campanha/pessoas',
|
||||
// PWA artifacts every authenticated page fetches on load — a cold compile
|
||||
// here mid-suite full-page-reloads connected clients (ERR_ABORTED class).
|
||||
'/campanha/manifest.webmanifest',
|
||||
|
|
|
|||
Loading…
Reference in a new issue