Run the 746 integration tests CI never executed - #131
Conversation
"P3 · Integration Tests · Backend" ran only `make test-integration-full`, which passes `-m full`. pyproject's addopts carry `-m 'not full'`, so the two selectors are exact complements: the job collected 206 of 952 integration tests and left 746 unexecuted on every branch. No other job covered them -- test-backend-legacy explicitly passes --ignore=tests/integration. The green check read as "integration tests pass" when it meant "the 22% of them carrying the full marker pass". Changes: - Add a `make test-integration` target for the default marker set, and run both targets in the P3 job so tests/integration/ is covered completely. - Give test-integration-full SUITE = integration-full. Artifact filenames are derived from SUITE, and both targets now run in one job, so sharing the value would have silently overwritten the first run's junit/coverage XML. Upload both pairs. - Repair the five tests that had rotted unnoticed. All five failed with ResponseValidationError on a missing `is_default`. They are stale fixtures, not a product defect: both files @patch their service class and hand-build the response dict, and those dicts were never updated when is_default was added to ModuleSourceResponse / BlueprintSourceResponse. The real serializers do emit it (module_source_service.py:240, blueprint_catalog_service.py:503), so the live endpoints were fine. - Validate each fixture against its response model inside the helper, so the next required field added to a schema fails at the fixture with the field named, rather than as a 500 buried in a FastAPI ExceptionGroup. Verified: 742 passed in the newly-enabled set, 187 passed / 19 skipped in the full set, ruff 0.15.2 clean, ci.yml parses as valid YAML. Closes #130 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KxvfrjQMigLtgt35REbQH
test-integration relied on pyproject's addopts (-m 'not full') to select the complement of test-integration-full. That is the same implicit coupling that produced #130: the two selectors could stop being complements -- by an addopts edit alone -- and the only symptom would be a quietly smaller test run behind a green check. Both targets now state their marker expression, so the pairing is visible in one place and a change to either is a change someone has to write down. Verified the set is unchanged: 742 collected with the explicit selector, 206 with -m full, 948 total in tests/integration/ -- an exact partition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KxvfrjQMigLtgt35REbQH
Self-review of #131Reviewed the change against the failure mode it exists to prevent: a selector pair that can silently stop covering the suite. One defect found in my own fix, plus the checks that came back clean. 1. The new target inherited its selector instead of stating it (fixed in
|
| Check | Result |
|---|---|
-m 'not full' set (newly enabled in CI) |
742 passed, 0 failed |
-m full set (behaviour unchanged) |
187 passed, 19 skipped |
| Repaired files | 23 passed |
ruff 0.15.2, tests/integration/ |
clean |
ci.yml |
parses as valid YAML |
make -n test-integration |
quoting survives make → pytest tests/integration/ -m 'not full' ... |
The drift guard was break-tested: dropping is_default from a fixture now fails at the helper with ValidationError ... is_default Field required naming the model and field, instead of the previous ResponseValidationError buried inside a FastAPI ExceptionGroup.
Deliberately not done
- No meta-test asserting CI runs both targets. It would have to parse
ci.ymland theMakefileas text; there is no precedent for that inbackend/tests/, and a brittle assertion over CI config is a poor trade for a check the explicit selectors now make legible. - Fail-fast left as is. If
make test-integrationfails, the step aborts andtest-integration-fulldoes not run. That is standard CI behaviour and the artifact upload already tolerates missing files (if-no-files-found: ignore). - The
fullmarker itself is untouched. It claims to mean "requires running Docker Compose app" while the job starts no stack and the tests pass anyway. Retiring or redefining it is a separate decision from restoring coverage.
Enabling the 746 tests immediately found one. CI ran them and two health checks failed with TypeError: the JSON object must be str, bytes or bytearray, not MagicMock raised from core/maintenance.py:64 by way of maintenance_middleware. get_maintenance_status() catches (RuntimeError, ConnectionError, RedisError) around its json.loads(), so anything unparseable under the maintenance key escapes instead. That call runs from middleware on EVERY request, which turns one bad value into a 500 for the whole API -- while the function's own docstring promises to degrade gracefully when it cannot get an answer. It now catches TypeError/ValueError too (JSONDecodeError and UnicodeDecodeError are ValueError subclasses, so decoding failures are covered) and logs the key it ignored. This is the only json.loads() on the request path; the other two middlewares have none. Why local runs missed it: settings.REDIS_URL is unset in the test container, so _get_redis() raised RuntimeError and was caught before reaching json.loads. ci.yml sets REDIS_URL globally, so CI got past it. Reproduced locally by exporting CI's env, which failed the same two tests, then confirmed the fix. The MagicMock stub in the health tests is deliberately left as it is. Making it a faithful redis double would let those tests pass without this fix and remove the only coverage that exercises the path. Verified with ci.yml's environment: 742 passed in tests/integration -m 'not full' (was 740 passed / 2 failed), 43 passed across the maintenance, backup and restore unit suites, ruff 0.15.2 clean. The new parametrised test was break-tested -- narrowing the except back to TypeError alone fails 3 of its 4 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KxvfrjQMigLtgt35REbQH
CI found a real bug — fixed in
|
| Check | Result |
|---|---|
tests/integration -m 'not full' with CI env |
742 passed, 0 failed (was 740 / 2) |
| maintenance + backup + restore unit suites | 43 passed |
| ruff 0.15.2 | clean |
Fixes #130.
The problem
"P3 · Integration Tests · Backend" ran 206 of 952 integration tests. The other 746 never executed in CI on any branch.
The job's only integration command is
make test-integration-full, which passes-m full.backend/pyproject.tomlsetsaddopts = "-v --tb=short -m 'not full'". The two selectors are exact complements, so the CI set and the default set are disjoint — and nothing ran the larger half.test-backend-legacyexplicitly passes--ignore=tests/integration, so it wasn't covering them either.A green "P3 · Integration Tests" meant "the 22% carrying the
fullmarker pass".What that hid
Five tests had been failing unnoticed, all with
ResponseValidationErroron a missingis_default:These are stale fixtures, not a product defect. Both files
@patchtheir service class outright and hand-build the response dict; those dicts were never updated whenis_defaultwas added to the response models. The real serializers do emit it —module_source_service.py:240computes it from the configured default URL/ref,blueprint_catalog_service.py:503delegates tois_default_blueprint_source()— so the live endpoints were always fine.Changes
make test-integration— new target for the default marker set. The P3 job now runs both targets, coveringtests/integration/completely.test-integration-full: SUITE = integration-full— artifact filenames derive fromSUITE, and both targets now run in one job, so sharing the value would have silently overwritten the first run's JUnit/coverage XML. Both pairs are uploaded.ExceptionGroup.TESTING.md,BRANCH_PROTECTION.md).Verification
make test-integration)fullset (unchanged)ci.ymlThe drift guard was break-tested: removing
is_defaultfrom a fixture now fails at the helper withValidationError ... is_default Field required, rather than the previous opaque response-validation 500.Left for a follow-up decision
The
fullmarker's declared meaning is "requires running Docker Compose app" (pyproject.toml:114), but the P3 job starts no Compose stack and those tests pass anyway — 187 passed, 19 skipped with nothing running. The marker looks vestigial, which is how the selector inversion survived. Whether to retire it is a separate call from restoring coverage, so this PR doesn't touch it.