diff --git a/.agents/rules/agent-pr-workflow.mdc b/.agents/rules/agent-pr-workflow.mdc index a75bfe4c..50066f4e 100644 --- a/.agents/rules/agent-pr-workflow.mdc +++ b/.agents/rules/agent-pr-workflow.mdc @@ -78,11 +78,13 @@ Use `--required` — without it, `--watch` also waits for Vercel. There is no stage→main promote. Merging to `main` is the delivery; `ci.yml` verifies the full suite and production deploys from the homeserver runbook. -## Safety net (GitHub Action) +## Safety net (workflow) -`agent-pr-ready-automerge.yml` — PRs same-repo → `main` (todas as branches): -se draft `cursor/*` → `gh pr ready`; sempre arma `gh pr merge --auto --rebase`. -Cobre o caso em que o agente (ou o default draft do Cloud) esqueceu e PRs -humanas/worktree com merge falho silencioso (incidente OPS52). **Draft de -branch não-`cursor/*` nunca é tocado** — o draft é o veto do ator (skip do CLI). -Não há opt-out por skill (audit incluído). +`agent-pr-ready-automerge.yml` — PRs same-repo → `main` (todas as branches). +O workflow roda `scripts/forgejo-pr-automerge.mjs` (plain Node, zero-dep): se +draft `cursor/*` → marca Ready; espera os checks e mergea por rebase (o +equivalente Forgejo do `gh pr merge --auto --rebase`). Cobre o caso em que o +agente (ou o default draft do Cloud) esqueceu e PRs humanas/worktree com merge +falho silencioso (incidente OPS52). **Draft de branch não-`cursor/*` nunca é +tocado** — o draft é o veto do ator (skip do CLI). Não há opt-out por skill +(audit incluído). diff --git a/.forgejo/workflows/agent-pr-ready-automerge.yml b/.forgejo/workflows/agent-pr-ready-automerge.yml index a72e7409..6fffc95e 100644 --- a/.forgejo/workflows/agent-pr-ready-automerge.yml +++ b/.forgejo/workflows/agent-pr-ready-automerge.yml @@ -20,10 +20,13 @@ on: jobs: ready-automerge: - # Same-repo only: a fork PR merged here would push attacker code to main - # (and deploy to production). The draft policy lives in the CLI (skips - # non-`cursor/*` drafts) so it reads the PR state from the API, not from - # webhook payload fields. + # Same-repo only. This MUST stay in the `if:` (pre-checkout): for a fork + # PR, actions/checkout checks out the fork's head, so a same-repo guard + # inside the CLI would already be running the fork's own script — RCE with + # the Actions token. Fork PRs are excluded here because the repo is public + # and a merged fork PR would deploy attacker code to production. The draft + # policy lives in the CLI (skips non-`cursor/*` drafts) because there the + # PR state is re-read from the API, not from webhook payload fields. if: >- github.event.pull_request.state == 'open' && github.event.pull_request.head.repo.full_name == github.repository diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index a906d577..7fd2e1ca 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -75,7 +75,7 @@ Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migra | `ci.yml` | push / dispatch `main` | service Postgres | Full sempre; fail-fast cancela o run no 1º job vermelho; rollup `checks`; job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) | | `issue-done-on-main-merge.yml` | PR merged → `main` | — | `Closes`/`Fixes` → `done` + `in-prod` | | `plan-issue-ready-on-main-merge.yml` | PR merged → `main` | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip) | -| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net: Ready + `gh pr merge --auto --rebase` (todas as branches; draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip) | +| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera checks e mergea por rebase; draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip | | `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) | Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `styfle/cancel-workflow-action@0.13.1` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`. diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index ef3c8078..786acf06 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -22,6 +22,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **Recently resolved (2026-08-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: nenhum novo (lógica de veredito já coberta pelos specs da OPS56; branch de draft é 3 linhas no CLI); gates unit/int verdes. +**Recently resolved (2026-08-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: spec nova em `forgejoApi.unit.spec.ts` (waitForChecks para quando o PR vira draft no meio do wait — veto garantido mesmo em conversão mid-run; autoMerge idempotente); gates unit/int verdes. **Validação ao vivo na própria entrega:** o Forgejo roda o workflow do head da PR (não o de main), então a PR #48 foi mergeada pelo safety net alargado no primeiro uso real — o `if:` com `head.repo.full_name` avaliou true em PR real e o script mergeou por rebase. Fixes pós-review do /simplify: `waitForChecks`/`autoMerge` param em PR draft (antes: 405 ruidoso na conversão mid-run, contradizendo o "veto nunca tocado"); CLI valida `base.ref === 'main'` (invocação manual); comentário do fork guard documenta por que ele não pode descer pro CLI (checkout de fork roda o script do fork = RCE com o token do Actions); docs sem o mecanismo fantasma `gh pr` (era GitHub, OPS50 trocou por CLI plain-Node). + **Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte. **Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- "`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte. diff --git a/docs/changelog/2026-08-18-ops57.md b/docs/changelog/2026-08-18-ops57.md index 64c82e6f..3a88223d 100644 --- a/docs/changelog/2026-08-18-ops57.md +++ b/docs/changelog/2026-08-18-ops57.md @@ -1 +1 @@ -**Recently resolved (2026-08-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: nenhum novo (lógica de veredito já coberta pelos specs da OPS56; branch de draft é 3 linhas no CLI); gates unit/int verdes. +**Recently resolved (2026-08-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: spec nova em `forgejoApi.unit.spec.ts` (waitForChecks para quando o PR vira draft no meio do wait — veto garantido mesmo em conversão mid-run; autoMerge idempotente); gates unit/int verdes. **Validação ao vivo na própria entrega:** o Forgejo roda o workflow do head da PR (não o de main), então a PR #48 foi mergeada pelo safety net alargado no primeiro uso real — o `if:` com `head.repo.full_name` avaliou true em PR real e o script mergeou por rebase. Fixes pós-review do /simplify: `waitForChecks`/`autoMerge` param em PR draft (antes: 405 ruidoso na conversão mid-run, contradizendo o "veto nunca tocado"); CLI valida `base.ref === 'main'` (invocação manual); comentário do fork guard documenta por que ele não pode descer pro CLI (checkout de fork roda o script do fork = RCE com o token do Actions); docs sem o mecanismo fantasma `gh pr` (era GitHub, OPS50 trocou por CLI plain-Node). diff --git a/scripts/forgejo-pr-automerge.mjs b/scripts/forgejo-pr-automerge.mjs index e46903d8..8997886b 100644 --- a/scripts/forgejo-pr-automerge.mjs +++ b/scripts/forgejo-pr-automerge.mjs @@ -46,20 +46,27 @@ try { ) process.exit(0) } + if (pr.base.ref !== 'main') { + console.log(`[forgejo-pr-automerge] PR #${prNumber} base é ${pr.base.ref} (não main) — skip`) + process.exit(0) + } if (pr.isDraft) { - if (pr.head.ref.startsWith('cursor/')) { - console.log(`[forgejo-pr-automerge] PR #${prNumber} é draft — marcando Ready`) - await api.markPullRequestReady(prNumber) - } else { + if (!pr.head.ref.startsWith('cursor/')) { console.log( - `[forgejo-pr-automerge] PR #${prNumber} é draft fora de cursor/* — skip (não força PR humano)`, + `[forgejo-pr-automerge] PR #${prNumber} é draft fora de cursor/* — skip (veto do ator)`, ) process.exit(0) } + console.log(`[forgejo-pr-automerge] PR #${prNumber} é draft — marcando Ready`) + await api.markPullRequestReady(prNumber) } else { console.log(`[forgejo-pr-automerge] PR #${prNumber} já Ready`) } - const { attempted, merged } = await api.autoMerge(prNumber, { + const { + attempted, + merged, + pr: after, + } = await api.autoMerge(prNumber, { log: (line) => console.log(`[forgejo-pr-automerge] ${line}`), }) if (merged) { @@ -69,7 +76,11 @@ try { : `[forgejo-pr-automerge] PR #${prNumber} já mergeado`, ) } else { - console.log(`[forgejo-pr-automerge] PR #${prNumber} fechado sem merge — skip`) + console.log( + after?.state === 'CLOSED' + ? `[forgejo-pr-automerge] PR #${prNumber} fechado sem merge — skip` + : `[forgejo-pr-automerge] PR #${prNumber} voltou a draft — skip (veto do ator)`, + ) } } catch (error) { console.error(`[forgejo-pr-automerge] falhou: ${error instanceof Error ? error.message : error}`) diff --git a/scripts/lib/forgejo-api.mjs b/scripts/lib/forgejo-api.mjs index 6d6b33b9..0ae7815b 100644 --- a/scripts/lib/forgejo-api.mjs +++ b/scripts/lib/forgejo-api.mjs @@ -272,12 +272,14 @@ export const createApi = ({ base, token, repository, fetchImpl } = {}) => { /** * All required statuses on the PR head are success (or a failure exists), * and Forgejo finished computing mergeability. Returns the last PR read. + * A draft PR stops the wait (converted mid-run): the caller decides what + * "no merge" means — the CLI skips non-`cursor/*` drafts (OPS57 veto). */ waitForChecks: async (number, { timeoutMs = 30 * 60 * 1000, pollMs = 15000, log } = {}) => { const started = Date.now() for (;;) { const pr = await api.getPullRequest(number) - if (!pr || pr.state !== 'OPEN' || pr.merged) return pr + if (!pr || pr.state !== 'OPEN' || pr.merged || pr.isDraft) return pr if (pr.mergeable === false) { throw new Error(`PR #${number} não mergeável (conflito?) — resolver manualmente.`) } @@ -314,7 +316,7 @@ export const createApi = ({ base, token, repository, fetchImpl } = {}) => { */ autoMerge: async (number, options) => { const pr = await api.waitForChecks(number, options) - if (!pr || pr.merged || pr.state !== 'OPEN') { + if (!pr || pr.merged || pr.state !== 'OPEN' || pr.isDraft) { return { attempted: false, merged: Boolean(pr?.merged), pr } } try { diff --git a/tests/unit/forgejoApi.unit.spec.ts b/tests/unit/forgejoApi.unit.spec.ts index 23dfacac..55e51f9e 100644 --- a/tests/unit/forgejoApi.unit.spec.ts +++ b/tests/unit/forgejoApi.unit.spec.ts @@ -221,6 +221,37 @@ describe('forgejo-api', () => { await expect(api.waitForChecks(4, { pollMs: 1 })).rejects.toThrow(/não mergeável/) }) + it('waitForChecks stops when the PR is converted to draft mid-wait (OPS57 veto)', async () => { + let poll = 0 + const api = createApi({ + token: 'tok', + fetchImpl: async (url) => { + poll += 1 + if (String(url).includes('/pulls/')) { + return ok({ + number: 4, + state: 'open', + merged: false, + mergeable: true, + is_draft: poll >= 3, + head: { ref: 'OPS57-x', sha: 'abc' }, + }) + } + return ok({ + statuses: + poll <= 2 + ? [{ context: 'CI / static', status: 'pending' }] + : [{ context: 'CI / static', status: 'success' }], + }) + }, + }) + + const pr = await api.waitForChecks(4, { pollMs: 1 }) + + expect(poll).toBe(3) + expect(pr.isDraft).toBe(true) + }) + it('autoMerge verifies the merge by re-reading the PR (POST answers 200 + empty body)', async () => { const calls: string[] = [] let poll = 0