fix(invariant): persist invariants to database via InvariantRepository and Alembic migration #8684

Merged
HAL9000 merged 15 commits from fix/invariant-database-persistence into master 2026-06-18 22:19:37 +00:00

15 Commits

Author SHA1 Message Date
CleverAgents Bot 525a0e1d56 fix(invariant): route persistence through repository
CI / push-validation (pull_request) Successful in 27s
CI / load-versions (pull_request) Successful in 27s
CI / lint (pull_request) Successful in 46s
CI / build (pull_request) Successful in 56s
CI / quality (pull_request) Successful in 1m27s
CI / typecheck (pull_request) Successful in 1m35s
CI / security (pull_request) Successful in 1m35s
CI / helm (pull_request) Successful in 3m17s
CI / unit_tests (pull_request) Successful in 5m42s
CI / integration_tests (pull_request) Successful in 9m8s
CI / docker (pull_request) Successful in 2m24s
CI / coverage (pull_request) Successful in 10m14s
CI / status-check (pull_request) Successful in 3s
2026-06-18 11:36:35 -04:00
drew d65c4a172d fix(invariant): serialize database-backed service mutations 2026-06-18 11:36:35 -04:00
HAL9000 19dc9c231f fix(invariant): neutralise legacy m3_001 migration + drop stale model fixtures
Commit f2626d66e removed the legacy ``InvariantModel`` (id/description/
is_active) and rewired ``InvariantModel`` onto the new ``id/text/scope/
source_name/active/non_overridable`` schema owned by
``m11_001_standalone_invariants``.  Two pieces of the previous design
remained behind and were breaking CI:

1. ``m3_001_invariants_table`` still issued ``op.create_table('invariants',
   ...)`` for the obsolete column set.  When the upgrade chain reached
   ``m11_001_standalone_invariants`` the same table was created a second
   time, raising ``OperationalError: table invariants already exists`` in
   ``before_scenario`` for every behave + robot scenario that
   initialises the test database.  That is the root cause of the 119
   ``errored`` scenarios in the CI ``unit_tests`` job and the
   integration_tests collapse on PR #8684.

   Turn ``m3_001`` into a no-op ``upgrade``/``downgrade`` so the
   historical revision id stays reachable by the downstream merge
   migrations (``m3_002_merge_invariants_and_a5_006`` and
   ``m9_004_merge_invariants_branch``) without touching the new
   schema that ``m11_001`` now owns.

2. ``robot/invariant_model.robot`` + ``robot/helper_invariant_model.py``
   and ``features/invariant_model.feature`` +
   ``features/steps/invariant_model_steps.py`` were smoke-tests written
   for the removed legacy schema (``description``, ``is_active``, UUID
   ids).  They instantiate ``InvariantModel(description=..., is_active=...)``
   and assert an ``ix_invariants_is_active`` index that the new schema
   does not have, so every scenario in the suite fails with
   ``TypeError: 'description' is an invalid keyword argument``.  The
   real persistence contract is already covered by
   ``features/tdd_invariant_persistence.feature``,
   ``robot/tdd_invariant_persistence.robot`` and
   ``robot/invariant_cli.robot`` against the current schema; the
   legacy-schema fixtures have no remaining users (verified via
   ``grep -rn helper_invariant_model``).  Delete them.

Also runs ``ruff format`` on ``models.py`` to drop a trailing blank
line that was tripping the ``format`` nox session in CI ``lint``.

Verified locally:

* ``ruff format --check .`` -> 2298 files already formatted.
* ``ruff check src/ scripts/ examples/ features/ robot/ .opencode/``
  -> All checks passed.
* ``nox -s unit_tests-3.13 -- features/tdd_invariant_persistence.feature``
  -> 4 scenarios passed, 0 failed, 0 errored.
* ``nox -s integration_tests-3.13 -- robot/tdd_invariant_persistence.robot
  robot/invariant_cli.robot robot/`` -> only the 8 deleted
  ``Suites.Robot.Invariant Model`` cases failed; deleting the suite
  removes them entirely.

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
CleverAgents Bot 4ed01ec0c1 fix(db): resolve invariant model migration head 2026-06-18 11:36:35 -04:00
HAL9000 c1dca18654 fix(invariant): use json format in persistence helper list calls
The helper used the CLI default Rich table renderer and substring-checked
for the invariant text in the captured output. With CliRunner's narrow
default console width the Rich table soft-wraps "Must validate inputs"
across two visual rows, so the substring check always failed even though
the invariant was correctly persisted and retrieved from the database.

Pass --format json on the list invocations: the JSON serialiser emits the
text verbatim so the substring assertion now reflects persistence rather
than column-width formatting. Verified locally by running all three
helper subcommands against a fresh SQLite DB — all three now print their
ok markers.

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
HAL9000 13e23a6a8a fix(invariant): isolate per-suite DB in tdd_invariant_persistence.robot
The TDD invariant persistence suite was using `Setup Test Environment`
(no DB isolation) but its helper script (helper_tdd_invariant_persistence.py)
relies on InvariantService cross-instance persistence. Without
CLEVERAGENTS_DATABASE_URL set, InvariantService falls back to in-memory
mode (invariant_service.py:90) and the second helper invocation cannot
see invariants written by the first — every TDD scenario fails:

  - TDD Invariant Add Then List Project Across Invocations
  - TDD Invariant Add Then List Global Across Invocations
  - TDD Invariant Remove Cross Instance

Switch the suite setup to `Setup Test Environment With Database
Isolation` so each suite gets a unique sqlite path via the existing
keyword in robot/common.resource. This also prevents pabot worker
collisions on the default `sqlite:///cleveragents.db`.

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
HAL9000 ea967142df fix(invariant): expose non_overridable via add_invariant + round-trip from DB
Three connected regressions surfaced by the new DB-backed list path
on the invariant_reconciliation_actor scenarios:

- `InvariantService.add_invariant()` had no way to set `non_overridable`,
  so the BDD step `step_add_non_overridable_global` was bypassing the
  service entirely (writing straight into `_invariants`). Once the
  service became DB-backed for ad-hoc constructors, those direct dict
  writes were invisible to `list_invariants()` and the reconciler saw a
  regular global invariant.
- `InvariantModel.to_domain()` was dropping the `non_overridable` column
  on read — it persisted correctly via `from_domain` but the round-trip
  silently lost the flag.
- The step file imported `Invariant` + `ULID` only for the bypass that
  is now gone; `ULID` is dropped, `Invariant` stays (still used in the
  `isinstance` assertion further down).

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
HAL9000 6e29206e7e fix(invariant): make InvariantService DB-backed for ad-hoc constructors
The BDD persistence scenarios construct ``InvariantService()`` directly
(simulating fresh CLI invocations), bypassing the DI container that
otherwise wires database_url + UnitOfWork. Without these hooks the
no-arg constructor silently reverted to in-memory mode and the
"persists across process restarts" guarantee broke under tests.

- Read CLEVERAGENTS_DATABASE_URL in the constructor when no database_url
  is passed, so ad-hoc callers still get cross-instance persistence.
- Route every DB-backed branch (add/list/remove/get_by_id) through
  _ensure_session_factory() so the session factory is materialised on
  first use, not only after a prior add.
- Drop class_=Session from sessionmaker — Session was only imported
  under TYPE_CHECKING so this raised NameError at runtime.  The default
  Session class is fine.
- Invoke MigrationRunner.init_or_upgrade() before handing the engine to
  sessionmaker so the test harness's template-DB patch (and prod
  migrations) gets a chance to materialise the invariants table before
  the first INSERT.
- Drop @mock_only tag from tdd_invariant_persistence.feature.  The
  feature now exercises real SQLite via the persistence service; the
  tag was a leftover from the in-memory-only era and prevented the
  per-scenario template copy from triggering.
- Make CLI list-output assertion robust against Rich's table line-wrap
  (the long invariant text is split across two rows of the same
  column with │ borders interleaved; word-level membership is the
  reliable check).

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
HAL9000 d9f66b44f3 fix(invariant): resolve alembic multi-heads + typecheck errors
- Chain m11_001_standalone_invariants migration onto m9_003 (was wrongly
  pointing at m10_001, which already had m9_003 as a child — causing
  "multiple heads" alembic error that broke unit_tests and
  integration_tests entirely).
- Convert InvariantModel columns to Mapped[T] + mapped_column(...) style
  so Pyright sees them as plain Python types instead of Column[T]. Fixes
  6 reportAttributeAccessIssue errors in invariant_repository.update()
  and 1 reportArgumentType error in InvariantModel.to_domain().
- Drop now-unnecessary cast(...) calls in to_domain().
- Use _ensure_session_factory()() directly in invariant_service so
  Pyright tracks the non-None factory through the call chain.
- Apply ruff format to both touched files.

ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00
controller-ci-rerun 19c67c0260 chore: re-trigger CI [controller] 2026-06-18 11:36:35 -04:00
controller-ci-rerun 9b8e27b924 chore: re-trigger CI [controller] 2026-06-18 11:36:35 -04:00
controller-ci-rerun 7ec3443031 chore: re-trigger CI [controller] 2026-06-18 11:36:35 -04:00
controller-ci-rerun c29918c7fa chore: re-trigger CI [controller] 2026-06-18 11:36:35 -04:00
controller-ci-rerun 9a93e9fb24 chore: re-trigger CI [controller] 2026-06-18 11:36:35 -04:00
HAL9000 421429c65f fix(invariant): persist invariants to database via InvariantRepository and Alembic migration
ISSUES CLOSED: #8573
2026-06-18 11:36:35 -04:00