Skip to content

Add pytest smoke tests for gateway, yt, and langextract services - #20

Merged
POWERFULMOVES merged 1 commit into
mainfrom
codex/add-pytest-smoke-tests-and-ci-integration
Sep 18, 2025
Merged

Add pytest smoke tests for gateway, yt, and langextract services#20
POWERFULMOVES merged 1 commit into
mainfrom
codex/add-pytest-smoke-tests-and-ci-integration

Conversation

@POWERFULMOVES

Copy link
Copy Markdown
Owner

Summary

  • add a pytest-based smoke test suite under pmoves/tests that exercises /hirag/query, /yt/download, and /extract/text with local fakes
  • provide lightweight stubs for optional services so tests run offline
  • document the pytest command and update CI to install deps and run the suite

Testing

  • pytest pmoves/tests -q

https://chatgpt.com/codex/tasks/task_b_68cb57ec431c8324a70acdfd91fc4187

@coderabbitai

coderabbitai Bot commented Sep 18, 2025

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@POWERFULMOVES has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 41 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 54a2a14 and c84ed9a.

📒 Files selected for processing (7)
  • pmoves/.github/workflows/ci.yml (1 hunks)
  • pmoves/README.md (1 hunks)
  • pmoves/tests/__init__.py (1 hunks)
  • pmoves/tests/conftest.py (1 hunks)
  • pmoves/tests/test_hirag_gateway.py (1 hunks)
  • pmoves/tests/test_langextract.py (1 hunks)
  • pmoves/tests/test_pmoves_yt.py (1 hunks)

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@POWERFULMOVES
POWERFULMOVES merged commit b30320a into main Sep 18, 2025
1 of 3 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines 1 to +23
name: pmoves-ci
on: {push: {branches: [main, dev]}, pull_request: {}}
jobs: {noop: {runs-on: ubuntu-latest, steps: [{uses: actions/checkout@v4}]}}

on:
push:
branches:
- main
- dev
pull_request:

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install fastapi==0.114.2 httpx==0.27.2 pytest==8.3.3 requests==2.32.3 lxml==4.9.4
- name: Run smoke tests
run: pytest pmoves/tests -q

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Relocate CI workflow to repository-level .github

The new smoke-test workflow lives under pmoves/.github/workflows/ci.yml, but GitHub Actions only loads workflows from the top-level .github/workflows directory. As a result this job never triggers on pushes or pull requests and the newly added tests will not run in CI, defeating the purpose of the change. Move the workflow into the repository root .github/workflows folder or reference it from there so Actions can discover it.

Useful? React with 👍 / 👎.

POWERFULMOVES added a commit that referenced this pull request Jan 18, 2026
…s-and-ci-integration

Add pytest smoke tests for gateway, yt, and langextract services
@POWERFULMOVES
POWERFULMOVES deleted the codex/add-pytest-smoke-tests-and-ci-integration branch March 7, 2026 21:41
POWERFULMOVES added a commit that referenced this pull request Jun 10, 2026
…ync, 7 commits) (#1774)

Advances c0925569 → 2091863d after fork PR #20 (7 behind, high-ahead tier 35).
saves/_list.json conflict resolved as union (PMOVES domain shapes + upstream
Sea Urchin/Alien Artifact). 35 PMOVES-ahead preserved.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Jun 11, 2026
…#1784)

Advances 92fb9551 → 849326748b after fork PR #20, which makes the E2E Smoke Tests
green by skipping the OpenAI-dependent tiers (e2e-codex, e2e-mixed) when
OPENAI_API_KEY is absent. Root cause was a missing-secret 401 (not an outdated
test); PMOVES avoids managed OpenAI, so the Codex tier now skips gracefully while
deterministic + Claude tiers run. Upstream is red for the same reason.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Jun 11, 2026
…k-sync campaign

Control-Body bookkeeping for the 2026-06-09→11 Z890 main-infra campaign (the docs
lagged it by a week). For 4090-CLAUDE pair-review.

- Claim register: new RELEASE section logging the campaign — supabase CRITICAL sync
  (#1761/#1768), branch-protection automation (#1760/#1767, 31 forks), full
  fork-sync clearance (auto-tier + high-ahead + ClawZ 8354c/Creator), Archon CI
  green (#19 lint / #20 E2E), space-agent public+protected.
- README Known Gaps: retire stale entries — OBSERVABILITY_MAP.md now on main (P2
  resolved), A2A exposure wired into compose (P0 → runtime-verify only), NATS-auth
  closed NON-URGENT (#1690, 23 refs left). Added fleet-governance state note.
- SITREP: +2 June wave rows (Main-Infra Pass, Fleet Fork-Sync + Governance),
  refreshed date 2026-05-27 → 2026-06-11, title Apr–May → Apr–Jun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Jun 12, 2026
…k-sync campaign (#1786)

Control-Body bookkeeping for the 2026-06-09→11 Z890 main-infra campaign (the docs
lagged it by a week). For 4090-CLAUDE pair-review.

- Claim register: new RELEASE section logging the campaign — supabase CRITICAL sync
  (#1761/#1768), branch-protection automation (#1760/#1767, 31 forks), full
  fork-sync clearance (auto-tier + high-ahead + ClawZ 8354c/Creator), Archon CI
  green (#19 lint / #20 E2E), space-agent public+protected.
- README Known Gaps: retire stale entries — OBSERVABILITY_MAP.md now on main (P2
  resolved), A2A exposure wired into compose (P0 → runtime-verify only), NATS-auth
  closed NON-URGENT (#1690, 23 refs left). Added fleet-governance state note.
- SITREP: +2 June wave rows (Main-Infra Pass, Fleet Fork-Sync + Governance),
  refreshed date 2026-05-27 → 2026-06-11, title Apr–May → Apr–Jun.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 10, 2026
The 2026-08-10 ratification said the migration script goes away: classic
branch protection is not deprecated, rulesets layer with it, and "the most
restrictive version of the rule applies" — so there is nothing to migrate
away from and no reason to DELETE classic protection before a replacement
exists.

The refactor commit b0fbf68 rewrote branch_protection.py to ruleset-only
but left the script and its 15 tests on disk, while the PR comment reported
them as deleted. Verified against the tree: both files were still tracked at
78157b7. This makes the reported state the real state.

That closes the four findings that only existed because of the script:
  N1  DELETE fires before any replacement (with a test asserting it should)
  N2  signed commits + linear history silently dropped by the migration
  N3  captured_required_status_checks captured, printed, never used
  #20 migration test docstrings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POWERFULMOVES added a commit that referenced this pull request Aug 15, 2026
* feat(tools): pmoves standard branch protection spec + tool

The PMOVES standard branch-protection tool. Single source of truth for
how a PMOVES org repo's main branch is protected. The tool reads a
canonical JSON spec (pmoves_standard.json) and either audits a repo
against the spec, applies the spec to a repo, or drift-checks the
whole org.

What this slice lands:

- pmoves/configs/branch_protection/pmoves_standard.json - the
  canonical spec. 2 profiles (monorepo + fork) + per_repo_overrides
  for the 3 PMOVES repos in the org. The shape mirrors the GitHub
  REST API 1:1, so a profile maps to actual API calls without
  intermediate transformation.

- pmoves/tools/branch_protection.py - the tool. Pure-stdlib Python
  (urllib.request + json), no new deps. Three public functions:
    audit(repo, profile)        - diff actual state vs spec
    apply(repo, profile, dry_run=True) - apply the spec; dry-run by default
    drift_check(org)            - audit every repo in the org's overrides
  8 dataclasses for structured results (AuditResult, DriftItem,
  ApplyResult, DriftReport, etc.) so the orchestrator can consume
  the output. CLI surface: `python -m pmoves.tools.branch_protection
  {audit,apply,drift-check}` with structured JSON output.

Why this is the next slice after the harness v0:

The 3-PR review pass (PMOVES.AI #2477 + PMOVES-hermes-agent #4 +
PMOVES-pinokio #1) shipped the CGP bootstrap contract. The contract
ties 3 repos together, but the SECURITY POSTURE is wildly
asymmetric: PMOVES.AI is heavily protected (4 required status
checks, reviews with code owner enforcement, linear history,
signatures, 3 rulesets), but the 2 forks have NO protection at
all. The Hermes PR #4 was admin-merged only because there's no
required gate to be met - that's a bug-as-feature, not a
designed protection.

This tool makes the asymmetry visible (drift-check) and
correctable (apply). The Mavis cron can call drift-check daily
and publish on pmoves.branch_protection.drift.v1 (the NATS
subject lands in a follow-up slice).

Design notes (codified in the docstring + tests):

- The tool shells out to `gh api` instead of using urllib directly
  for HTTP. Reason: the PMOVES GitHub App token + the operator's
  PAT both flow through gh's auth, and wrapping gh gives the
  operator free auth-state inspection via `gh auth status`.
  Tradeoff: the tool requires `gh` installed and authenticated.
  Documented in BRANCH_PROTECTION_BASELINE.md (follow-up docs).

- dry-run is the default. The tool never issues a PUT/POST without
  `--no-dry-run`. The dry-run output is a JSON list of would-be
  API calls; the operator reviews the list before issuing live
  apply. The 2 forks in the spec will be applied manually after
  this PR merges (slice 2, separate PRs per repo).

- The per_repo_overrides section is the per-repo customization
  point. New forks add themselves here; the spec's strict shape
  (additionalProperties: false on the profile keys) catches
  typos before the tool hits the network.

- The diff function separates block (must-fix) from warn
  (advisory) severity. required_status_checks + required_review
  count are block; dismiss_stale_reviews + require_code_owner
  are warn. A compliant repo has zero block-level drift;
  warn-level drift is logged but doesn't fail the audit.

Three-body: delivery=Mavis (this PR), control=DARKXSIDE (operator
reviews the spec + the drift report, then runs apply manually for
each repo), memory=this trail + the spec + the LEARNINGS file.
CHIT trail unsigned-local (no CHIT_PASSPHRASE loaded in this Mavis
session).

* test(tools): 44 tests for branch_protection across 8 groups

Eight test groups cover the spec loader, the diff logic, the
apply body builder, audit + apply + drift-check end-to-end (with
mocked `gh api`), the CLI surface, and 2 error paths (gh missing
+ gh 404 for unprotected branch).

Test groups (each is a unittest.TestCase class):

- A. SpecLoaderTests (5) - load + resolve_repo_profile default +
  with override + unknown repo + unknown profile
- B. DiffLogicTests (14) - 4 status-check scenarios, 3 review-policy
  scenarios, 4 boolean-field scenarios (including the nested
  {"enabled": bool} shape the real API returns), 3 rulesets
  scenarios
- C. ApplyBodyTests (4) - classic body has all 9 required keys,
  preserves values, ruleset body has 6 required keys, preserves
  rules
- D. AuditTests (6) - compliant repo, no-protection-is-block,
  missing-check-is-block, extra-check-is-warn, explicit profile,
  unknown-repo-raises
- E. ApplyTests (5) - dry-run-no-calls, live-calls, skips-existing-
  ruleset, includes-per-repo-override-checks, unknown-repo-raises
- F. DriftCheckTests (3) - one-report-per-repo, only-org-repos,
  surfaces-audit-error-as-synthetic-drift
- G. CLITests (4) - audit-exits-0-when-compliant, audit-exits-1-
  when-drift, drift-check-exits-2-when-any-drift, apply-default-
  is-dry-run (the dry-run does ONE read for existing rulesets
  so the output can accurately report skip-vs-create; no PUT or
  POST is issued; the test asserts both)
- H. ErrorPathTests (2) - gh-missing-raises, gh-404-for-unprotected-
  returns-none (the real GitHub API returns 404 + "Branch not
  protected" stderr for an unprotected branch; the tool
  recognizes this and returns None, not an error)

All 44 tests pass. The mocked subprocess calls use a lambda
side_effect keyed on (method, path) so the test surface is
explicit and the failure mode is "unmocked gh call" rather than
a silent no-op.

Test design notes (codified in the test docstring):

- The mocks use the SAME shape as the real GitHub API
  response: required_status_checks.checks is a list of
  {"context": "name"} objects; required_linear_history etc are
  nested as {"enabled": bool}; rulesets is a list of dicts with
  name + id + rules + bypass_actors. The diff logic was updated
  to handle both the spec's bare-boolean shape and the API's
  {"enabled": bool} shape, so a real audit + a unit test give
  the same answer.

- The drift-check test (F3) ensures that an audit error (e.g.,
  gh subprocess failure) doesn't crash the whole drift report -
  the erroring repo appears with a synthetic DriftItem so the
  operator can see which repos failed and why. This is the
  pattern the Mavis cron relies on.

Three-body: delivery=Mavis (this), control=DARKXSIDE (operator
can run the tests locally with `python -m unittest
pmoves.tools.tests.test_branch_protection` before applying the
spec to the 2 forks), memory=this trail. CHIT trail unsigned-local.

* docs(agnote): branch protection v0 CLAIM row + 2-fork apply record

Records the Slice 2 fan-out: the PMOVES standard branch protection
tool landed (PR #2490) + both unprotected forks now have
the fork profile applied.

Real-run evidence (this commit's author):
- python -m pmoves.tools.branch_protection apply --repo
  POWERFULMOVES/PMOVES-pinokio --no-dry-run → created classic
  protection (CodeRabbit required, 1 reviewer, linear history,
  conversation resolution) + [main] ruleset (id=20589542)
- python -m pmoves.tools.branch_protection apply --repo
  POWERFULMOVES/PMOVES-hermes-agent --no-dry-run → created
  classic protection (9 required status checks, 1 reviewer,
  linear history, conversation resolution) + [main] ruleset
  (id=20589548)
- python -m pmoves.tools.branch_protection drift-check --org
  POWERFULMOVES (post-apply) → both repos compliant, zero drift

What's NOT in this slice (intentional follow-up):
- PMOVES.AI migration to rulesets-only (Option A approved by
  operator; needs a separate migration script because the
  current tool's `apply` doesn't support "delete classic +
  consolidate rulesets"). The bypass_actor list from the
  [main] ruleset (RepositoryRole id=5, Integration id=1144995,
  Integration id=1236702) must be re-registered in the new
  ruleset.
- NATS subject pmoves.branch_protection.drift.v1 (Slice 3)
- Mavis cron that calls drift-check daily (Slice 3)
- BRANCH_PROTECTION_BASELINE.md + pair-review skill update
  (Slice 4)

Three-body: delivery=Mavis, control=DARKXSIDE, memory=this
trail + PR #2490. CHIT trail unsigned-local.

* feat(tools): PMOVES.AI branch-protection migration (Option A)

The one-off migration script that consolidates PMOVES.AI's
classic + 3-ruleset layered state into a single ruleset
([ main ]) with the status check + review requirements +
copilot_code_review + the 3 bypass_actors preserved.

What this slice lands:

- pmoves/tools/branch_protection_migrate_pmai.py - the
  migration script. Pure-stdlib (no new deps), uses the existing
  branch_protection.py helpers (the same `gh api` wrapper, the
  same spec loader, the same dataclass patterns). 2 public
  functions:
    plan()         - reads the current state, computes the new
                      [ main ] ruleset body, returns a MigrationPlan
    apply(plan, dry_run=True) - issues DELETE classic + PUT
                      [ main ] ruleset; dry-run is the default

- pmoves/tools/tests/test_branch_protection_migrate_pmai.py -
  15 tests across 4 groups (compute_main_ruleset,
  capture_state, plan, apply). All 59 tests pass across
  both the tool + the migration.

- pmoves/configs/branch_protection/pmoves_standard.json -
  added `submodule-gitlink-gate` to the monorepo profile's
  required status checks. The actual state has 5 required
  checks; the original spec had 4. This aligns the spec
  with reality.

The migration is destructive (DELETE classic + PUT ruleset
in a different shape), so dry-run is the default. The
operator reviews the call sequence + the captured state
before --no-dry-run is issued.

Design notes (codified in the LEARNINGS file):

- The list endpoint /rulesets returns a SUMMARY without
  bypass_actors. The migration re-fetches the per-ruleset
  body to get the full bypass_actors list. Without this
  re-fetch, the migration would silently drop the operator's
  preauthorized --admin bypass. Captured in LEARNINGS lesson 1.

- The pull_request rule in a ruleset uses different field
  names than the classic required_pull_request_reviews
  block. The migration explicitly maps the spec's
  required_pull_request_reviews keys to the ruleset
  pull_request parameters. Captured in LEARNINGS lesson 2.

- The spec's monorepo profile hard-codes RepositoryRole id=5
  as the default bypass_actor. The migration OVERRIDES this
  with the captured bypass_actors from the existing ruleset
  (3 actors: RepositoryRole id=5, Integration id=1144995,
  Integration id=1236702). The spec is the source of truth
  for new repos; the migration preserves the operator's
  actual escape hatch for this repo. Captured in LEARNINGS
  lessons 5 + 6.

- The migration is a one-off. After it runs, the canonical
  branch_protection.py apply tool keeps the [ main ]
  ruleset in sync with the spec. The migration script is
  archived in the tool's directory; the spec + the tool are
  the source of truth going forward.

Migration call sequence (dry-run, current state):

1. DELETE /repos/POWERFULMOVES/PMOVES.AI/branches/main/protection
2. PUT /repos/POWERFULMOVES/PMOVES.AI/rulesets/10887588 with:
   - name: [ main ]
   - rules: deletion, non_fast_forward, pull_request (1 reviewer
     + code owner + dismiss stale + review thread resolution),
     copilot_code_review, required_status_checks (5 checks),
   - bypass_actors: 3 (preserved)

Three-body: delivery=Mavis, control=DARKXSIDE (operator
reviews the dry-run output before --no-dry-run), memory=this
trail + the LEARNINGS file + the BRANCH_PROTECTION_BASELINE.md
doc. CHIT trail unsigned-local.

* docs(operations+learnings): branch protection baseline + 5-class LEARNINGS

Two companion docs for the branch protection fan-out.

- pmoves/docs/operations/BRANCH_PROTECTION_BASELINE.md - the
  human-readable version of pmoves_standard.json. Covers:
    - Why a baseline (the 3-PR review pass surfaced the
      asymmetric protection state; this doc is the fix)
    - The 2 profiles (monorepo + fork) with field-level
      rationale + the GitHub doc citation for each
    - Current state per repo (PMOVES.AI: not yet migrated;
      PMOVES-hermes-agent: applied; PMOVES-pinokio: applied)
    - How to apply / audit / drift-check (with the exact
      `python -m pmoves.tools.branch_protection` invocations)
    - How to add a new repo or a new profile
    - The PMOVES.AI migration plan (Option A, the next apply)
    - Wire-up to the harness (load_bootstrap CGP, Mavis cron,
      orchestrator dispatch)
    - 5 references to the official GitHub docs (rulesets,
      protected branches, troubleshooting, MergeStateStatus
      enum, the LEARNINGS file)

- pmoves/tools/LEARNINGS/branch-protection-v0_LEARNINGS.md -
  the 5-class taxonomy + 4-bucket learning signal per the
  pr-trim convention. Populated with 13 already-fixed, 5
  out-of-scope, 0 pre-existing observations. The "Pattern
  update" section adds 6 new lessons to the pmoves-pair-review
  skill's step 7:
    1. The list endpoint /rulesets returns a SUMMARY without
       bypass_actors. Re-fetch the per-ruleset body when
       bypass_actors is needed.
    2. The pull_request rule in a ruleset uses different field
       names than the classic required_pull_request_reviews
       block. Map explicitly.
    3. additionalProperties: false is the right default for
       required objects, but bypass_actors and status_checks
       should stay open (extending pair-review lesson 3 to
       nested arrays).
    4. UNSTABLE = mergeable + bypass_actors re-fetch = mandatory.
       Both are silent-corruption traps.
    5. The spec is the source of truth for fresh repos, but
       the migration captures the existing bypass_actors to
       preserve the operator's escape hatch.
    6. Migrate the operator's preauthorized bypass list
       explicitly; don't rely on the spec's defaults.

Three-body: delivery=Mavis, control=DARKXSIDE, memory=this
trail + the spec + the migration script. CHIT trail
unsigned-local.

* refactor(tools): collapse branch_protection to ruleset-only per operator ratification

Per the operator's 2026-08-10 ratification (PR #2490 review id 4893614185) grounded
in GitHub's "About rulesets" docs: classic branch protection is NOT deprecated,
rulesets LAYER with it ("the most restrictive version of the rule applies"),
and "start using rulesets without overriding any of your existing protection rules"
is the intended adoption path. This collapses the original migration script
(N1/N2/N3 delete) + the classic-PUT body builder (P1-A/P1-C/N8 delete) into
the new ownership split:

  - .github/workflows/branch-protection-sync.yml owns CLASSIC protection
  - pmoves/tools/branch_protection.py owns RULESETS only
  - The two writers layer additively (most-restrictive-wins)
  - Additive adoption is monotonic - the tool can only make a branch stricter

What changes:
  - pmoves/tools/branch_protection.py: dropped _build_classic_body +
    _diff_required_status_checks + _diff_review_policy + _diff_boolean_field;
    added SpecValidator (validates at load, per P1-B); added resolve_branch()
    (per_repo_overrides -> .gitmodules -> spec default -> "main", per N4);
    deep-diff _ruleset_matches() (rules + conditions + bypass_actors, per N6);
    apply() now creates missing AND updates existing rulesets; _gh_api has
    GH_TIMEOUT_SECONDS=30
  - pmoves/configs/branch_protection/pmoves_standard.json: upgraded to v2
    (pmoves.rulesets/v2); profiles only have rulesets: []; monorepo profile
    carries 8 ruleset rules; fork profile has required_approving_review_count=0
    (matches workflow default, per N5); per_repo_overrides includes PMOVES.AI +
    PMOVES-hermes-agent + PMOVES-pinokio + PMOVES-nats-server (the new fork)
  - pmoves/docs/operations/BRANCH_PROTECTION_BASELINE.md: rewritten with the
    ownership split documented at the top
  - pmoves/tools/LEARNINGS/branch-protection-v0_LEARNINGS.md: 5-class taxonomy
    updated (15 already-fixed / 6 owner / 5 out-of-scope / 4 pre-existing);
    2 new pair-review lessons (#7 merge-by-type ruleset overrides; #8
    ~DEFAULT_BRANCH sentinel in conditions.ref_name.include); ratification
    documented (5 of 6 P1s collapse to deletions)

Bugs caught and fixed during the refactor:
  - spec had require_linear_history typo (correct: required_linear_history)
  - VALID_RULESET_RULE_TYPES was missing required_conversation_resolution
  - _ruleset_matches() was running the conditions.ref_name.include comparison
    after stripping the sentinel (should have skipped it entirely)
  - resolve_repo_profile() was REPLACING the rules array in ruleset_overrides
    (should have MERGED by type)

CHIT trail unsigned-local. Three-body: delivery=Mavis, control=DARKXSIDE,
memory=this commit + the spec + the LEARNINGS file.

* test(tools): rewrite 55 tests for ruleset-only branch_protection

The 44 old tests targeted the old classic+ruleset tool shape
(_build_classic_body, _diff_required_status_checks, _diff_review_policy,
_diff_boolean_field). Replaced with 55 tests across 9 groups for the
post-ratification ruleset-only API:

  A. SpecValidatorTests (13 tests)
     - spec shape, rule type validation, target/enforcement validation,
       override->profile cross-check, multi-error collection, load_spec
       path + skip-validation flag, validator set completeness
       (required_conversation_resolution, required_linear_history)

  B. ResolveRepoProfileTests (7 tests)
     - default + ruleset_override merge, unknown repo/profile raise,
       no-input-mutation, MERGE-BY-TYPE semantics (B6, B7)

  C. ResolveBranchTests (4 tests)
     - override wins, .gitmodules lookup matches workflow logic,
       no-override-no-gitmodules -> main, slug extraction for PMOVES.AI

  D. RulesetDiffTests (7 tests)
     - compliant, missing rule, extra rule, drifted parameters,
       drifted bypass_actors, drifted enforcement, ~DEFAULT_BRANCH
       sentinel handling (D7)

  E. AuditTests (5 tests)
     - compliant repo, no rulesets drift, explicit profile, unknown
       repo raise, per-ruleset re-fetch for bypass_actors (lesson #1)

  F. ApplyTests (7 tests)
     - dry-run creates, live creates, update existing with drift,
       skip in-sync, per_repo ruleset_overrides, unknown repo raise,
       strip ~DEFAULT_BRANCH sentinel

  G. DriftCheckTests (3 tests)
     - one report per repo, org filter, audit error surface

  H. CLITests (4 tests)
     - exit 0 compliant, exit 1 drift, exit 2 any-repo drift,
       default dry-run

  I. GHErrorPathTests (4 tests)
     - gh missing, gh timeout, gh nonzero stderr, "Branch not protected"
       returns None (404 is expected state)

Two new pair-review lessons (B6/B7 merge-by-type + D7 ~DEFAULT_BRANCH
sentinel) are codified in the LEARNINGS file.

CHIT trail unsigned-local.

* docs(agnote): Mavis::BRANCH-PROTECTION-V0-RATIFICATION-REFACTOR trail row

Records the 2026-08-10 refactor of the branch_protection tool to
ruleset-only per the operator's ratification (PR #2490 review id
4893614185). Captures the 5-of-6 P1s collapse to deletions, the
ownership split (tool = rulesets, workflow = classic), the spec v2
upgrade, the 4 additional bugs caught and fixed during the refactor
(require_linear_history typo, missing rule type, ~DEFAULT_BRANCH
sentinel handling, merge-by-type ruleset overrides), and the 55-test
rewrite.

CHIT trail unsigned-local. Three-body: delivery=Mavis, control=DARKXSIDE,
memory=this trail.

* refactor(tools): actually delete branch_protection_migrate_pmai.py

The 2026-08-10 ratification said the migration script goes away: classic
branch protection is not deprecated, rulesets layer with it, and "the most
restrictive version of the rule applies" — so there is nothing to migrate
away from and no reason to DELETE classic protection before a replacement
exists.

The refactor commit b0fbf68 rewrote branch_protection.py to ruleset-only
but left the script and its 15 tests on disk, while the PR comment reported
them as deleted. Verified against the tree: both files were still tracked at
78157b7. This makes the reported state the real state.

That closes the four findings that only existed because of the script:
  N1  DELETE fires before any replacement (with a test asserting it should)
  N2  signed commits + linear history silently dropped by the migration
  N3  captured_required_status_checks captured, printed, never used
  #20 migration test docstrings

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tools): make the resolved branch load-bearing in the ruleset writer

Five defects, all in the path between "the spec says which branch" and
"the ruleset GitHub actually stores". Verified against the live org, not
just the diff.

1. ~DEFAULT_BRANCH was stripped, never substituted (CRITICAL)
   _build_ruleset_body removed the sentinel from conditions.ref_name.include
   and put nothing back, so a created ruleset carried an EMPTY include list
   and matched no ref. It now takes the resolved branch and writes
   refs/heads/<branch>. The old test asserted only assertNotIn(sentinel),
   which an empty list satisfies — that is how it shipped.

2. The diff SKIPPED the include comparison whenever the spec used the
   sentinel, so a ruleset pinned to the wrong branch reported compliant.
   _ruleset_matches now takes the branch and resolves the sentinel on the
   EXPECTED side only. A live ~DEFAULT_BRANCH stays unresolved on purpose:
   it means "whatever GitHub currently calls default", which is not the
   branch we mean.

   Live evidence for why both matter: PMOVES-hermes-agent's default branch
   is main, but the monorepo consumes PMOVES.AI-Edition-Hardened. The
   ruleset applied in Slice 2 targets ~DEFAULT_BRANCH -> main. The branch
   that actually ships has no ruleset, and audit called it compliant. It
   now reports drift. This is N4 in production, not in theory.

3. .gitmodules lookup used `slug in section`, a substring match. The slug
   PMOVES-nats matched submodule "PMOVES-nats-server" and would write that
   repo's branch. Now matches the exact section name or the url basename.

4. resolve_branch step 3 looped over EVERY profile and returned the first
   branch it found, so one profile declaring a branch would leak it onto
   every repo without an override. It now takes the resolved profile name
   and reads only that profile.

5. apply crashed on `created.get` when a POST returned an empty body
   (_gh_api returns None). The write had already happened, so the repo was
   left changed with no entry in `applied`. Now records it with a fallback id.

Also: rule parameters were compared by strict equality, but GitHub echoes
back its own defaults (required_reviewers, allowed_merge_methods) that the
spec never declares — every audit reported permanent drift and every apply
re-PUT a correct ruleset. Comparison is now a subset over spec-declared keys
only, which is what makes drift_check trustworthy enough to run on a cron.

Tests: 64 pass (was 55). New: C5-C7 (exact + url-basename match, profile
scoping), D8-D11 (include drift on a non-default branch, live sentinel not
silently matched, API-defaulted params not drift, declared mismatch still
reported), F7-F9 (substitution, no spec mutation, empty POST body).
Fixed A11, which patched read_text but not exists() and so passed on the
"spec not found" error without ever reaching validation; and I2-I4, which
depended on a real gh binary being on PATH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(branch-protection): correct the baseline + LEARNINGS against the tree

- NATS catalog link was ../nats-subjects.md, which resolves to
  pmoves/docs/nats-subjects.md — a file that does not exist. Repointed at
  the canonical .claude/context/nats-subjects.md. (A prior comment marked
  this fixed; it was not.)
- require_linear_history -> required_linear_history everywhere. The v2
  contract and the GitHub rule type both use the required_ prefix, and an
  operator copying the table into pmoves_standard.json would fail validation.
- Lesson count 6 -> 8, and "5 of 6 P1s" -> "6 of 6" (the section lists six:
  N1, N2, N3, P1-A, P1-C, N8).
- Rewrote lesson 8. It codified "skip the include check when the spec uses
  the sentinel" as the right behavior; that was the bug. Replaced with the
  general form: a sentinel a builder strips but never substitutes is a
  silent no-op — resolve it, and assert on what replaced it rather than on
  its absence.
- Lesson 5 notes that the migration script it was learned on is gone.
- Documented the release gate on --no-dry-run: claim -> work (dry-run,
  confirm the resolved branch) -> sign -> release, with post-apply evidence.
  If signing is unavailable, the release stays pending.

Two corrections that came out of reading the workflow rather than the diff:

- The doc said the monorepo profile "layers on top of whatever classic
  protection branch-protection-sync.yml writes". It does not.
  The workflow derives its scope from .gitmodules and PMOVES.AI is not a
  submodule of itself, so on the monorepo there is no second writer and
  required_approving_review_count: 1 is the only review gate in play. That
  also means the N5 layering deadlock cannot apply to this profile — the
  fork profile already resolves it at 0.
  Flagged for the operator instead: apply --no-dry-run on PMOVES.AI would
  newly enforce required_signatures and required_linear_history on main.
  Both are real behavior changes to the merge flow, so they are called out
  as decisions rather than defaults.
- Added the 2026-08-10 audit finding: the Slice 2 rulesets on
  PMOVES-hermes-agent and PMOVES-pinokio target ~DEFAULT_BRANCH, so the
  hardened gitlink branch is ungated. Remediation is a re-apply behind the
  release gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(agnote): 4090-CLAUDE::PR2490-TRIM-16-THREADS trail row

Appended as a correction rather than an edit to the preceding row: that
row recorded the migration script as deleted and the ~DEFAULT_BRANCH
include-skip as correct behavior, and both were wrong against the tree.
The historical row stays as written; this one records what was actually
found and what changed.

Also records the two decisions left to the operator (the PMOVES.AI
--no-dry-run, and the re-apply that remediates the wrong-branch rulesets
on the two Slice 2 forks) rather than taking them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(learnings): lesson 9 — a test that can only assert absence cannot say no

Promotes the root cause of lesson 8 to its own entry, at the team lead's
request, because the fix is a habit and the failure mode is silent.

The guard on the sentinel substitution was
`assertNotIn("~DEFAULT_BRANCH", includes)`, which passes on an empty list.
It therefore held green across exactly the two states it existed to
distinguish: sentinel correctly replaced by a real ref, and sentinel
deleted with nothing put back. A test that could only report success,
guarding a ruleset whose empty include list matched no ref while `apply`
printed "applied".

The tell is structural rather than domain-specific, so the lesson is
written to generalize: an assertion whose predicate is satisfied by the
empty/null/absent case is not a gate. assertNotIn, assertNotEqual,
assertFalse, "no error raised", an empty `grep -v`, `rc == 0` on a command
that no-ops when misconfigured — each admits a degenerate state alongside
the intended one. When a transform removes something, assert on what
replaced it.

With three checks in cost order: ask what the empty case does; mutate the
implementation to the degenerate state and confirm the test goes red (a
guard that survives its own sabotage was never a guard); and for tools
that write to an external system, verify against live state once. Here a
single `gh api ... --jq .default_branch` collapsed the whole question.

Lesson count 8 -> 9 in both this file and the baseline doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Mavis <Mavis@pmoves.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant