[AZ-810] Clamp UAV test-fixture coordinates to OSM-valid range
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful

The AZ-810 metadata validator rejects lat outside [-90, 90] and lon
outside [-180, 180]. Two NextTestCoordinate() helpers seeded their
counter from `(Ticks/TicksPerSecond) % 1_000_000` and returned
`60 + n*0.0005`, producing lat well above 90° for almost any seed
(e.g. n=200000 -> lat=160). Pre-AZ-810 there was no validator and no
DB constraint, so the out-of-range values were silently accepted; the
new validator (correctly) rejected them at HTTP 400.

Clamp both helpers to non-overlapping OSM-valid ranges:
  - UavUploadTests.cs:           lat in [50, 70),  lon in [10, 40)
  - UavUploadValidationTests.cs: lat in [-70, -50), lon in [-40, -10)

Non-overlap (not the prior +5_000_000 counter offset) is what now
guarantees AZ-488 and AZ-810 suites don't collide on the per-source
UNIQUE index when both run against the same DB.

No production code change; AZ-810 validator behaviour is unchanged.

Also:
- Correct AC-9 in batch_04_cycle8_report.md: the original claim
  ("verified by tracing source") was a false-PASS; the autodev
  Step 11 test run surfaced the gap. Now confirmed by full-suite
  green (scripts/run-tests.sh --full).
- Add ring-buffer lesson on AC-verification standards for input-
  validation changes: tracing fixture variables to their generators
  is insufficient; only a green integration-test run is sound
  evidence for a "no-regression" AC.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-23 14:20:45 +03:00
parent bbe87835a9
commit b763da3f24
5 changed files with 26 additions and 12 deletions
@@ -511,9 +511,14 @@ public static class UavUploadTests
private static (double Latitude, double Longitude) NextTestCoordinate() private static (double Latitude, double Longitude) NextTestCoordinate()
{ {
// Spread test coordinates far enough apart to fall into distinct tile cells // Spread test coordinates far enough apart to fall into distinct tile cells
// so concurrent runs don't collide on the per-source unique index. // so concurrent runs don't collide on the per-source unique index. Wrap on
// 40_000-cell axes so the result always stays strictly inside the
// OSM-valid ranges enforced by UavTileMetadataValidator (AZ-810):
// lat in [50.0, 70.0), lon in [10.0, 40.0).
var n = Interlocked.Increment(ref _coordinateCounter); var n = Interlocked.Increment(ref _coordinateCounter);
return (60.0 + n * 0.0005, 30.0 + n * 0.0005); var lat = 50.0 + ((uint)n % 40_000u) * 0.0005;
var lon = 10.0 + ((uint)n % 60_000u) * 0.0005;
return (lat, lon);
} }
private static async Task<int> CountUavRowsAsync(string connectionString, double latitude, double longitude) private static async Task<int> CountUavRowsAsync(string connectionString, double latitude, double longitude)
@@ -646,13 +646,20 @@ public static class UavUploadValidationTests
return stream.ToArray(); return stream.ToArray();
} }
// Same coordinate-seeding strategy as UavUploadTests so AZ-810 happy-path // Use a southern-hemisphere range that does NOT overlap UavUploadTests'
// inserts don't collide with the AZ-488 suite when both run back-to-back. // northern range ([50,70) x [10,40)). Non-overlap (not counter offset) is
private static int _coordinateCounter = (int)((DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) % 1_000_000) + 5_000_000; // what guarantees the AZ-488 and AZ-810 suites don't collide on the
// per-source UNIQUE index when both run against the same DB. Wrap on
// 40_000-cell axes so the result always stays strictly inside the
// OSM-valid ranges enforced by UavTileMetadataValidator:
// lat in [-70.0, -50.0), lon in [-40.0, -10.0).
private static int _coordinateCounter = (int)((DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) % 1_000_000);
private static (double Latitude, double Longitude) NextTestCoordinate() private static (double Latitude, double Longitude) NextTestCoordinate()
{ {
var n = Interlocked.Increment(ref _coordinateCounter); var n = Interlocked.Increment(ref _coordinateCounter);
return (60.0 + n * 0.0005, 30.0 + n * 0.0005); var lat = -50.0 - ((uint)n % 40_000u) * 0.0005;
var lon = -10.0 - ((uint)n % 60_000u) * 0.0005;
return (lat, lon);
} }
} }
@@ -22,7 +22,7 @@
| AC-6 | `_docs/02_document/modules/api_program.md::POST /api/satellite/upload` endpoint description updated; `Api/Validators` section gained entries for `UavTileBatchMetadataPayloadValidator`, `UavTileMetadataValidator`, `UavUploadValidationFilter`; `Common/DTO (AZ-488)` updated to note `[JsonRequired]` additions; DI Registration list gained the `UavUploadValidationFilter` transient registration. | | AC-6 | `_docs/02_document/modules/api_program.md::POST /api/satellite/upload` endpoint description updated; `Api/Validators` section gained entries for `UavTileBatchMetadataPayloadValidator`, `UavTileMetadataValidator`, `UavUploadValidationFilter`; `Common/DTO (AZ-488)` updated to note `[JsonRequired]` additions; DI Registration list gained the `UavUploadValidationFilter` transient registration. |
| AC-7 | `[JsonRequired]` annotations on `UavTileMetadata` + `UavTileBatchMetadataPayload` propagate to Swashbuckle's OpenAPI as `required: [latitude, longitude, tileZoom, tileSizeMeters, capturedAt]` and `required: [items]`. Endpoint chain in `Program.cs` declares `.Accepts<UavTileBatchUploadRequest>("multipart/form-data")` + `.Produces<UavTileBatchUploadResponse>(200)` + `.ProducesProblem(400)`. Explicit OpenAPI range annotations omitted per existing project pattern (FluentValidation messages convey the range to API consumers via `ValidationProblemDetails.errors`). | | AC-7 | `[JsonRequired]` annotations on `UavTileMetadata` + `UavTileBatchMetadataPayload` propagate to Swashbuckle's OpenAPI as `required: [latitude, longitude, tileZoom, tileSizeMeters, capturedAt]` and `required: [items]`. Endpoint chain in `Program.cs` declares `.Accepts<UavTileBatchUploadRequest>("multipart/form-data")` + `.Produces<UavTileBatchUploadResponse>(200)` + `.ProducesProblem(400)`. Explicit OpenAPI range annotations omitted per existing project pattern (FluentValidation messages convey the range to API consumers via `ValidationProblemDetails.errors`). |
| AC-8 | Probe script `scripts/probe_upload_validation.sh` — happy + 14 failure modes via `curl`. Reuses `probe_route_validation.sh` structure (JWT mint, status-code assertion, `--exit-on-fail` driver). | | AC-8 | Probe script `scripts/probe_upload_validation.sh` — happy + 14 failure modes via `curl`. Reuses `probe_route_validation.sh` structure (JWT mint, status-code assertion, `--exit-on-fail` driver). |
| AC-9 | No regression in AZ-488: validator rules all align with the legal payloads `UavUploadTests` already sends (lat/lon in range, tileZoom = 18, tileSizeMeters = 200.0, capturedAt = `UtcNow` or recent past, items.Count ∈ [1, 100]). The defence-in-depth check (`IUavTileQualityGate` per-item rejects post-validator) is unchanged and still runs in the handler. Verified by tracing each AZ-488 test payload's metadata shape against `UavTileMetadataValidator` + `UavTileBatchMetadataPayloadValidator` rules. Full integration-test pass gating deferred to autodev Step 11. | | AC-9 | No regression in AZ-488: validator rules align with the field shape AZ-488 tests send (`tileZoom = 18`, `tileSizeMeters = 200.0`, `capturedAt = UtcNow` or recent past, `items.Count ∈ [1, 100]`, no unknown fields). The defence-in-depth check (`IUavTileQualityGate` per-item rejects post-validator) is unchanged and still runs in the handler. **Step 11 caveat (resolved):** the integration test run exposed a latent bug in `UavUploadTests.NextTestCoordinate` — the pre-existing seed `(Ticks/TicksPerSecond) % 1_000_000` produced latitudes far above 90° (e.g. n=200_000 → lat=160), which previously slipped through silently (no validator, no DB constraint) but AZ-810 correctly rejects. Fixed in `UavUploadTests.cs` (clamped to lat ∈ [50,70), lon ∈ [10,40)) and `UavUploadValidationTests.cs` (clamped to lat ∈ [-70,-50), lon ∈ [-40,-10) — non-overlapping range for per-source UNIQUE-index safety). No production code change; AZ-810 validator behaviour unchanged. |
## Code Review Verdict: PASS_WITH_WARNINGS ## Code Review Verdict: PASS_WITH_WARNINGS
+2
View File
@@ -37,6 +37,8 @@ If the enum's wire string happens to match a member name case-insensitively (e.g
## Ring buffer (last 15 entries — newest at top) ## Ring buffer (last 15 entries — newest at top)
- [2026-05-23] [process] When verifying a "no-regression" AC for an input-validation change ("AZ-NNN does not break existing tests"), the only sound evidence is a green integration-test run — tracing fixture variables back to their generators in source is insufficient because helpers can produce values outside the new bounds and previously slipped through silently when no validator existed; document the standard as "verified by reading source" → unconfirmed, "verified by full test run" → confirmed, and gate the batch report's AC table on the latter before the implement skill closes the batch (cycle 8: AZ-810 batch_04 AC-9 claimed "no AZ-488 regression" based on tracing `latitude = coord.Latitude` in test source, but `NextTestCoordinate` seeded by `(Ticks/TicksPerSecond) % 1_000_000` produced lat far above 90° at runtime; the false-PASS only surfaced at autodev Step 11 when the integration test run returned HTTP 400 from the new validator on the AZ-488 happy path).
Source: _docs/03_implementation/batch_04_cycle8_report.md (AC-9 row)
- [2026-05-22] [process] When the implement skill ships a cycle's batch commit without writing `_docs/03_implementation/implementation_report_*_cycle{N}.md`, downstream skills (test-spec cycle-update, document task mode, retrospective Step 1) must fall back to reading the cycle's task specs in `_docs/02_tasks/done/` plus the commit body via `git log --grep='[AZ-...]'` — codify the fallback in those skills' instructions instead of leaving it as per-cycle improvisation, because the implicit contract between Step 10 and Steps 11-17 broke silently this cycle and only succeeded because every downstream skill happened to be robust enough to substitute (cycle 7: AZ-794+AZ-795+AZ-796 shipped as commit `865dfdb` with no report artifact; doc-skill auto-walked the diff, test-spec read the task specs, retrospective wrote from the deploy + security + perf reports — all worked, but the contract was never formal). - [2026-05-22] [process] When the implement skill ships a cycle's batch commit without writing `_docs/03_implementation/implementation_report_*_cycle{N}.md`, downstream skills (test-spec cycle-update, document task mode, retrospective Step 1) must fall back to reading the cycle's task specs in `_docs/02_tasks/done/` plus the commit body via `git log --grep='[AZ-...]'` — codify the fallback in those skills' instructions instead of leaving it as per-cycle improvisation, because the implicit contract between Step 10 and Steps 11-17 broke silently this cycle and only succeeded because every downstream skill happened to be robust enough to substitute (cycle 7: AZ-794+AZ-795+AZ-796 shipped as commit `865dfdb` with no report artifact; doc-skill auto-walked the diff, test-spec read the task specs, retrospective wrote from the deploy + security + perf reports — all worked, but the contract was never formal).
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-22] [testing] When a strict-validation layer ships (`JsonSerializerOptions.UnmappedMemberHandling.Disallow`, FluentValidation rules, explicit DTO `[JsonRequired]`), expect the project's own integration tests to surface latent bugs the prior lenient defaults had been masking — silent PascalCase fallback property names, out-of-range fixture coordinates, wrong-cased JSON keys; correct them in the same PR or the test suite goes red and the strict layer looks like a regression instead of the bug-finder it is (cycle 7: `IdempotentPostTests.RoutePoint` had been posting `{"Lat":...}` against a `[JsonPropertyName("lat")]` DTO for months; the new strict deserializer caught it and the 2-line payload fix landed alongside the strict layer). - [2026-05-22] [testing] When a strict-validation layer ships (`JsonSerializerOptions.UnmappedMemberHandling.Disallow`, FluentValidation rules, explicit DTO `[JsonRequired]`), expect the project's own integration tests to surface latent bugs the prior lenient defaults had been masking — silent PascalCase fallback property names, out-of-range fixture coordinates, wrong-cased JSON keys; correct them in the same PR or the test suite goes red and the strict layer looks like a regression instead of the bug-finder it is (cycle 7: `IdempotentPostTests.RoutePoint` had been posting `{"Lat":...}` against a `[JsonPropertyName("lat")]` DTO for months; the new strict deserializer caught it and the 2-line payload fix landed alongside the strict layer).
+5 -5
View File
@@ -4,12 +4,12 @@
flow: existing-code flow: existing-code
step: 11 step: 11
name: Run Tests name: Run Tests
status: not_started status: in_progress
sub_step: sub_step:
phase: 0 phase: 2
name: awaiting-invocation name: run-tests
detail: "" detail: "re-run after coord-clamp fix"
retry_count: 0 retry_count: 1
cycle: 8 cycle: 8
tracker: jira tracker: jira
auto_push: true auto_push: true