Cycle-2 retrospective covering AZ-487 + AZ-488. Captures six patterns (duplicate JWT helpers diverged then both broke; pre-existing test bugs unmasked by downstream test pressure; cycle 1 perf-NFR action stopped adding scenarios but did not drain backlog; doc-path F1 carried over twice with no decision; integration test DB isolation = wallclock workaround; 8 SP friction observable even with user override). Top-3 improvement actions: consolidate JWT mint helpers, promote PT-07/PT-08/JWT-attach to real PBI, real integration DB-reset hook. LESSONS.md ring buffer now holds 6 entries (testing x3, process x2, estimation x1). Structural snapshot: 6 components / 12 PR edges unchanged; contract coverage 14% -> 29%; new external NuGet edges (JwtBearer 8.0.21 + ImageSharp 3.1.11) tied to cycle-2 security findings. Autodev pointer advances to cycle 3 / Step 9 New Task. Co-authored-by: Cursor <cursoragent@cursor.com>
4.9 KiB
Engineering Lessons
Recurring bugs, surprising library behaviors, and process insights extracted from completed cycles. Newest at the top. Keep entries short — this is for fast scanning at the start of new cycles, not exhaustive history.
This file has two layers:
- Deep engineering lessons (L-NNN): library bugs, architectural insights, multi-paragraph context. Persist forever.
- Ring buffer at the bottom: most recent 15 single-sentence lessons emitted by the retrospective skill, consumed by
new-task/plan/decompose/autodevStep 0. Oldest entries drop off the top.
Categories: estimation · architecture · testing · dependencies · tooling · process
L-001 — Dapper TypeHandler<T> is bypassed for enum types during read deserialization
Cycle: 1 (AZ-484)
Discovered by: integration test failure (Error parsing column 12 (source=google_maps - String)); root-caused via web search to long-standing Dapper issue #259.
Affects: Dapper 2.1.35 (and most other versions until the proposed Settings.PreferTypeHandlersForEnums opt-in in PR #2200, not yet merged).
What happens
Registering SqlMapper.AddTypeHandler(new MyEnumHandler()) for an enum type — even via SqlMapper.TypeHandler<TEnum> — works for writes (the handler's SetValue is invoked for parameter binding) but is silently bypassed for reads. Dapper's IL-emitted deserializer checks IsEnum first and falls back to Enum.TryParse(string, ignoreCase: true).
Why this is dangerous
If the enum's wire string happens to match a member name case-insensitively (e.g., RegionStatus.Failed ↔ "failed"), the bypass goes unnoticed and round-trip works accidentally. The bug only surfaces when the wire format diverges from the C# member name (e.g., TileSource.GoogleMaps ↔ "google_maps" — Enum.TryParse("google_maps") does not match GoogleMaps because of the underscore).
Recommended approach
- Do not rely on
SqlMapper.TypeHandler<TEnum>for read-side enum mapping unless the wire values match the enum member names case-insensitively. - For enums whose wire format diverges (snake_case, kebab-case, custom IDs), store the entity field as
stringand provide an explicit converter (*Converter.ToWireValue/FromWireValue) for use at the service-layer boundary. This is what AZ-484 does forTileEntity.Source↔TileSourceConverter. - Unit-test the converter directly. Do not assume that round-tripping through Dapper proves anything for enums.
Detection
- Unit tests of the type handler in isolation will pass even when the handler is bypassed at runtime.
- Failure surfaces only at integration-test time when the actual SELECT runs.
- If you must keep an enum-typed field, write at minimum one integration test that reads the enum back through Dapper from a real database row.
Ring buffer (last 15 entries — newest at top)
- [2026-05-11] [testing] Test helpers shared across unit + integration projects must live in one consolidated location — duplicate near-identical copies will diverge and require parallel fixes (cycle 2:
JwtTokenFactory.csandJwtTestHelpers.cshad the sameExpires < NotBeforebug fixed in separate commits). Source: _docs/06_metrics/retro_2026-05-11_cycle2.md - [2026-05-11] [process] Deferred-status NFR entries are allowed at most ONCE per NFR — if a Deferred NFR has not landed by the end of the cycle that follows the one in which it was deferred, the harness work must be promoted to a real PBI before any new NFR is accepted as Deferred (cycle 2 inherited cycle 1's PT-07 + added PT-08 + JWT-attach script-rot). Source: _docs/06_metrics/retro_2026-05-11_cycle2.md
- [2026-05-11] [testing] Integration tests must explicitly reset DB state at startup — relying on wallclock seeds or "tests probably won't collide" is a workaround, not isolation; the persistent Postgres volume in docker-compose makes test data accumulation the default state (cycle 2:
UavUploadTests._coordinateCountercollision was patched with a wallclock seed instead of a real DB-reset hook). Source: _docs/06_metrics/retro_2026-05-11_cycle2.md - [2026-05-11] [testing] Persisted enums need a Dapper read-roundtrip integration test — unit-testing the type handler in isolation does not prove read-side behavior (see L-001). Source: _docs/06_metrics/retro_2026-05-11.md
- [2026-05-11] [process] NFR test-spec additions must include the runner-script implementation in the same step, or be tagged "Deferred — harness work tracked in "; otherwise scenarios accumulate as Unverified across cycles. Source: _docs/06_metrics/retro_2026-05-11.md
- [2026-05-11] [estimation] Task-spec test-site-count estimates must be backed by an explicit grep evidence block, not pattern-matched against neighboring code (AZ-484 spec said ~3 sites in
RegionServiceTests; actual = 0). Source: _docs/06_metrics/retro_2026-05-11.md