ops28(OPS28): worktrees rodam o seed mínimo no provisionamento — e2e local espelha a CI
- scripts/worktree.mjs: provision() aplica migrate + db:seed:minimal nos bancos dev e test (paridade com a CI, que roda o par antes do e2e); --no-migrate pula os dois (o seed depende do catálogo migrado); fallback sem Docker também migra+seeda o teqo/teqo_test compartilhado (com warning de sobrescrita dos pins). - Guard de regressão tests/e2e/seedParity.setup.e2e.spec.ts no projeto setup: falha cedo com mensagem clara se o banco de teste não estiver seedado; evidência = users do seed (MINIMAL_CAMPAIGN_USERS), não os pins de priority — o cleanup dos fixtures e2e reseta priority por round-robin persistente. - scripts/lib/test-affected-core.mjs: scripts/worktree.mjs em HIGH_RISK_EXACT. - Docs: AGENTS.md, skill local-database, CHANGELOG-AGENTS.md, impl plan. Closes #584
This commit is contained in:
parent
91a0aca5f0
commit
0047d2637c
8 changed files with 213 additions and 15 deletions
|
|
@ -29,10 +29,10 @@ VMs Cloud **não rodam** `docker compose` / `pnpm db:start`. Postgres nativo: `.
|
||||||
Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay]` provisions each worktree with a deterministic isolated env (`next`/`plan`/`new` print `cd <dir>` by default — apply it, or pass `--stay` to suppress; `--go` is still accepted as a no-op):
|
Parallel agents must not fight over port 3000, the shared `teqo_test`, or a second Postgres container. `pnpm worktree next [--stay]` provisions each worktree with a deterministic isolated env (`next`/`plan`/`new` print `cd <dir>` by default — apply it, or pass `--stay` to suppress; `--go` is still accepted as a no-op):
|
||||||
|
|
||||||
- `slot` = numeric part of the issue code (`C15` → 15, `B164` → 164; hash fallback without digits; bumped on collision among live worktrees).
|
- `slot` = numeric part of the issue code (`C15` → 15, `B164` → 164; hash fallback without digits; bumped on collision among live worktrees).
|
||||||
- Dev server port `3100+slot`; databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests), created on the **one shared container** with the full migration chain applied (one-time, ~30 s each).
|
- Dev server port `3100+slot`; databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests), created on the **one shared container** with the full migration chain **and the minimal seed** applied (one-time, ~30 s each; OPS28 parity with CI, which runs `migrate → seed:minimal` before e2e — a worktree's `pnpm test:e2e` mirrors CI).
|
||||||
- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL, NEXT_PUBLIC_SITE_URL, **PLAYWRIGHT_BASE_URL** — both URL vars derived from the same slot port so a parallel e2e run never falls back to :3000; see `worktreeEnvFileContents` in `scripts/lib/worktree-env.mjs`). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` (or `pnpm worktree plan` for the /plan-issue worktree, `pnpm worktree new` for a neutral one) to regenerate.
|
- Writes `.env.local` (DATABASE_URL, PORT, NEXT_PUBLIC_SITE_URL, PLAYWRIGHT_BASE_URL, PAYLOAD_SECRET + Blob/VAPID copied from the main repo) and `.env.test.local` (test DATABASE_URL, NEXT_PUBLIC_SITE_URL, **PLAYWRIGHT_BASE_URL** — both URL vars derived from the same slot port so a parallel e2e run never falls back to :3000; see `worktreeEnvFileContents` in `scripts/lib/worktree-env.mjs`). Both carry the marker `# teqo worktree env (generated by pnpm worktree next)`; **manual env files are never overwritten** — remove them and re-run `pnpm worktree next` (or `pnpm worktree plan` for the /plan-issue worktree, `pnpm worktree new` for a neutral one) to regenerate.
|
||||||
- `--no-migrate` skips the migration runs; `pnpm worktree kill` drops the worktree's generated databases (best-effort, `DROP DATABASE … WITH (FORCE)`).
|
- `--no-migrate` skips the migration runs **and the minimal seed** (the seed depends on the migrated catalog); `pnpm worktree kill` drops the worktree's generated databases (best-effort, `DROP DATABASE … WITH (FORCE)`).
|
||||||
- Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning and no port override.
|
- Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning and no port override — migrate + seed still run on the shared databases (same parity).
|
||||||
- Pure derivation: `scripts/lib/worktree-env.mjs` (unit-tested in `tests/unit/worktree.unit.spec.ts`).
|
- Pure derivation: `scripts/lib/worktree-env.mjs` (unit-tested in `tests/unit/worktree.unit.spec.ts`).
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ These are product/architecture constraints — not a ban on editing existing cod
|
||||||
- `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit.
|
- `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit.
|
||||||
- To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data).
|
- To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data).
|
||||||
- `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory.
|
- `pnpm db:doctor` diagnoses local database connectivity for the dev AND test targets: it names who holds the port, points at exited/crash-looping containers, and flags Postgres containers sharing one data volume (two postmasters on one PGDATA corrupt the WAL — this destroyed the local cluster's checkpoint on 2026-07-25; the canonical setup is ONE compose container on 5432 hosting `teqo` + `teqo_test`). `pnpm dev` runs the same preflight via `guard-dev-db` and refuses to start with a named remedy instead of a later ECONNREFUSED. `db:start`/`db:stop` pin the compose project with `-p teqo`, so they always target that ONE container even from a worktree directory.
|
||||||
- **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests) created on the shared container with the full migration chain applied; writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan`/`new` print `cd <dir>` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd <main>` so the shell never sits in a destroyed directory. **In the interactive terminal the shell function sets `TEQO_WORKTREE_TERMINAL=1` and `next`/`plan`/`new` additionally print a `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt <command>]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue` already sent (the OPS25 command runs the whole cycle), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). The launch is without `exec` (exiting the TUI returns to the shell in the worktree, no auto-`kill`), `--stay` suppresses both the cd and the launch, the presets are constants in `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, unit-tested), and **the `/worktree` opencode command never launches a TUI** (no marker — no nested TUI). `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree** so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-<bag>` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-<n>` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `<Code>-<slug>`, so a later `next` never collides in branch or slot. `worktree new [bag] [--stay]` (OPS27, 2026-08-10) is the NEUTRAL sibling: a worktree with **no pre-defined function\*\* (explore an idea, chat, plan without registering anything) — also a DIFFERENT one per invocation, branch `work/<bag>` (suffixed `-2`/`-3` on collision) or the next free sequential `work/<n>` (`WORK_BRANCH_PREFIX`/`workBranchName`, same `namespaceBranchName` core as `plan`); the lowercase `work/…` prefix collides with neither `next` nor `plan`, and the provisioning is identical with `purpose: 'new'` (env comment `gerado por pnpm worktree new`; the frozen env marker keeps its historical `…next` text). `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo_<worktree>_test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning.
|
- **Per-worktree environments (`pnpm worktree next`, 2026-08-08; default-go 2026-08-10):** parallel agents each get a deterministic isolated env — no fighting over port 3000, the shared `teqo_test`, or a second Postgres container. `worktree next [--stay]` provisions: a dev-server port `3100+slot` (slot = numeric part of the issue code, hash fallback, bumped on collision among live worktrees), plus dedicated databases `teqo_wt<slot>` (dev) and `teqo_wt<slot>_test` (tests) created on the shared container with the full migration chain AND the minimal seed applied (`pnpm migrate` + `pnpm db:seed:minimal` per database — OPS28 parity: CI runs the same pair before e2e, so a worktree's `pnpm test:e2e` mirrors CI instead of failing on seed-dependent specs; the seed is idempotent and local-guarded, and `--no-migrate` skips both); writes `.env.local` + `.env.test.local` (gitignored, marker `# teqo worktree env (generated by pnpm worktree next)`; manual envs are never overwritten) and copies `PAYLOAD_SECRET`/Blob/VAPID from the main repo. **`next`/`plan`/`new` print `cd <dir>` as the last line BY DEFAULT** — the caller applies it (shell function `worktree()` in `.agents/shell/worktree.sh`, or the `/worktree` opencode command); `--stay` suppresses the cd, `--go` explicit stays accepted as a no-op (it was the old default), and `kill` ends with `cd <main>` so the shell never sits in a destroyed directory. **In the interactive terminal the shell function sets `TEQO_WORKTREE_TERMINAL=1` and `next`/`plan`/`new` additionally print a `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt <command>]` directive BEFORE the `cd` line, which the function executes after the cd (OPS26, 2026-08-10; OPS31 for `plan`): `next` opens the opencode TUI in the worktree with `/work-issue` already sent (the OPS25 command runs the whole cycle), `plan` opens with `/plan-issue` already sent (the TUI lands straight in the planning flow), `new` opens with no prompt ("apenas conversar", no skill — the opencode CLI's `--prompt` always auto-submits, there is no prefill-without-submit; that stays an upstream-only feature request, annotated in the opencode proposition). The launch is without `exec` (exiting the TUI returns to the shell in the worktree, no auto-`kill`), `--stay` suppresses both the cd and the launch, the presets are constants in `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, unit-tested), and **the `/worktree` opencode command never launches a TUI** (no marker — no nested TUI). `worktree plan [bag] [--stay]` (2026-08-08) is the sibling for the `/plan-issue` skill: **each invocation creates a DIFFERENT planning worktree** so parallel `/plan-issue` sessions never share one — branch `plans/plan-issue-<bag>` when a `bag` slug is given (suffixed `-2`/`-3` if that name is taken) or the next free sequential `plans/plan-issue-<n>` when not (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`); the lowercase `plans/…` prefix deliberately never matches `next`'s uppercase-led `<Code>-<slug>`, so a later `next` never collides in branch or slot. `worktree new [bag] [--stay]` (OPS27, 2026-08-10) is the NEUTRAL sibling: a worktree with **no pre-defined function\*\* (explore an idea, chat, plan without registering anything) — also a DIFFERENT one per invocation, branch `work/<bag>` (suffixed `-2`/`-3` on collision) or the next free sequential `work/<n>` (`WORK_BRANCH_PREFIX`/`workBranchName`, same `namespaceBranchName` core as `plan`); the lowercase `work/…` prefix collides with neither `next` nor `plan`, and the provisioning is identical with `purpose: 'new'` (env comment `gerado por pnpm worktree new`; the frozen env marker keeps its historical `…next` text). `kill` drops the worktree's generated databases best-effort. Tests read `.env.test.local` after `.env.test` (vitest.setup.ts / playwright.config.ts / db-doctor / gate-ci mirror the layering), and `assertTestDatabase` accepts `teqo_test` or `teqo_<worktree>_test` on localhost only — still fail-closed on remote. Pure derivation lives in `scripts/lib/worktree-env.mjs` (unit-tested). Without Docker (Cursor Cloud) it degrades to the shared `teqo`/`teqo_test` with a warning — and still runs migrate + seed mínimo on those shared databases (same parity; the seed re-pins campaignGoals and 5 municipalities there, overwriting manual tweaks).
|
||||||
- To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below.
|
- To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below.
|
||||||
- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below.
|
- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**Recently resolved (2026-08-10):** **OPS28** (Issue #584) — `worktree next`/`plan` passam a aplicar o **seed mínimo** além das migrations em cada banco provisionado (`migrate` + `db:seed:minimal` por banco, dev e test) — paridade com a CI, que roda o mesmo par antes do e2e: o e2e de worktree deixa de divergir do de CI (specs dependentes dos pins `priority: 'alta'` de `salvador-ze-1`/`camacari` — FAB suggest e `campaignSavedFilters` — falhavam só localmente com banco migrado cru 435×normal, o padrão que o ledger TECH-DEBT row 19 ficha como "masks real failures"). `--no-migrate` pula migrate **e** seed (o seed depende do catálogo migrado); re-provisionamento continua idempotente (upserts). Fallback sem Docker (Cursor Cloud) também roda migrate+seed nos `teqo`/`teqo_test` compartilhados (decisão de produto do gate: paridade total). **Guard de regressão** `tests/e2e/seedParity.setup.e2e.spec.ts` no projeto `setup` do Playwright: falha cedo com mensagem clara ("rode `pnpm migrate && pnpm db:seed:minimal`") se o banco de teste não estiver seedado — dependentes do setup pulam, nada de spec vermelho confuso; a evidência checada são os **users do seed** (`MINIMAL_CAMPAIGN_USERS`), deliberadamente não os pins `priority: 'alta'` — o cleanup dos fixtures e2e reseta `priority` dos municípios que toca (round-robin por sequência persistente), então pins morrem legitimamente após runs repetidas no mesmo banco de worktree e só o `pnpm worktree next` re-aplica (guard nunca checaria pins); `scripts/worktree.mjs` entrou em `HIGH_RISK_EXACT` (mudança no provisionador roda a suíte cheia na CI). Sem migration/access/Consent/UI. Intenção [`worktree-seed-minimal-paridade-e2e-ci.md`](plans/worktree-seed-minimal-paridade-e2e-ci.md); impl [`worktree-seed-minimal-paridade-e2e-ci-impl.md`](plans/worktree-seed-minimal-paridade-e2e-ci-impl.md).
|
||||||
|
|
||||||
**Recently resolved (2026-08-10):** **OPS34** (Issue #599) — o job `e2e` do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~67 s fixos + build 172 s + testes 196 s) roda agora **em 2 shards paralelos** (`--shard=N/2`, matrix `fail-fast: false`, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). **Cadeia de projetos do Playwright (`setup → campaign → frontend → admin`) + prewarm do `setup` viraram dev-only:** `playwright.config.ts` ganhou `isProdMode = CI || E2E_PROD=1` (mesma condição do `webServer.command`, agora reusada) — em prod o projeto `setup` sai do array (arquivo não casado = não coletado) e as `dependencies` zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: `--list` mostra `[setup]` só sem CI; smoke e2e dev rodou setup→campaignAuth). `ci-pr.yml`: o job `scope` emite `e2e_matrix`/`e2e_shards` via novo `e2eShardConfig(mode)` em `scripts/lib/test-affected-core.mjs` (fonte única, unit-pinada: full → `[1,2]`/2, selected/none → `[1]`/1 — pagar 2 builds por 3 specs é desperdício); `ci.yml` hardcodeia `[1,2]`/2 com comentário apontando o single-source. `PLAYWRIGHT_WORKERS: 2` explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão **sem** o separador `--` — pnpm repassa o `--` literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o `setup` não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. **Medição pós-merge (main, run 31418206512):** job e2e 435 s → **326 s (shard 1) / 369 s (shard 2)** — shard 2 = fixo 68 s + build 176 s + testes 124 s (a fase de testes caiu de 196 s seriais para 124 s — metade da suite por runner + cadeia removida; o shard 2 ainda pagou retry da flake conhecida de `campaignMunicipalities`; shard 1: testes ~82 s); gate completo do pipeline (checks) ~7,5 → ~6,4 min. Build passou a dominar o shard — gatilho da intenção (opção B, artefato compartilhado) reavaliável quando os testes encolherem mais. Intenção [`ci-e2e-paralelizar-job.md`](plans/ci-e2e-paralelizar-job.md); impl [`ci-e2e-paralelizar-job-impl.md`](plans/ci-e2e-paralelizar-job-impl.md).
|
**Recently resolved (2026-08-10):** **OPS34** (Issue #599) — o job `e2e` do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~67 s fixos + build 172 s + testes 196 s) roda agora **em 2 shards paralelos** (`--shard=N/2`, matrix `fail-fast: false`, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). **Cadeia de projetos do Playwright (`setup → campaign → frontend → admin`) + prewarm do `setup` viraram dev-only:** `playwright.config.ts` ganhou `isProdMode = CI || E2E_PROD=1` (mesma condição do `webServer.command`, agora reusada) — em prod o projeto `setup` sai do array (arquivo não casado = não coletado) e as `dependencies` zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: `--list` mostra `[setup]` só sem CI; smoke e2e dev rodou setup→campaignAuth). `ci-pr.yml`: o job `scope` emite `e2e_matrix`/`e2e_shards` via novo `e2eShardConfig(mode)` em `scripts/lib/test-affected-core.mjs` (fonte única, unit-pinada: full → `[1,2]`/2, selected/none → `[1]`/1 — pagar 2 builds por 3 specs é desperdício); `ci.yml` hardcodeia `[1,2]`/2 com comentário apontando o single-source. `PLAYWRIGHT_WORKERS: 2` explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão **sem** o separador `--` — pnpm repassa o `--` literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o `setup` não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. **Medição pós-merge (main, run 31418206512):** job e2e 435 s → **326 s (shard 1) / 369 s (shard 2)** — shard 2 = fixo 68 s + build 176 s + testes 124 s (a fase de testes caiu de 196 s seriais para 124 s — metade da suite por runner + cadeia removida; o shard 2 ainda pagou retry da flake conhecida de `campaignMunicipalities`; shard 1: testes ~82 s); gate completo do pipeline (checks) ~7,5 → ~6,4 min. Build passou a dominar o shard — gatilho da intenção (opção B, artefato compartilhado) reavaliável quando os testes encolherem mais. Intenção [`ci-e2e-paralelizar-job.md`](plans/ci-e2e-paralelizar-job.md); impl [`ci-e2e-paralelizar-job-impl.md`](plans/ci-e2e-paralelizar-job-impl.md).
|
||||||
|
|
||||||
**Recently resolved (2026-08-10):** **OPS22** (Issue #562) — CI de `main` vermelho determinístico desde o merge do B188 (05:54): os 3 e2e da agenda mobile (`campaignAgendaMobile` :96/:242/:295) falhavam com todas as retries → deploy bloqueado. **Root cause (reproduzida no prod build local + error-contexts com o drawer aberto):** o login e2e roda em **viewport desktop** → o settle de B167 abre o chat (`open=true`) → o **efeito persist do B188** (status `ready` já nasce) grava `{open:true}` no `sessionStorage` → o spec muda para 390px na **mesma aba** → o **restore do B188** reabre o **drawer mobile sozinho** → o modal Radix **aria-hide + inert** a página e cobre o viewport (combobox fora da a11y tree no :242, swipe cai no drawer no :96, clique do seletor bloqueado no :295; teclado/:173 passam por não usarem hit-testing). **Defeito real de produto** (aba desktop → mobile na mesma aba faz o chat "saltar"), não só timing de teste. **Fix na origem:** `SollinhaChatSession` ganhou **`openBy: 'user' | 'settle'`** (opcional — sessão legada sem o campo = `'settle'`, fail-closed; sem bump de versão) e o provider agora distingue **intenção do usuário** (`setOpen`/`toggle` do context value marcam `userToggledOpenRef`; FAB/header/drawer já passam por eles) de **verdade de layout** (settle B167 usa o setter cru): o **restore espera o `measured`** (viewport real) e honra `open:true` em mobile **só quando `openBy==='user'`**; o persist grava `openBy` conforme a intenção. Todos os contratos B188/resize mantidos (reload restaura conversa; drawer aberto por usuário volta aberto; visita mobile nova fechada; crossing ao vivo B167 intocado). Testes: unit (round-trip/invalidez/legado de `openBy`) + **e2e pin OPS22** no spec B188 (login desktop → poll do persist `open:true` → viewport 390 → `/campanha/agenda` → sem dialog + botão do Sollinha visível); spec da agenda intacto (4/4 no prod build). Sem migration/access/Consent/schema. Intenção [`ci-e2e-agenda-mobile-regressao-b188.md`](plans/ci-e2e-agenda-mobile-regressao-b188.md); impl [`ci-e2e-agenda-mobile-regressao-b188-impl.md`](plans/ci-e2e-agenda-mobile-regressao-b188-impl.md).
|
**Recently resolved (2026-08-10):** **OPS22** (Issue #562) — CI de `main` vermelho determinístico desde o merge do B188 (05:54): os 3 e2e da agenda mobile (`campaignAgendaMobile` :96/:242/:295) falhavam com todas as retries → deploy bloqueado. **Root cause (reproduzida no prod build local + error-contexts com o drawer aberto):** o login e2e roda em **viewport desktop** → o settle de B167 abre o chat (`open=true`) → o **efeito persist do B188** (status `ready` já nasce) grava `{open:true}` no `sessionStorage` → o spec muda para 390px na **mesma aba** → o **restore do B188** reabre o **drawer mobile sozinho** → o modal Radix **aria-hide + inert** a página e cobre o viewport (combobox fora da a11y tree no :242, swipe cai no drawer no :96, clique do seletor bloqueado no :295; teclado/:173 passam por não usarem hit-testing). **Defeito real de produto** (aba desktop → mobile na mesma aba faz o chat "saltar"), não só timing de teste. **Fix na origem:** `SollinhaChatSession` ganhou **`openBy: 'user' | 'settle'`** (opcional — sessão legada sem o campo = `'settle'`, fail-closed; sem bump de versão) e o provider agora distingue **intenção do usuário** (`setOpen`/`toggle` do context value marcam `userToggledOpenRef`; FAB/header/drawer já passam por eles) de **verdade de layout** (settle B167 usa o setter cru): o **restore espera o `measured`** (viewport real) e honra `open:true` em mobile **só quando `openBy==='user'`**; o persist grava `openBy` conforme a intenção. Todos os contratos B188/resize mantidos (reload restaura conversa; drawer aberto por usuário volta aberto; visita mobile nova fechada; crossing ao vivo B167 intocado). Testes: unit (round-trip/invalidez/legado de `openBy`) + **e2e pin OPS22** no spec B188 (login desktop → poll do persist `open:true` → viewport 390 → `/campanha/agenda` → sem dialog + botão do Sollinha visível); spec da agenda intacto (4/4 no prod build). Sem migration/access/Consent/schema. Intenção [`ci-e2e-agenda-mobile-regressao-b188.md`](plans/ci-e2e-agenda-mobile-regressao-b188.md); impl [`ci-e2e-agenda-mobile-regressao-b188-impl.md`](plans/ci-e2e-agenda-mobile-regressao-b188-impl.md).
|
||||||
|
|
|
||||||
76
docs/plans/worktree-seed-minimal-paridade-e2e-ci-impl.md
Normal file
76
docs/plans/worktree-seed-minimal-paridade-e2e-ci-impl.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Impl: Worktrees provisionam os bancos e2e sem o seed mínimo — e2e diverge da CI
|
||||||
|
|
||||||
|
Status: aprovado
|
||||||
|
Atualizado em: 2026-08-10
|
||||||
|
Issue: #584
|
||||||
|
Intenção: docs/plans/worktree-seed-minimal-paridade-e2e-ci.md
|
||||||
|
Appetite restante: herdado (~0,5 dia eng; um outcome verificável)
|
||||||
|
|
||||||
|
## Leitura da intenção
|
||||||
|
|
||||||
|
- **Outcome:** um worktree recém-provisionado (`next` **e** `plan`) roda `pnpm test:e2e` com o mesmo resultado da CI nos specs dependentes de dados do seed (`campaignMunicipalities` FAB suggest, `campaignSavedFilters` e os que tocam `priority`/`expectedVotes`/`campaignGoals`); a CI continua sendo a fonte canônica (nada do que este item faz muda o que a CI executa); o seed roda idempotente e protegido pelo guard de banco local; re-provisionamento não duplica nem corrompe.
|
||||||
|
- **O que NÃO negociar:** não alterar specs e2e para "aceitar dados ausentes" (o provisão é o defeito); nunca rodar seed contra banco não-local; CI intocada; guard de regressão sem enfraquecer specs.
|
||||||
|
- **O que reavaliar:** a hipótese de "Direção no codebase" aponta `scripts/worktree.mjs` + `seed-minimal.mjs` + manifest + setup e2e — confirmada na exploração; a mecânica real das falhas foi verificada no código (FAB suggest filtra `priority === 'alta'` em `rankHomeSearchSuggestMunicipalities`; o recorte `?priority=alta` nasce vazio sem os pins; o nome sugerido vem de `formatMunicipalityActiveFiltersSummary(state)` — o fluxo quebra porque o recorte vazio derruba a linha 49 do spec).
|
||||||
|
|
||||||
|
## Abordagem recomendada
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[provision worktree] --> B[runMigrate dev]
|
||||||
|
B --> C[runSeedMinimal dev]
|
||||||
|
C --> D[runMigrate test]
|
||||||
|
D --> E[runSeedMinimal test]
|
||||||
|
E --> F[env files + porta do slot]
|
||||||
|
F --> G[e2e igual à CI]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opções consideradas:** A | B | C
|
||||||
|
**Recomendação:** A — rodar `pnpm db:seed:minimal` dentro de `provision()` em `scripts/worktree.mjs`, logo após cada `runMigrate`, para os DOIS bancos (dev e test), reusando o mesmo padrão de env (`DATABASE_URL` + `PAYLOAD_SECRET` explícitos) e o mesmo script npm da CI (`pnpm db:seed:minimal` — idempotente por upsert, guard `assertLocalDatabase` já embutido). `--no-migrate` pula migrations **e** o seed (o seed depende do catálogo migrado). Re-provisionamento (`next` num worktree existente) roda tudo de novo — migrate e seed são idempotentes, o aceite "não duplica nem corrompe" vale por construção.
|
||||||
|
**Rejeitadas:**
|
||||||
|
|
||||||
|
- **B — seedar só o banco de teste** (test-only): corrige o e2e mas deixa o `pnpm dev` do worktree divergente (FAB suggest vazio no navegador manual) e cria dois contratos de provisionamento; o custo de seedar o dev é o mesmo.
|
||||||
|
- **C — documentar no AGENTS.md "rode o seed manualmente no worktree"**: trata o sintoma no papel; o ambiente padrão de entrega continua divergente e o agente segue aprendendo a ignorar o e2e vermelho (o padrão exato do ledger TECH-DEBT row 19).
|
||||||
|
|
||||||
|
### Componentes / mudanças
|
||||||
|
|
||||||
|
- **`runSeedMinimal`** (`scripts/worktree.mjs`): irmã de `runMigrate` — `execFileSync('pnpm', ['db:seed:minimal'], { cwd: dir, env: { ...process.env, DATABASE_URL, PAYLOAD_SECRET }, stdio: 'inherit' })`. O script npm já carrega `tsx/esm` + `seed-loader`; o seed faz `loadCliEnv()` sem `override`, então os env explícitos vencem.
|
||||||
|
- **`provision()`** (`scripts/worktree.mjs`): `migrate dev → seed dev → migrate test → seed test`; `skipMigrate` passa a pular os dois (mensagem atualizada).
|
||||||
|
- **Fallback sem Docker** (`writeFallbackEnv` path): **decisão Q1 aprovada no gate — paridade total (opção A)**: `migrate + seed:minimal` também no `teqo`/`teqo_test` compartilhados (ler `PAYLOAD_SECRET` do main env nesse branch; `--no-migrate` pula igualmente).
|
||||||
|
- **Guard de regressão** (Q2 da intenção, **decisão aprovada no gate — opção B, setup e2e**): spec novo `tests/e2e/seedParity.setup.e2e.spec.ts` no projeto `setup` do Playwright (o match `/setup\.e2e\.spec\.ts/` pega o arquivo; projeto `setup` roda primeiro e os projetos dependentes pulam se ele falhar): boota `getPayload({ config })` (padrão já usado nos fixtures) e falha com mensagem clara ("rode `pnpm migrate && pnpm db:seed:minimal` — ou `pnpm worktree next` para reprovisionar") se o banco de teste não estiver seedado. **Evidência checada = `MINIMAL_CAMPAIGN_USERS`** (users sintéticos do seed), deliberadamente NÃO os pins de `priority: 'alta'`: o cleanup dos fixtures e2e reseta `priority` dos municípios tocados (round-robin por sequência persistente — `campaignE2EFixtures.ts`), então pins morrem legitimamente após runs repetidas no mesmo banco de worktree e não são evidência indelével; os users do seed nunca são deletados pelo cleanup (que só deleta o que os fixtures criaram). Mensagem clara em vez de spec vermelho confuso. (Sem extensão extra do manifest unit — o pin existente de slugs fica como está.)
|
||||||
|
- **`HIGH_RISK_EXACT`** (`scripts/lib/test-affected-core.mjs`): adicionar `scripts/worktree.mjs` — mudança no provisionador deve rodar a suíte cheia na CI, não só o diff.
|
||||||
|
- **Docs:** AGENTS.md "Per-worktree environments" (migrations → migrations + seed mínimo, paridade com a CI); header/uso do `scripts/worktree.mjs` (`--no-migrate` agora pula migrate+seed); `.agents/skills/local-database/SKILL.md` (linhas 32/34, mesmo contrato); entrada curta em `docs/CHANGELOG-AGENTS.md`.
|
||||||
|
- **Migration:** nenhuma (sem schema/dados novos).
|
||||||
|
- **Access / Consent:** nenhum (o seed já faz `provisionOnda0ConsentAndPrivacy` + chaves fail-closed — intocado).
|
||||||
|
- **UI:** Impeccable A — N/A (sem UI).
|
||||||
|
|
||||||
|
### Dados → forma (se aplicável)
|
||||||
|
|
||||||
|
Não aplicável — infra de provisionamento; o "dado" é a paridade de estados entre ambientes, verificada pelo guard do setup (pin `alta` no banco de teste) e pelo e2e real.
|
||||||
|
|
||||||
|
## Fases verificáveis
|
||||||
|
|
||||||
|
1. **Tracer — provisionamento** (quota principal): `runSeedMinimal` + chamadas no `provision()` (Docker path) + `--no-migrate` → verificação manual num banco de teste recém-criado (seed aplicado, idempotência em re-execução). Fallback sem Docker segue a decisão do gate (Q1).
|
||||||
|
2. **Guard** — `seedParity.setup.e2e.spec.ts` no projeto `setup` (falha com mensagem clara contra banco não-seedado — incluindo banco nunca-migrado, via try/catch com a mesma mensagem; verde com seed) + `HIGH_RISK_EXACT`.
|
||||||
|
3. **Gates** — `pnpm gate:fast` na iteração; depois `pnpm test:e2e -- tests/e2e/campaignSavedFilters.e2e.spec.ts` + o spec do FAB suggest contra o banco do worktree seedado (reprodução real das duas falhas da intenção); `pnpm test:int` (unit+int); `pnpm check:cycles`/`format:check`/`knip`; entrega com `pnpm push`.
|
||||||
|
|
||||||
|
## Rabbit holes / Não escopo (engenharia)
|
||||||
|
|
||||||
|
- Não tocar em nenhum spec e2e existente (o único arquivo de teste novo é o guard).
|
||||||
|
- Não mudar o CI (`ci.yml`/`ci-pr.yml` já rodam `migrate → seed:minimal` — nada a fazer).
|
||||||
|
- Não alterar o `seed-minimal.mjs` nem o manifest (já idempotentes e corretos).
|
||||||
|
- Não mexer nos bancos compartilhados `teqo`/`teqo_test` no fluxo Docker (fora do contrato de worktree).
|
||||||
|
- Não adicionar lock/transação ao seed para o caso de dois fallbacks seedarem o mesmo `teqo_test` em paralelo (fallback é degradado; colisão de upsert find-before-create é raça rara e ruidosa — documentar no impl, não engenhar).
|
||||||
|
- Gap observado (não deste item): o fallback não grava `PAYLOAD_SECRET` no `.env.local`/`.env.test.local` gerados — o seed receberá o secret explícito no env; o gap do dev no fallback é pré-existente e fica fora de escopo.
|
||||||
|
|
||||||
|
## Riscos e mitigação
|
||||||
|
|
||||||
|
- **Seed adiciona ~10–30 s por banco no provisionamento** (boot do Payload + upserts): aceitável — migrations já levam ~30 s cada; é one-time por banco e idempotente.
|
||||||
|
- **Re-execução em worktree existente**: `next` re-roda `provision()`; migrate+seed idempotentes → sem duplicação (upsert por chave estável/slug/email/phone).
|
||||||
|
- **Guard do setup com banco compartilhado do fallback**: se dois fallbacks rodarem e2e no mesmo `teqo_test`, o guard lê estado do banco — o seed é idempotente; risco residual documentado (raça find-before-create) e ruidoso se ocorrer.
|
||||||
|
- **E2E novo no projeto `setup` pode afetar a duração do setup**: boot do payload ~2–5 s; aceitável (o setup já prewarms 13 rotas).
|
||||||
|
|
||||||
|
## Aceite de engenharia
|
||||||
|
|
||||||
|
- [ ] Aceite de produto da intenção ainda coberto (e2e do worktree = CI; CI intacta; idempotência)
|
||||||
|
- [ ] Invariantes AGENTS/engineering-standards (local-only via `assertLocalDatabase`; nenhum schema/access/Consent tocado)
|
||||||
|
- [ ] Testes previstos: guard e2e (falha clara + verde), unit do manifest estendido, reprodução real dos 2 specs que falhavam
|
||||||
|
|
@ -22,6 +22,7 @@ export const HIGH_RISK_EXACT = new Set([
|
||||||
'src/payload.config.ts',
|
'src/payload.config.ts',
|
||||||
'scripts/seed-minimal.mjs',
|
'scripts/seed-minimal.mjs',
|
||||||
'scripts/lib/seed-minimal-manifest.mjs',
|
'scripts/lib/seed-minimal-manifest.mjs',
|
||||||
|
'scripts/worktree.mjs',
|
||||||
'scripts/lib/test-affected-core.mjs',
|
'scripts/lib/test-affected-core.mjs',
|
||||||
'scripts/lib/e2e-affected-manifest.mjs',
|
'scripts/lib/e2e-affected-manifest.mjs',
|
||||||
'scripts/test-affected.mjs',
|
'scripts/test-affected.mjs',
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,12 @@
|
||||||
* deterministicamente do branch (ver
|
* deterministicamente do branch (ver
|
||||||
* scripts/lib/worktree-env.mjs), para agentes em
|
* scripts/lib/worktree-env.mjs), para agentes em
|
||||||
* paralelo não disputarem porta 3000 nem o
|
* paralelo não disputarem porta 3000 nem o
|
||||||
* `teqo_test` compartilhado. `--no-migrate` pula a
|
* `teqo_test` compartilhado. Migrations E seed
|
||||||
* aplicação das migrations nos bancos novos.
|
* mínimo (`db:seed:minimal`, OPS28) são aplicados
|
||||||
|
* aos dois bancos — paridade com a CI, que roda
|
||||||
|
* `migrate → seed:minimal` antes do e2e.
|
||||||
|
* `--no-migrate` pula migrations E o seed (que
|
||||||
|
* depende do catálogo migrado).
|
||||||
* pnpm worktree plan [bag] [--stay] [--no-migrate]
|
* pnpm worktree plan [bag] [--stay] [--no-migrate]
|
||||||
* cria um worktree de PLANEJAMENTO novo para rodar
|
* cria um worktree de PLANEJAMENTO novo para rodar
|
||||||
* a skill /plan-issue sem ocupar o main — cada
|
* a skill /plan-issue sem ocupar o main — cada
|
||||||
|
|
@ -59,6 +63,8 @@
|
||||||
* `--prompt` (apenas conversar, nenhuma skill).
|
* `--prompt` (apenas conversar, nenhuma skill).
|
||||||
* `--stay` suprime a linha `cd` e a diretiva;
|
* `--stay` suprime a linha `cd` e a diretiva;
|
||||||
* `--go` explícito continua aceito como no-op.
|
* `--go` explícito continua aceito como no-op.
|
||||||
|
* Migrations E seed mínimo nos dois bancos, como
|
||||||
|
* o `next`/`plan` (OPS28).
|
||||||
* pnpm worktree kill [--force] destrói o worktree em que o shell atual está
|
* pnpm worktree kill [--force] destrói o worktree em que o shell atual está
|
||||||
* (recusa worktree sujo sem `--force`) e remove os
|
* (recusa worktree sujo sem `--force`) e remove os
|
||||||
* bancos gerados do worktree (best-effort); por
|
* bancos gerados do worktree (best-effort); por
|
||||||
|
|
@ -240,6 +246,24 @@ const runMigrate = (dir, databaseUrl, payloadSecret) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the minimal synthetic seed on a provisioned database — the same
|
||||||
|
* `pnpm db:seed:minimal` CI runs after `pnpm migrate`, so a worktree's dev and
|
||||||
|
* test databases carry the same content CI's e2e asserts on (OPS28: worktrees
|
||||||
|
* were migrate-only and the e2e diverged). Idempotent (upsert by stable key)
|
||||||
|
* and guarded by the seed's own `assertLocalDatabase` — it never touches a
|
||||||
|
* non-local database. Skipped alongside migrations by `--no-migrate`, since the
|
||||||
|
* seed requires the migrated municipality catalog.
|
||||||
|
*/
|
||||||
|
const runSeedMinimal = (dir, databaseUrl, payloadSecret) => {
|
||||||
|
console.log(`[worktree] ▶ seed mínimo em ${databaseUrl} (paridade com a CI)…`)
|
||||||
|
execFileSync('pnpm', ['db:seed:minimal'], {
|
||||||
|
cwd: dir,
|
||||||
|
env: { ...process.env, DATABASE_URL: databaseUrl, PAYLOAD_SECRET: payloadSecret },
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** Cursor Cloud / no-Docker fallback: shared teqo + teqo_test, no port override. */
|
/** Cursor Cloud / no-Docker fallback: shared teqo + teqo_test, no port override. */
|
||||||
const writeFallbackEnv = (dir, branch) => {
|
const writeFallbackEnv = (dir, branch) => {
|
||||||
const devEnv = [
|
const devEnv = [
|
||||||
|
|
@ -259,13 +283,12 @@ const writeFallbackEnv = (dir, branch) => {
|
||||||
writeFileSync(join(dir, '.env.local'), devEnv)
|
writeFileSync(join(dir, '.env.local'), devEnv)
|
||||||
writeFileSync(join(dir, '.env.test.local'), testEnv)
|
writeFileSync(join(dir, '.env.test.local'), testEnv)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministic per-worktree environment: shared container + own databases +
|
* Deterministic per-worktree environment: shared container + own databases +
|
||||||
* own dev-server port + env files + migrations. Skips everything when a
|
* own dev-server port + env files + migrations + minimal seed. Skips everything
|
||||||
* manual `.env.local`/`.env.test.local` (no marker) exists — never clobbers.
|
* when a manual `.env.local`/`.env.test.local` (no marker) exists — never
|
||||||
* `purpose` labels the env comment (`next` | `plan` | `new`); `issue` is optional and
|
* clobbers. `purpose` labels the env comment (`next` | `plan` | `new`); `issue`
|
||||||
* fills the `issue #N` part of the comment only when present.
|
* is optional and fills the `issue #N` part of the comment only when present.
|
||||||
*/
|
*/
|
||||||
const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpose = 'next' }) => {
|
const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpose = 'next' }) => {
|
||||||
const manual =
|
const manual =
|
||||||
|
|
@ -281,12 +304,33 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpo
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mainEnv = readMainEnv(mainRoot)
|
||||||
|
const payloadSecret = mainEnv.PAYLOAD_SECRET ?? randomBytes(24).toString('hex')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dockerComposeUp(dir)
|
dockerComposeUp(dir)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[worktree] Docker indisponível — usando o banco compartilhado (teqo/teqo_test).')
|
console.warn('[worktree] Docker indisponível — usando o banco compartilhado (teqo/teqo_test).')
|
||||||
console.warn(`[worktree] (${error.message.split('\n')[0]})`)
|
console.warn(`[worktree] (${error.message.split('\n')[0]})`)
|
||||||
writeFallbackEnv(dir, branch)
|
writeFallbackEnv(dir, branch)
|
||||||
|
// OPS28 — parity with CI even on the degraded shared databases: migrate +
|
||||||
|
// seed the shared teqo/teqo_test so a Cloud worktree's e2e runs against the
|
||||||
|
// same content CI asserts on. Both are idempotent and local-only, though
|
||||||
|
// two CONCURRENT fallback provisionings can race the migrate/upserts — the
|
||||||
|
// loser dies loudly and a re-run converges (degraded mode; not lock-guarded).
|
||||||
|
if (!skipMigrate) {
|
||||||
|
console.warn(
|
||||||
|
'[worktree] atenção: o seed re-pina campaignGoals e 5 municípios no teqo/teqo_test ' +
|
||||||
|
'compartilhados (valores ajustados manualmente no banco compartilhado serão sobrescritos).',
|
||||||
|
)
|
||||||
|
ensureWorktreeDeps(dir)
|
||||||
|
runMigrate(dir, 'postgresql://teqo:teqo@localhost:5432/teqo', payloadSecret)
|
||||||
|
runSeedMinimal(dir, 'postgresql://teqo:teqo@localhost:5432/teqo', payloadSecret)
|
||||||
|
runMigrate(dir, 'postgresql://teqo:teqo@localhost:5432/teqo_test', payloadSecret)
|
||||||
|
runSeedMinimal(dir, 'postgresql://teqo:teqo@localhost:5432/teqo_test', payloadSecret)
|
||||||
|
} else {
|
||||||
|
console.log('[worktree] --no-migrate: migrations e seed mínimo ficam para depois.')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,8 +341,6 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpo
|
||||||
const devUrl = `postgresql://teqo:teqo@localhost:5432/${devDatabase}`
|
const devUrl = `postgresql://teqo:teqo@localhost:5432/${devDatabase}`
|
||||||
const testUrl = `postgresql://teqo:teqo@localhost:5432/${testDatabase}`
|
const testUrl = `postgresql://teqo:teqo@localhost:5432/${testDatabase}`
|
||||||
|
|
||||||
const mainEnv = readMainEnv(mainRoot)
|
|
||||||
const payloadSecret = mainEnv.PAYLOAD_SECRET ?? randomBytes(24).toString('hex')
|
|
||||||
const copy = (key) => (mainEnv[key] ? [`${key}=${mainEnv[key]}`] : [])
|
const copy = (key) => (mainEnv[key] ? [`${key}=${mainEnv[key]}`] : [])
|
||||||
|
|
||||||
const issueLabel = issue ? ` · issue #${issue.number}` : ''
|
const issueLabel = issue ? ` · issue #${issue.number}` : ''
|
||||||
|
|
@ -327,12 +369,14 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpo
|
||||||
console.log('[worktree] .env.test.local escrito.')
|
console.log('[worktree] .env.test.local escrito.')
|
||||||
|
|
||||||
if (skipMigrate) {
|
if (skipMigrate) {
|
||||||
console.log('[worktree] --no-migrate: migrations ficam para depois (pnpm migrate).')
|
console.log('[worktree] --no-migrate: migrations e seed mínimo ficam para depois.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ensureWorktreeDeps(dir)
|
ensureWorktreeDeps(dir)
|
||||||
runMigrate(dir, devUrl, payloadSecret)
|
runMigrate(dir, devUrl, payloadSecret)
|
||||||
|
runSeedMinimal(dir, devUrl, payloadSecret)
|
||||||
runMigrate(dir, testUrl, payloadSecret)
|
runMigrate(dir, testUrl, payloadSecret)
|
||||||
|
runSeedMinimal(dir, testUrl, payloadSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdNext = async (stay, skipMigrate) => {
|
const cmdNext = async (stay, skipMigrate) => {
|
||||||
|
|
@ -604,7 +648,10 @@ if (!subcommand) {
|
||||||
)
|
)
|
||||||
console.log(' imprime também a diretiva `launch opencode …` (OPS26: abre o TUI com')
|
console.log(' imprime também a diretiva `launch opencode …` (OPS26: abre o TUI com')
|
||||||
console.log(' deepseek/deepseek-v4-flash + auto + /work-issue enviado); --stay suprime cd e')
|
console.log(' deepseek/deepseek-v4-flash + auto + /work-issue enviado); --stay suprime cd e')
|
||||||
console.log(' launch; --go explícito continua aceito como no-op; --no-migrate pula migrations')
|
console.log(
|
||||||
|
' launch; --go explícito continua aceito como no-op; --no-migrate pula migrations e o',
|
||||||
|
)
|
||||||
|
console.log(' seed mínimo (db:seed:minimal) nos bancos novos (OPS28: paridade com a CI)')
|
||||||
console.log(`\n plan [bag] [--stay] [--no-migrate]`)
|
console.log(`\n plan [bag] [--stay] [--no-migrate]`)
|
||||||
console.log(
|
console.log(
|
||||||
' cria um worktree de planejamento DIFERENTE a cada invocação (sessões /plan-issue',
|
' cria um worktree de planejamento DIFERENTE a cada invocação (sessões /plan-issue',
|
||||||
|
|
|
||||||
71
tests/e2e/seedParity.setup.e2e.spec.ts
Normal file
71
tests/e2e/seedParity.setup.e2e.spec.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
|
import { MINIMAL_CAMPAIGN_USERS } from '../../scripts/lib/seed-minimal-manifest.mjs'
|
||||||
|
import config from '../../src/payload.config.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OPS28 — parity guard between the worktree provisioning and CI.
|
||||||
|
*
|
||||||
|
* CI runs `pnpm migrate && pnpm db:seed:minimal` before e2e; worktrees
|
||||||
|
* provisioned by `pnpm worktree next`/`plan` now do the same (OPS28). This
|
||||||
|
* spec pins the outcome on the TEST DATABASE: if the seed is missing, the
|
||||||
|
* suite fails here with a clear message instead of red specs that "work in
|
||||||
|
* CI" (the exact pattern TECH-DEBT row 19 calls out as masking real failures).
|
||||||
|
*
|
||||||
|
* The check is the seed's INDELIBLE evidence, deliberately NOT the
|
||||||
|
* municipality priority pins: the e2e fixture cleanup resets `priority` on
|
||||||
|
* the municipalities it touches (round-robin over the catalog with a
|
||||||
|
* persistent sequence), so after repeated suite runs against the same
|
||||||
|
* worktree database a pin can legitimately be back to `normal` — that is
|
||||||
|
* not "seed missing". The seed's campaign users (synthetic, stable email /
|
||||||
|
* username, created only by `db:seed:minimal`, never deleted by the e2e
|
||||||
|
* cleanup, which owns only rows the fixtures created) cannot die that way:
|
||||||
|
* their presence is exactly "the minimal seed ran here".
|
||||||
|
*
|
||||||
|
* Runs in the `setup` Playwright project (matches `setup.e2e.spec.ts`), which
|
||||||
|
* the `campaign` project depends on: a failure here skips the dependent
|
||||||
|
* projects with this message instead of running a confusing suite.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SEED_MISSING_MESSAGE =
|
||||||
|
'O seed mínimo não foi aplicado neste banco de teste. Rode `pnpm migrate && pnpm db:seed:minimal` ' +
|
||||||
|
'(worktrees provisionados pelo `pnpm worktree next`/`plan` já fazem isso; um banco migrado cru ' +
|
||||||
|
'não tem nenhum dado do seed).'
|
||||||
|
|
||||||
|
test('test database carries the minimal seed (run pnpm migrate && pnpm db:seed:minimal)', async () => {
|
||||||
|
expect(MINIMAL_CAMPAIGN_USERS.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
let payload
|
||||||
|
try {
|
||||||
|
payload = await getPayload({ config })
|
||||||
|
} catch (error) {
|
||||||
|
// A never-migrated database fails on schema access, not on missing rows —
|
||||||
|
// surface the same actionable message instead of a raw PG error.
|
||||||
|
throw new Error(
|
||||||
|
`${SEED_MISSING_MESSAGE}\n(detalhe: o banco pode nem estar migrado — ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const user of MINIMAL_CAMPAIGN_USERS) {
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: 'campaignUser',
|
||||||
|
where: user.email
|
||||||
|
? { email: { equals: user.email } }
|
||||||
|
: { username: { equals: user.username } },
|
||||||
|
depth: 0,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
})
|
||||||
|
const found = docs[0]
|
||||||
|
expect(
|
||||||
|
found,
|
||||||
|
`Usuário do seed ${user.email ?? user.username} ausente no banco de teste — ${SEED_MISSING_MESSAGE}`,
|
||||||
|
).toBeDefined()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await payload.db.destroy?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -26,6 +26,7 @@ describe('classifyTestScope (OPS5)', () => {
|
||||||
'pnpm-lock.yaml',
|
'pnpm-lock.yaml',
|
||||||
'src/collections/Post.ts',
|
'src/collections/Post.ts',
|
||||||
'scripts/lib/seed-minimal-manifest.mjs',
|
'scripts/lib/seed-minimal-manifest.mjs',
|
||||||
|
'scripts/worktree.mjs',
|
||||||
'scripts/lib/test-affected-core.mjs',
|
'scripts/lib/test-affected-core.mjs',
|
||||||
'tests/helpers/campaignFixtures.ts',
|
'tests/helpers/campaignFixtures.ts',
|
||||||
'tests/unit/ciSkipInvariants.unit.spec.ts',
|
'tests/unit/ciSkipInvariants.unit.spec.ts',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue