Repository navigation
test(export): pin that a full export reads inside one snapshot - #1025
Conversation
|
Review of record: Codex Review of PR #1025Reviewed head FindingP2 | CONFIRMED | The test claims to protect every dataset but exercises only Concrete failure scenario: change only Evidence: I changed only Either extend coverage to reject individual dataset bypasses, or narrow the method name, comment and PR claim to the facade's snapshot lifecycle using expense categories, explicitly documenting the untested branches. Merely enumerating the other tables without asserting their snapshot behavior would not close this gap. This is a test coverage/claim defect, not a product defect in the reviewed head. All current dataset branches use Mutation evidenceEach mutation ran separately against the new test with a real PostgreSQL database. Each was reverted afterward. All six red results below failed at line 308 with
The exact mutations and runner are retained in 1025-mutations.py. Determinism and isolationNo timing defect found. The first Each test creates a fresh account and unique row IDs. The reader explicitly resolves that account; each writer uses a fresh tenant-resolved scope. The integration collection serializes its tests, and the test modifies no shared factory configuration. The final read confirms both the mid-snapshot and post-disposal inserts become visible. It does not assert tenant isolation, but no account leakage or shared-state dependency was found in this change. Endpoint gap and recommendationThe documented gap is real at Recommend a small test-only I built and ran this approach as a throwaway prototype:
This uses no production hook, source-text assertion, timing dependency, or concurrent insert. It complements the real database visibility test. It does not by itself prove each query branch uses the snapshot context, so the P2 finding still needs its stated resolution. The prototype was removed from the checkout and retained only beside this report. Style and final validationThe added usings are outside the namespace and the file retains its file-scoped namespace, satisfying #985. The test follows the file's factory helpers and domain qualification conventions. No style issue found. Baseline new test: 1/1 passed. After restoring every mutation and removing the probe, all 10 NitsNone. Verdict: REQUEST CHANGES to narrow or substantiate the “every dataset” claim; no product defect found. |
|
Response to Astra round 1 (review of 1. P2: only one dataset was covered. Accepted.
2. Endpoint gap. I adopted your recorder approach as
Verification:
|
|
Review of record: Codex PR #1025, round 2Reviewed fix-only changes at Both round-one mutations now fail for the intended reason. Two further coverage defects remain. Neither finding is a product defect in this head. They are false negatives in the new guards; the product defects described below exist only under the reverted mutations. FindingsP2 | CONFIRMED |
|
| Mutation | Result and reason |
|---|---|
Customers uses requestDb.Customers |
RED: "customers" reads through [requestDb], not only activeDb |
Customers uses this.db.Customers, with an explicit context field |
RED: identifies db |
Ternary chooses between activeDb and requestDb |
RED: identifies both contexts |
Request field renamed to fallbackDb, then used by customers |
RED: identifies the renamed context |
| Local shadow routes only customers to request context | GREEN, finding 1 |
| Helper ignores the supplied active-context query | GREEN, finding 1 |
| Ternary uses a fully qualified context field | GREEN, finding 1 |
| Delete endpoint begin call | RED: expected begin event is absent |
| Begin twice | RED: extra begin/end events |
| Dispose while the final dataset is enumerating | RED: download fails because the underlying reader is closed |
| Construct dataset queries before begin | GREEN, finding 2 |
Replace await using with disposal only on successful completion |
GREEN on submitted tests; exception limitation below |
The last-dataset mutation disposes the snapshot after the audit-event enumerator yields a row and before it finishes, rather than merely closing it before the loop. Its failure is a real reader-lifetime failure, not compilation or setup failure.
Exact mutations and logs are retained beside this report: source runner, endpoint runner, behavioral probes.
Exception path and determinism
The submitted endpoint test exercises successful completion only. Moving disposal onto the success path leaves all 16 relevant tests green. A temporary probe injected an exception during audit-event enumeration and checked that disposal still occurred. The actual endpoint passed; success-only disposal failed, with the last event rows:audit-events instead of end. This is a measured coverage limit, not an additional blocking finding or an existing cleanup defect. An exception-path test would be a small follow-up if cleanup is part of the promised guarantee.
No flakiness found in the submitted tests. Each endpoint test uses a fresh account, host, and event list. The producer records events sequentially, and the endpoint awaits the producer before completing the response callback. The client fully consumes the response before reading the list. No global recorder, timer, row-order assumption, or cross-test account dependency was introduced. Empty datasets still record enumeration start and completion. The wrapper forwards cancellation and awaits the real begin/disposal operations.
The decorator explicitly constructs ExportQueries, so it does not verify the production IExportQueries registration. The separate facade test still resolves production DI. The confirmed ordering gap is the unrecorded query-construction step in finding 2.
Nits and false positives
P3 | CONFIRMED | tests/Cluckwork.Application.Tests/Architecture/ExportSnapshotSourceTests.cs:64
SequenceEqual rejects a harmless switch-arm reorder even though archive ordering comes from DatasetNames.
Concrete scenario: move the unchanged customers arm immediately before the fallback arm. Every mapping and query stays the same, but the source Fact fails because its arm sequence differs from the registry. Mutation evidence. Compare membership and uniqueness if the intended invariant is complete dataset coverage, rather than requiring incidental source order. This is not a product defect.
A consistent rename of the private activeDb field also fails the explicit name assertion. That is visible name coupling, not a runtime regression; document it if intentionally retained.
Validation and checkout state
The unchanged baseline and final restored-source runs both passed all 5 source tests and 11 export tests. See final source results and final export results. The changed files retain outside-namespace usings and file-scoped namespaces; builds run with the repository's warnings-as-errors and #985 style gate. No style defect found. Full solution and full integration-suite runs were not repeated.
Every mutation and temporary probe was reverted; git status --short and git diff --exit-code confirmed a clean checkout at the reviewed head. No commit, push, GitHub comment, or change to another checkout was made.
Verdict: REQUEST CHANGES for the two confirmed guard bypasses; no product defect found in the reviewed head.
|
Response to Astra round 2 (review of 1. P2: the source guard treated the identifier
2. P2: the recorder did not check when
Verification on the final head:
The review loop was stopped deliberately by the owner after two consecutive rounds that found no product defect (rounds 1 and 2 found only test gaps). No round 3 will be triggered. |
Why
WriteZipAsyncopensBeginConsistentReadAsynconce, then reads all 20 datasets through the sameIInsightsModule.ExportQueriespoints every read at a separate, non-retrying context inside aRepeatableReadtransaction (#269), so every CSV sees the same instant. Before this PR no test checked that.TransientDbResilienceTests.Export_ExplicitRepeatableReadTransaction_StillWorksUnderTheRetryStrategychecks only that the call does not throw.Three tests now cover it. Each test proves one link, and the table states only what that test proves.
ExportTests.ConsistentRead_HidesAWriteCommittedInsideTheSnapshot(Integration, real Postgres)expense-categories, throughIInsightsModulefrom real DI.ExportTests.FullBackup_BuildsAndEnumeratesEveryDatasetInsideTheSnapshot(Integration, HTTP)GET /api/v1/export/allopens exactly one snapshot. For every dataset, theGetDatasetcall (which binds the query to the active context) and the row enumeration (which runs the SQL) both happen after the snapshot opens and before it closes. A test-onlyIExportQueriesrecorder wraps the realExportQueriesbehind the real facade, registered viaConfigureTestServices.ExportSnapshotSourceTests.EveryDatasetArm_NamesTheSnapshotFieldDirectly(Application, Roslyn syntax)GetDatasetarm names theactiveDbfield directly, names no otherAppDbContextfield or primary-constructor parameter, and the set of arms equalsDatasetNamesin any order. A Theory applies four mutations to the real source, so the guard is shown to fail.Limit of the source test. It is a syntax check of a naming convention and does not resolve identifiers. Indirection passes it: a local that shadows
activeDb, a helper that ignores its argument, or a ternary. Code review and the two behavioural tests are what cover those cases. RenamingactiveDbmakes the test fail, which is intentional.Test only, no call graph change.
Mutation evidence
Each mutation was applied locally, run against the named test, and reverted. The unmutated source is GREEN for every test.
InsightsModule.BeginConsistentReadAsyncreturns a no-op disposableConsistentRead_…Assert.DoesNotContain() Failure: Item found in setExportQueriesopensReadCommittedinstead ofRepeatableReadConsistentRead_…customersarm readsrequestDb.CustomersExportSnapshotSourceTests"customers" reads through [requestDb], not only activeDbcustomersarm moved to the end of the switch (round 2 P3: a harmless reorder)ExportSnapshotSourceTestsBeginConsistentReadAsyncline fromWriteZipAsyncFullBackup_Builds…query:flocks, notbeginWriteZipAsyncdisposes the snapshot before the dataset loopFullBackup_Builds…rows-end:audit-events, notendWriteZipAsyncbuilds everyGetDatasetquery into a dictionary before opening the snapshot, then enumerates them inside it (round 2 prefetch)FullBackup_Builds…query:flocks, notbeginIn CI, the Theory in
ExportSnapshotSourceTestsalso runsrequestDb.Customers,db.Customers,this.requestDb.AuditEvents, and a renamed arm label.Test counts (
dotnet test --list-tests)6e3f5845origin/mainhas moved on since6e3f5845. None of these files changed upstream:src/Cluckwork.Infrastructure/Insights,src/Cluckwork.Api/Endpoints/Export,src/Cluckwork.Application/Features/Export,ExportTests.cs.Local runs (final head)
dotnet build Cluckwork.slnreports 0 warnings and 0 errors. Application passed 644/644; that total counts each Theory case, which--list-testsdoes not. Integration passed 1875/1875.pstack:deslopfound nothing to remove in any round.