Skip to content

fix(OMN-15245): fail-closed changed-test coverage in the governed selector - #2505

Merged
jonahgabriel merged 2 commits into
devfrom
jonah/omn-15245-selector-changed-tests-invariant
Jul 27, 2026
Merged

jonahgabriel merged 2 commits into
devfrom
jonah/omn-15245-selector-changed-tests-invariant

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator

Ticket

OMN-15245 — omnibase_infra governed selector picks only tests/unit/ for a scripts/ + tests/scripts/ diff; second recorded instance dropped five changed tests/integration/** modules.

Defect

The governed selector (scripts/ci/detect_test_paths.py + test_selection_adjacency.yaml) could drop a test file the diff itself changed. Two live instances, both green CI runs that never collected the changed test modules:

Instance Diff Selector emitted Consequence
OMN-15218 / #2493 3 × scripts/** + 2 × tests/scripts/** (47 new tests) ["tests/unit/"] 22053 tests ran, none of them the 47 new ones
OMN-15263 / #2504 6 changed test files ["tests/ci/"] (run 30296123866, Detect Changes job 90082641865) 975 tests ran; the five changed tests/integration/** modules were never collected on the PR that existed to repair them

Both reproduce deterministically against compute_selection() at dev@0b65877c.

Named per the ticket DoD — this is all three of the candidate causes at once, not one:

  1. Directory-family mapping gap (_resolve): only src/omnibase_infra/**, tests/unit/** and the CI-process family contributed anything. tests/integration/**, tests/scripts/**, tests/replay/**, tests/nodes/**, tests/services/**, tests/audit/**, tests/redeploy/** contributed nothing, and scripts/** (deploy-path + governance-guard code) contributed nothing either.
  2. Fail-open default, not escalation: with no contribution, step 6 fell back to ["tests/unit/"] — it never escalated. Confirmed live, not inferred.
  3. A stale assertion encoding the bug: test_integration_test_only_change_does_not_select_unit_tests asserted paths == [] on the premise that "the integration job runs all integration tests on every PR anyway". That premise is false. No CI job runs all of tests/integration/ on a PR — test-parallel is the only job that runs it at all, and in smart mode it runs only selected_paths.

Fix

Invariant (fail-closed): any CHANGED path under tests/ is covered by the emitted selection — its own directory at minimum. Narrowing may add tests; it may never drop one the diff touched.

  • _uncovered_changed_test_dirs() runs last in _resolve(), so it sees everything the other mappings already cover and is purely additive (a changed test already covered contributes nothing).
  • scripts/** → tests/scripts/ + tests/unit/scripts/ — the two families that actually exercise scripts/. This replaces the blanket tests/unit/ fallback for script diffs, which had no causal relationship to them (nothing under src/ imports scripts/).
  • New CHANGED_TEST_UNNARROWABLE full-suite escalation: a changed test module directly under tests/ root has no containing directory below tests/ itself. Emitting tests/ as a smart selection would run the whole suite under the smart step's split count; escalate to the real 15-split full suite instead.
  • UNRUNNABLE_TEST_PREFIXES — documented, and not a narrowing carve-out. tests/integration/docker/ is --ignored by both pytest steps in ci.yml (full suite included) and has its own gate in docker-build.yml whose paths filter is tests/integration/docker/**; tests/chaos/ and tests/performance/ are deselected by the job's marker expression. Selecting them cannot make them run, and when one is the sole selected path pytest exits 5 ("no tests ran") — a red gate that ran nothing.

Seams

  • selector → .github/workflows/ci.yml smart pytest step. Step passes --ignore=tests/integration/docker; the selector never emits that prefix, so no exit-5 and no ignored-path no-op. selected_paths still satisfies the TestPath pattern (trailing-slash directories) — the output contract is unchanged, no workflow edit needed.
  • selector → scripts/hooks/prepush_smart_tests.sh. This is the seam the invariant would have broken: the hook is unit-scoped by design and passes --ignore=tests/integration, so a selection containing tests/integration/** would have collected nothing from it and exited 5 on any diff whose only selected path was an integration directory — blocking a push on a gate that ran nothing. The hook now filters those out visibly (filter_prepush_runnable_paths, deferred-to-CI log line), and that function is executed by a test, not grepped.

Verification

RED-first, against exists-but-wrong (not absence):

  • New tests vs the pre-fix selector: 14 failed / 32 passed, including both recorded replays (test_omn15218_scripts_diff_selects_the_changed_test_modules, test_omn15263_test_file_diff_selects_the_changed_integration_modules) and all 7 dropped path families. Post-fix: 61 passed.
  • Hook seam, exists-but-wrong: with filter_prepush_runnable_paths present but matching the wrong pattern (tests/nonexistent/*), 2 failed / 3 passed; restored → 5 passed. The filter is proven by execution, not by grep.
  • No-regression on narrowing (the risk this fix could have introduced): a pure src/omnibase_infra/cli/foo.py diff still emits exactly ["tests/unit/cli/"], 1 split; src/omnibase_infra/services/foo.py still emits exactly its 5 reverse-dep unit dirs with no tests/ and no tests/integration/**; shared-module escalation still outranks the additive rule; the docs-only exemption (OMN-14753) still selects nothing, including for a .md under tests/.

Replay of both instances through the fixed selector:

OMN-15218 diff -> ['tests/scripts/', 'tests/unit/scripts/']            (both new test files collected)
OMN-15263 diff -> ['tests/ci/', 'tests/integration/infra/']            (all 5 non-docker changed modules collected)
this PR's diff -> ['tests/ci/', 'tests/scripts/', 'tests/unit/scripts/']

Gates — run on .200 (stickybeatz-studio) per rule 11a, patch-transferred with per-file sha256 verified equal on both hosts before the run, so no vacuous green from the edit-locality trap:

  • ruff format --check scripts/ tests/unit/scripts/ — 287 files already formatted
  • ruff check scripts/ci/ tests/unit/scripts/ — All checks passed
  • mypy scripts/ci/detect_test_paths.py scripts/ci/test_selection_models.py — no issues
  • pre-commit run --files over all 5 changed files — 42 hooks passed, 0 failed, no hook modified a file
  • pytest tests/ci/ tests/scripts/ tests/unit/scripts/ — 2286 passed, 5 skipped (skips are pre-existing host/env skips: GNU realpath and a sibling-repo probe)
  • Dogfood, end-to-end: bash scripts/hooks/prepush_smart_tests.sh on .200 selected tests/ci/ tests/scripts/ tests/unit/scripts/ from this PR's own diff and ran them green — the fixed selector selects the tests that prove the fixed selector.

Port check (ticket DoD)

Probed the sibling selectors live at their current dev checkouts with a tests/integration/foo/test_x.py-only diff:

  • omnibase_core → ['tests/unit/'] — same fail-open, changed test not collected. Port filed (see Refs).
  • omniclaude → ['tests/unit/'] — same class; this is the open OMN-14252 (tests/skills/**), so evidence was added there rather than duplicating a ticket.
  • omnimarket → ['tests/integration/'] — covers the changed file; not affected by this class.

Notes

  • Source-only diff. No lane mutation, no deploy, no workflow change, no adjacency-YAML change.
  • OCC companion owed — not self-authored (implementer ≠ author of own evidence); Evidence-Source line added once autobind mints it.
  • No merges, no --auto, no drafts, no skip tokens, no --no-verify.

Refs: OMN-15245, OMN-15218 (#2493), OMN-15263 (#2504), OMN-14252, OMN-14399, OMN-13969/13970, OMN-14194

Evidence-Source: OCC#5190
Evidence-Commit: 669ab8ec505d86866516d7e83684bcef66beec9b
Evidence-Ticket: OMN-15245

…ector

The governed selector could drop a test file the diff itself changed. Two
recorded live instances, both green CI runs that never collected the changed
test modules:

  * OMN-15218 / #2493 -- a scripts/ + tests/scripts/ diff selected
    ["tests/unit/"] (22053 tests, none of them the 47 new ones).
  * OMN-15263 / #2504 -- a six-test-file diff selected ["tests/ci/"]
    (run 30296123866, Detect Changes job 90082641865); the five changed
    tests/integration/** modules were never collected on the PR that
    existed to repair them.

Invariant: any CHANGED path under tests/ is covered by the emitted selection
(its own directory at minimum). Narrowing may add tests, never drop one the
diff touched. Applied last in _resolve() so it sees every other mapping.

Also:
  * scripts/** now maps to tests/scripts/ + tests/unit/scripts/ -- the two
    families that actually exercise scripts/. Previously scripts/ produced no
    selection at all and fell through to the blanket tests/unit/ fallback.
  * New CHANGED_TEST_UNNARROWABLE full-suite escalation: a changed test module
    directly under tests/ has no containing directory below tests/ itself.
  * UNRUNNABLE_TEST_PREFIXES documents the families the pytest job structurally
    cannot run (tests/integration/docker/ is --ignored by both pytest steps and
    has its own gate in docker-build.yml; tests/chaos/ and tests/performance/
    are marker-deselected). Selecting them cannot make them run and would make
    pytest exit 5 when one is the sole selected path.
  * Consumer seam: prepush_smart_tests.sh filters tests/integration/ out of its
    pytest invocation (it also passes --ignore=tests/integration), so the new
    integration selections cannot wedge a push on exit 5.

RED-first, exists-but-wrong (not absence): the new tests fail 14/14 against the
pre-fix selector, including both recorded replays; the hook seam test fails 2/2
when the filter pattern is wrong rather than missing.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f73d83d-e708-4ebc-8523-edeca251a28c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b65877 and f97bc84.

📒 Files selected for processing (5)
  • scripts/ci/detect_test_paths.py
  • scripts/ci/test_selection_models.py
  • scripts/hooks/prepush_smart_tests.sh
  • tests/unit/scripts/ci/test_detect_test_paths.py
  • tests/unit/scripts/test_prepush_smart_tests_seam.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonah/omn-15245-selector-changed-tests-invariant

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

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Blocking findings (critical): 0
Total findings: 0
Models succeeded: none

Note: All reviewer models failed or were unavailable. Degraded results are informational during the pilot phase (OMN-8468/OMN-8524) and do not block merge. Error: all review endpoints [192.168.86.201:8000 192.168.86.201:8001 ] unreachable — preflight short-circuit (no models available)


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical findings No
blocked CRITICAL findings found Yes
degraded All models unavailable (infra) No (pilot)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8468/OMN-8524)

jonahgabriel added a commit to OmniNode-ai/onex_change_control that referenced this pull request Jul 27, 2026
…ibase_infra#2505 (#5190)

* evidence(OMN-15245): author OCC companion for OmniNode-ai/omnibase_infra#2505

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 8588047e8f70e202e938ddeae68d462eafe23edd.

* evidence(OMN-15245): self-bind OCC#5190 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@jonahgabriel
jonahgabriel merged commit 45d4709 into dev Jul 27, 2026
104 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-15245-selector-changed-tests-invariant branch July 27, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant