teqo/tests/unit/branchProtection.unit.spec.ts
Francisco Solla ac11afecf3 ops(OPS61): flip de issues pós-merge com PAT + gate do rollup checks + branch protection real no Forgejo
- issue-done/plan-issue-ready: env FORGEJO_API_TOKEN (GITHUB_TOKEN 403 em
  Issues) + fork guard (checkout de fork roda script do fork com o PAT — RCE)
- forgejo-issue-transition: falha de flip vira exit 1 (job vermelho, nunca
  skip silencioso)
- waitForChecks: exige o rollup CI (PR) / checks presente+verde (raça PR #52:
  snapshot com jobs não-agendados parecia verde); mergeable=false só conta
  como conflito após a cascata assentar; timeouts 30→45min (CLI) e 40→55min
  (workflow)
- configure-branch-protection: contexto real CI (PR) / checks* (Forgejo casa
  por glob contra 'CI (PR) / checks (pull_request)'; literal 'checks' nunca
  casaria), idempotente GET→POST/PATCH, verificação pós-aplicação; decisão
  pura em scripts/lib/branch-protection.mjs
- docs honestas (AGENT-OPS, agent-pr-workflow.mdc, CUTOVER-MAIN-ONLY, README)
- testes: raça do rollup/rollup failure/mergeable pré-settle +
  branchProtection create/update/noop + glob real
2026-08-18 17:45:54 -03:00

53 lines
2.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
DESIRED_RULE,
planBranchProtectionRule,
ruleMatches,
} from '../../scripts/lib/branch-protection.mjs'
describe('branch-protection rule planning (OPS61)', () => {
it('plans create when no rule exists', () => {
expect(planBranchProtectionRule(null).action).toBe('create')
})
it('plans noop when the rule already matches', () => {
const compliant = { ...DESIRED_RULE, branch_name: 'main' }
expect(planBranchProtectionRule(compliant).action).toBe('noop')
expect(ruleMatches(compliant)).toBe(true)
})
it('plans update when the rule drifts', () => {
const drift = { ...DESIRED_RULE, status_check_contexts: ['checks'] }
expect(planBranchProtectionRule(drift).action).toBe('update')
expect(planBranchProtectionRule(drift).action).not.toBe('noop')
})
it('detects drift in the contexts list (extra context)', () => {
const extra = {
...DESIRED_RULE,
status_check_contexts: ['CI (PR) / checks*', 'migration-lock'],
}
expect(planBranchProtectionRule(extra).action).toBe('update')
})
it('compares status_check_contexts as a set (order does not matter)', () => {
const desired = { ...DESIRED_RULE, status_check_contexts: ['a', 'b'] }
const existing = { ...desired, status_check_contexts: ['b', 'a'] }
expect(ruleMatches(existing, desired)).toBe(true)
expect(ruleMatches({ ...desired, status_check_contexts: ['a'] }, desired)).toBe(false)
})
it('the desired glob matches the real observed rollup context, not the push rollup', () => {
// Forgejo matches rule contexts as gobwas globs against the full context
// string — including the ` (event)` suffix — so the observed rollup is
// `CI (PR) / checks (pull_request)`. A literal `checks` would never match.
const glob = DESIRED_RULE.status_check_contexts[0]
const toRegex = (pattern: string) =>
new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace('*', '.*') + '$')
const match = toRegex(glob)
expect(match.test('CI (PR) / checks (pull_request)')).toBe(true)
expect(match.test('CI / checks (push)')).toBe(false)
expect(match.test('CI (PR) / checks')).toBe(true)
})
})