TDD: merge_invariants() does not enforce non_overridable flag on GLOBAL invariants — plan-level invariants silently override security constraints #10379

Open
opened 2026-04-18 09:18:06 +00:00 by HAL9000 · 0 comments
Owner

Metadata

Field Value
Branch tdd/m3-merge-invariants-non-overridable
Commit Message test(domain/invariant): add @tdd_expected_fail scenario for merge_invariants ignoring non_overridable GLOBAL invariants
Milestone v3.2.0

Background and Context

This is the TDD counterpart to the companion bug issue: merge_invariants() does not enforce non_overridable flag on GLOBAL invariants.

The Invariant domain model in src/cleveragents/domain/models/core/invariant.py defines a non_overridable field (lines 86–92):

non_overridable: bool = Field(
    default=False,
    description=(
        "When True and scope is GLOBAL, this invariant cannot be "
        "overridden by lower-scope invariants during reconciliation"
    ),
)

However, the merge_invariants() function (lines 166–197) never checks this flag. It iterates plan → project → global in precedence order and uses a seen set to de-duplicate by text. This means a plan-level invariant with the same text as a non_overridable=True GLOBAL invariant will silently override it — the global version is excluded from the result.

Exact code with the bug:

# src/cleveragents/domain/models/core/invariant.py, lines 185–197
seen: set[str] = set()
result: list[Invariant] = []

for inv_list in (plan_invariants, project_invariants, global_invariants):
    for inv in inv_list:
        if not inv.active:
            continue
        key = inv.text.strip().lower()
        if key not in seen:  # ← non_overridable is NEVER checked here
            seen.add(key)
            result.append(inv)

return result

A failing test must be written that proves this bug exists before the fix is implemented.

Expected Behavior

A Behave scenario tagged @tdd_issue @tdd_issue_<N> @tdd_expected_fail should:

  1. Create a GLOBAL Invariant with non_overridable=True and a specific text (e.g., "no external network access")
  2. Create a PLAN-level Invariant with the same text but non_overridable=False
  3. Call merge_invariants(plan_invariants=[plan_inv], project_invariants=[], global_invariants=[global_inv])
  4. Assert that the result contains the GLOBAL invariant (the non_overridable=True version)
  5. Assert that the result does NOT silently drop the global invariant in favour of the plan-level one

This test must fail on the current codebase (because merge_invariants ignores non_overridable), confirming the bug exists.

Acceptance Criteria

  • A Behave scenario exists tagged @tdd_issue, @tdd_issue_<N>, and @tdd_expected_fail
  • The scenario creates a GLOBAL invariant with non_overridable=True and a PLAN invariant with the same text
  • The scenario calls merge_invariants() and asserts the GLOBAL non_overridable invariant is present in the result
  • The scenario fails on the current codebase (confirming the bug)
  • The scenario passes after the fix from the companion bug issue is applied
  • Step definitions are implemented (no placeholder pass statements)
  • All other existing tests continue to pass

Subtasks

  • Create or update a feature file (e.g., features/domain_invariant_merge.feature) with a scenario tagged @tdd_issue @tdd_issue_<N> @tdd_expected_fail
  • Write the scenario: given a GLOBAL non_overridable invariant and a PLAN invariant with the same text, when merge_invariants() is called, then the GLOBAL invariant is present in the result
  • Implement step definitions in the appropriate steps file
  • Verify the scenario fails on the current codebase (expected — confirms the bug)
  • Run nox -s unit_tests to confirm no regressions in other tests
  • Verify coverage ≥ 97% via nox -s coverage_report

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • The failing Behave scenario is committed on the tdd/m3-merge-invariants-non-overridable branch.
  • The scenario is tagged correctly with @tdd_issue, @tdd_issue_<N>, and @tdd_expected_fail.
  • The scenario fails before the fix and passes after the fix from the companion bug issue is applied.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.

Automated by CleverAgents Bot
Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor

## Metadata | Field | Value | |---|---| | **Branch** | `tdd/m3-merge-invariants-non-overridable` | | **Commit Message** | `test(domain/invariant): add @tdd_expected_fail scenario for merge_invariants ignoring non_overridable GLOBAL invariants` | | **Milestone** | v3.2.0 | ## Background and Context This is the TDD counterpart to the companion bug issue: **`merge_invariants()` does not enforce `non_overridable` flag on GLOBAL invariants**. The `Invariant` domain model in `src/cleveragents/domain/models/core/invariant.py` defines a `non_overridable` field (lines 86–92): ```python non_overridable: bool = Field( default=False, description=( "When True and scope is GLOBAL, this invariant cannot be " "overridden by lower-scope invariants during reconciliation" ), ) ``` However, the `merge_invariants()` function (lines 166–197) never checks this flag. It iterates plan → project → global in precedence order and uses a `seen` set to de-duplicate by text. This means a plan-level invariant with the same text as a `non_overridable=True` GLOBAL invariant will silently override it — the global version is excluded from the result. **Exact code with the bug:** ```python # src/cleveragents/domain/models/core/invariant.py, lines 185–197 seen: set[str] = set() result: list[Invariant] = [] for inv_list in (plan_invariants, project_invariants, global_invariants): for inv in inv_list: if not inv.active: continue key = inv.text.strip().lower() if key not in seen: # ← non_overridable is NEVER checked here seen.add(key) result.append(inv) return result ``` A failing test must be written that proves this bug exists before the fix is implemented. ## Expected Behavior A Behave scenario tagged `@tdd_issue @tdd_issue_<N> @tdd_expected_fail` should: 1. Create a GLOBAL `Invariant` with `non_overridable=True` and a specific text (e.g., `"no external network access"`) 2. Create a PLAN-level `Invariant` with the same text but `non_overridable=False` 3. Call `merge_invariants(plan_invariants=[plan_inv], project_invariants=[], global_invariants=[global_inv])` 4. Assert that the result contains the GLOBAL invariant (the `non_overridable=True` version) 5. Assert that the result does NOT silently drop the global invariant in favour of the plan-level one This test **must fail** on the current codebase (because `merge_invariants` ignores `non_overridable`), confirming the bug exists. ## Acceptance Criteria - [ ] A Behave scenario exists tagged `@tdd_issue`, `@tdd_issue_<N>`, and `@tdd_expected_fail` - [ ] The scenario creates a GLOBAL invariant with `non_overridable=True` and a PLAN invariant with the same text - [ ] The scenario calls `merge_invariants()` and asserts the GLOBAL non_overridable invariant is present in the result - [ ] The scenario **fails** on the current codebase (confirming the bug) - [ ] The scenario **passes** after the fix from the companion bug issue is applied - [ ] Step definitions are implemented (no placeholder `pass` statements) - [ ] All other existing tests continue to pass ## Subtasks - [ ] Create or update a feature file (e.g., `features/domain_invariant_merge.feature`) with a scenario tagged `@tdd_issue @tdd_issue_<N> @tdd_expected_fail` - [ ] Write the scenario: given a GLOBAL non_overridable invariant and a PLAN invariant with the same text, when `merge_invariants()` is called, then the GLOBAL invariant is present in the result - [ ] Implement step definitions in the appropriate steps file - [ ] Verify the scenario fails on the current codebase (expected — confirms the bug) - [ ] Run `nox -s unit_tests` to confirm no regressions in other tests - [ ] Verify coverage ≥ 97% via `nox -s coverage_report` ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - The failing Behave scenario is committed on the `tdd/m3-merge-invariants-non-overridable` branch. - The scenario is tagged correctly with `@tdd_issue`, `@tdd_issue_<N>`, and `@tdd_expected_fail`. - The scenario fails before the fix and passes after the fix from the companion bug issue is applied. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#10379
No description provided.