[AZ-493] Cycle 3 batch 3: integration test DB-reset hook
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful

AZ-493 (2 SP): replace the cycle-2 wallclock-seeded _coordinateCounter
workaround with a proper Postgres state-reset hook that runs at
integration test runner startup, eliminating the per-source-unique-index
collision risk that the persistent docker-compose Postgres volume
introduced post-AZ-484.

The reset is split into two surfaces:

* SatelliteProvider.TestSupport.IntegrationTestResetGuard - pure
  static class, I/O-free, unit-tested. Two independent guards: (a)
  ASPNETCORE_ENVIRONMENT must equal "Testing", (b) DB_CONNECTION_STRING
  Host must be in the allowed-host list (postgres, localhost, 127.0.0.1).
  Failure of either guard surfaces a structured operator-friendly
  InvalidOperationException.
* SatelliteProvider.IntegrationTests.IntegrationTestDatabaseReset -
  instance class owning the Npgsql side effects. Calls the guard then
  runs TRUNCATE TABLE route_regions, route_points, routes, regions,
  tiles RESTART IDENTITY CASCADE inside a single Npgsql transaction.

Spec-vs-reality: the task spec prescribed "DB name contains _test" as
Guard 2; the actual compose file uses Database=satelliteprovider and
DB rename is gated on user confirmation per coderule.mdc. Substituted
a Host allowlist as the equivalent guard (intent identical: reject
remote / production hosts). Recorded as Low/Spec-Gap in the review.

Program.cs adds --keep-state CLI flag and INTEGRATION_KEEP_STATE env
var (1/true) opt-outs so a developer can inspect leftover state when
debugging. Startup banner shows which path executed.
docker-compose.tests.yml gets ASPNETCORE_ENVIRONMENT=Testing +
passthrough for INTEGRATION_KEEP_STATE. scripts/run-tests.sh wires the
--keep-state flag through to compose.

UavUploadTests._coordinateCounter wallclock seed is retained as
defense-in-depth (per the task spec's implementer choice). The reset
is the primary isolation path; the seed is the belt-and-suspenders
fallback for --keep-state runs.

8 new unit tests in SatelliteProvider.Tests/TestSupport/
IntegrationTestResetGuardTests.cs cover Production/Staging/missing-env
throw, allowed-host case-insensitivity, disallowed-host rejection
with representative prod hostnames, and the AllowedHosts contract.

tests_integration.md gains a Reliability section that documents the
hook, the two guards, the truncate order, and the three opt-out forms.
module-layout.md TestSupport entry extended with the new pure guard
and the explicit "Npgsql stays in IntegrationTests" boundary.

Test-suite gate (AC-6) deferred to Step 16 Final Test Run per implement
skill convention. Per-batch review verdict: PASS_WITH_WARNINGS with 1
Low (spec-vs-reality on Guard 2, non-blocking).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 01:38:42 +03:00
parent c396740644
commit 745f4840e6
12 changed files with 385 additions and 13 deletions
@@ -0,0 +1,65 @@
# Batch Report — Batch 03 cycle 3
**Batch**: 03 (cycle 3)
**Tasks**: AZ-493 (integration test DB-reset hook)
**Date**: 2026-05-12
## Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|---------------|-------|-------------|--------|
| AZ-493_integration_test_db_reset_hook | Done | 3 added + 5 modified | 8 new unit tests for the guard (in `SatelliteProvider.Tests/TestSupport/IntegrationTestResetGuardTests.cs`); reset itself exercised by the existing integration suite at Step 16 | 6/6 ACs covered | 0 blockers; 1 spec-vs-reality note (see below) |
## AC Test Coverage: All covered (6 of 6)
## Code Review Verdict: pending (this batch report precedes per-batch review)
## Auto-Fix Attempts: 0
## Stuck Agents: None
## What was implemented
The reset is split into two parts so the guard logic is pure-string and unit-testable while the actual DB side effects live in the integration-tests project (which already depends on Npgsql).
### Added
- `SatelliteProvider.TestSupport/IntegrationTestResetGuard.cs` (pure static class). Validates `(environment, host)` against two rules: (a) `ASPNETCORE_ENVIRONMENT` MUST equal `"Testing"` (case-insensitive); (b) `Host` MUST be one of `postgres`, `localhost`, `127.0.0.1`. Throws `InvalidOperationException` with structured operator-friendly messages on either failure. The class is intentionally I/O-free so unit tests don't need Postgres.
- `SatelliteProvider.IntegrationTests/IntegrationTestDatabaseReset.cs` (instance class). Constructor takes a connection string; `EnsureCleanStateAsync()` calls the guard then runs `TRUNCATE TABLE route_regions, route_points, routes, regions, tiles RESTART IDENTITY CASCADE` inside an Npgsql transaction. FK-safe order is preserved as both the SQL argument list AND the public `TruncateOrder` `IReadOnlyList<string>`. Structured success log includes which tables were truncated and the Host/Database tuple so operators have an audit trail.
- `SatelliteProvider.Tests/TestSupport/IntegrationTestResetGuardTests.cs` (8 unit tests). Covers: Production / Staging / missing-environment all throw (AC-4); allowed hosts (`postgres`, `Postgres`, `POSTGRES`, `localhost`, `127.0.0.1`) all pass with case-insensitivity; disallowed hosts (`prod.example.com`, `rds.amazonaws.com`, `db.staging.internal`) all throw with the specific host name in the message; missing host throws; the `AllowedHosts` contract is asserted as an immutable expectation.
### Modified
- `SatelliteProvider.IntegrationTests/Program.cs`:
- Argument parsing now recognizes `--keep-state` (CLI) and `INTEGRATION_KEEP_STATE=1` / `INTEGRATION_KEEP_STATE=true` (env var).
- Reads `DB_CONNECTION_STRING` up front (alongside the existing `API_URL` read) so the reset and the test classes share the same connection string.
- After the API readiness probe and before any test class runs, calls `await new IntegrationTestDatabaseReset(connectionString).EnsureCleanStateAsync()`. Exits with code 1 if the guard throws (with the structured error message visible to the operator).
- Startup banner gains a `State : reset (clean DB at startup, AZ-493)` or `State : keep (DB reset skipped)` line so the run log makes it unambiguous which path executed.
- `SatelliteProvider.IntegrationTests/UavUploadTests.cs``_coordinateCounter` wallclock seed retained as **defense-in-depth** (per the AZ-493 task spec's implementer choice). An inline comment back-references AZ-493 and explains that the reset hook is the primary isolation path; the wallclock seed is the belt to the suspenders so `--keep-state` runs don't immediately collide on the per-source unique index.
- `docker-compose.tests.yml``integration-tests` service gains `ASPNETCORE_ENVIRONMENT=Testing` (the AZ-493 Guard 1) and `INTEGRATION_KEEP_STATE=${INTEGRATION_KEEP_STATE:-}` (developer-overridable env var). API service is unchanged.
- `scripts/run-tests.sh``--keep-state` flag added with usage docs; passes `INTEGRATION_KEEP_STATE=1` through to the compose run when set.
- `_docs/02_document/modules/tests_integration.md` — new `## Reliability` section documents the AZ-493 hook, the two guards, the truncate order, and the three opt-out forms (`./scripts/run-tests.sh --keep-state`, `INTEGRATION_KEEP_STATE=1`, `dotnet run ... -- --keep-state`). The UavUploadTests entry now flags the coordinate-counter wallclock seed as "promoted to defense-in-depth by AZ-493 cycle 3". The `## Dependencies` section now lists the ProjectReference to `SatelliteProvider.TestSupport` and the actual NuGet references (Npgsql 9.0.2, ImageSharp 3.1.11) instead of the previous "No project references" line.
- `_docs/02_document/module-layout.md` § TestSupport — extended with the `IntegrationTestResetGuard` entry, the "no new packages" note, and an explicit boundary: AZ-493's Npgsql-bearing reset class lives in `SatelliteProvider.IntegrationTests` (not TestSupport) so the Npgsql dependency doesn't leak into unit tests.
## AC Verification
| AC | Status | Evidence |
|----|--------|----------|
| AC-1: Empty-state on startup | ✓ | `TRUNCATE ... RESTART IDENTITY CASCADE` against `route_regions, route_points, routes, regions, tiles` runs in a single Npgsql transaction before any test class is reached |
| AC-2: Wallclock workaround no longer needed for back-to-back runs | ✓ (with belt-and-suspenders) | Reset hook ensures empty start. Wallclock seed retained as defense-in-depth per implementer choice — does not weaken AC-2 since the primary isolation mechanism is the reset, not the seed |
| AC-3: Opt-out preserves state | ✓ | `--keep-state` (CLI) and `INTEGRATION_KEEP_STATE` (env) both wired through `scripts/run-tests.sh` → docker-compose → Program.cs; startup banner shows which path executed |
| AC-4: Reset only fires in test environment | ✓ | 8 unit tests in `IntegrationTestResetGuardTests` cover Production/Staging/missing-env → throw; 3 disallowed-host examples → throw; allowed-host case-insensitivity → pass |
| AC-5: Documentation reflects new convention | ✓ | tests_integration.md `## Reliability` section is canonical; module-layout.md cross-references the guard's pure-vs-side-effectful split |
| AC-6: Existing tests pass unchanged | Deferred to Step 16 | All pre-AZ-493 test logic untouched; only Program.cs startup wiring + the new reset class were added |
## Spec-vs-reality notes
- **Guard 2 deviation from task spec**: The task spec § Safety prescribed "DB name contains `_test`" as the second guard. However, the existing test compose file uses `Database=satelliteprovider`*not* `satelliteprovider_test`. Renaming the database is gated on user confirmation per `coderule.mdc`. The "or equivalent" qualifier in the spec was used to substitute a **Host allowlist** (`postgres`, `localhost`, `127.0.0.1`) as the second guard. The intent is identical (refuse to truncate against a remote / production host) and the implementation is unit-tested with a representative production-shape host list (`prod.example.com`, `rds.amazonaws.com`, `db.staging.internal`) all hitting the rejection path.
- This is being recorded as a Low / Spec-Gap finding in the code-review report, mirroring the AZ-496 pattern (spec inaccuracy detected during implementation, documented and worked around without renaming production assets).
## Open follow-ups (non-blocking)
- **Test-DB rename decision** (optional future PBI): if the team wants to align with the spec's preferred two-guard model (`ASPNETCORE_ENVIRONMENT=Testing` + DB name contains `_test`), the integration-tests compose `Database=satelliteprovider` would become `Database=satelliteprovider_test`. This requires a DB rename + migration-runner verification + at least one round of integration-suite re-verification. Out of scope for AZ-493.
- **Reset performance under load**: AZ-493 NFR sets a < 1 s budget for `TRUNCATE` against an O(10K)-row DB. The current cycle's tile-table row count is far below that threshold; no measurement is in scope. A future cycle that exercises higher-volume scenarios should validate the NFR.
- **Truncate-order audit on schema changes**: noted in tests_integration.md § Reliability — new tables with FK relationships to `tiles`/`regions`/`routes` need a `TruncateOrder` revision. Add to the decompose-skill review checklist if the pattern recurs (suggested but not in scope for AZ-493).
## Next Batch: AZ-492 (Perf harness PT-07 + PT-08 + JWT-attach)
AZ-492 is 3 SP. Promotes the two deferred performance NFRs (PT-07: P95 latency-vs-load; PT-08: error-rate-under-pressure) into runnable scenarios under `scripts/run-performance-tests.sh`, and fixes the cycle-2-regressed JWT-attach so every perf scenario sends a valid Bearer token (post-AZ-487 the endpoints are protected, so every perf request currently 401s). With AZ-491 done, AZ-492 can consume `SatelliteProvider.TestSupport.JwtTokenFactory` indirectly via a small shell-side `python3 -c` minter that mirrors its parameters (or by shelling out to a tiny dotnet entry point — implementer's choice at batch start).
@@ -0,0 +1,54 @@
# Code Review Report — Batch 03 cycle 3
**Batch**: 03 (cycle 3) — AZ-493 (integration test DB-reset hook)
**Date**: 2026-05-12
**Verdict**: PASS_WITH_WARNINGS
## Findings
| # | Severity | Category | File:Line | Title |
|---|----------|----------|-----------|-------|
| 1 | Low | Spec-Gap | `_docs/02_tasks/done/AZ-493_integration_test_db_reset_hook.md` § Safety + § Risk 1 | Task spec prescribed "DB name contains `_test`" as Guard 2; production reality uses `Database=satelliteprovider` and rename requires user confirmation |
### Finding Details
**F1: Task spec prescribed "DB name contains `_test`" as Guard 2; production reality uses `Database=satelliteprovider`** (Low / Spec-Gap)
- Location: `_docs/02_tasks/done/AZ-493_integration_test_db_reset_hook.md` § Safety (line ~98) and § Risk 1 (line ~125)
- Description: The AZ-493 spec asserted "the integration test compose file already uses a separate DB" and prescribed the second guard as "the DB name contains `_test`". Reading `docker-compose.tests.yml` showed the integration tests use the same `Database=satelliteprovider` as the API service. Renaming the database is gated on user confirmation per `coderule.mdc` ("Do not rename any databases or tables or table columns without confirmation."). The spec's "or equivalent" qualifier was used to substitute a Host allowlist (`postgres`, `localhost`, `127.0.0.1`) as Guard 2.
- Suggestion: Capture this in the batch report (already done — Spec-vs-reality notes section) and consider a future PBI to align on the spec's preferred guard pattern *if* the team wants `_test` in the DB name. The current implementation provides identical safety (remote / production hostnames are rejected) and is unit-tested. No code action required. Pattern matches cycle-3 batch-1 F1 (AZ-496 Tests.csproj non-existence) — a recurring symptom of task specs encoding assumptions that aren't verified against the actual codebase before authoring.
- Task: AZ-493
## Phase-by-Phase Summary
| Phase | Result | Notes |
|-------|--------|-------|
| 1. Context Loading | OK | AZ-493 spec read; 6 ACs identified; defense-in-depth choice on `_coordinateCounter` documented before implementation |
| 2. Spec Compliance | OK | 6/6 ACs verified at code level. AC-6 (existing tests pass unchanged) waits on Step 16 final test gate; the structural prerequisite (Program.cs startup path is the only diff) is satisfied |
| 3. Code Quality | OK | Clean separation of pure guard logic (`IntegrationTestResetGuard` — static, I/O-free, easy to test) from side-effectful reset (`IntegrationTestDatabaseReset` — instance, owns the Npgsql connection). Single responsibility per class. Errors surface with structured operator-friendly messages. No silent suppression. Test class has 8 well-isolated `[Fact]` / `[Theory]` cases following Arrange/Act/Assert. |
| 4. Security Quick-Scan | OK | The guard's express purpose is preventing accidental truncate against non-test databases. Two independent checks (env sentinel + Host allowlist) — either alone would have prevented the cycle-2 scenario; together they reduce the false-positive risk to near zero. Unit tests confirm the rejection paths with representative production-shape hostnames |
| 5. Performance Scan | OK | Reset is O(table-count) Npgsql round-trips wrapped in a single transaction; AZ-493 NFR budget (< 1 s on O(10K) rows) is comfortably within Postgres TRUNCATE behavior |
| 6. Cross-Task Consistency | OK | The new `IntegrationTestResetGuard` lives in TestSupport alongside the AZ-491 `JwtTokenFactory` — both are pure utility surfaces consumed by both test projects. The boundary is enforced: `IntegrationTestDatabaseReset` (Npgsql-bearing) stays in IntegrationTests so the Npgsql dependency does not leak into unit tests. This explicit separation is the architectural improvement the AZ-491 batch teed up |
| 7. Architecture Compliance | OK | No production-component change. TestSupport's role as a shared/cross-cutting test library is reinforced (now serves two cycle-3 PBIs). No new ProjectReferences in production code. No cycles. Layering invariant preserved: TestSupport is *below* both test projects; production projects do NOT reference TestSupport |
## Cross-cutting observations (info, no finding)
- The guard's `AllowedHosts` is a constant list, not a configurable env var. This is intentional — the safety guarantee weakens if operators can override it. A future PBI that needs to support a non-listed Host (e.g., a Kubernetes pod hostname for ephemeral test environments) would extend the list in source, not via runtime config.
- The decision to retain the wallclock-seeded `_coordinateCounter` in `UavUploadTests` is recorded as defense-in-depth. AC-2 explicitly allowed either approach; the batch chose retention because the additional code surface is minimal and the safety benefit (against `--keep-state` collisions) is non-trivial. The retention comment back-references AZ-493 so future agents understand why the cycle-2 workaround is still there.
## Baseline Delta
| Class | Count | Notes |
|-------|-------|-------|
| Carried over | 0 | No Architecture findings; nothing recurring |
| Resolved | 1 (informal) | The cycle-2 Pattern 5 (Integration test state leakage from persistent Postgres volume) is structurally closed by this batch. Tracked in `retro_2026-05-11_cycle2.md` § 6 Pattern 5; not an Architecture-class baseline entry, so it does not appear in the cycle-1 baseline |
| Newly introduced | 0 | — |
## Verdict Logic
- 0 Critical, 0 High, 0 Medium, 1 Low → **PASS_WITH_WARNINGS**
- The Low finding is the spec-vs-reality observation about Guard 2 — documented, mitigated, and unit-tested. Does not block commit per the implement-skill auto-fix gate.
## Recommendation to /implement
Proceed to commit + push + tracker transition (Steps 11-13).