The runtime fix is coherent and the 101-file migration is complete for test sources. Fresh containers preserve test isolation, process-lifetime retention protects contexts that escape setup helpers, and the regression passes. The PR should not be pushed yet because its advertised recurrence guard does not cover several realistic unsafe factory forms; one active agent note also directs contributors to a removed helper.
Scope reviewed
Fetched origin/main
Reviewed git diff origin/main...HEAD
101 files: 627 additions, 293 deletions
No production Swift source changes
Verdict basis
Migration completeness: pass
Container ownership: pass
Retention tradeoff: acceptable and explicit
Lint guard coverage: must fix
Regression tests: pass, with scope note
Findings
P2 · Must fix
.swiftlint.yml:13-17 — the guard misses unsafe factories that can recreate T-2003
The error-level custom rule matches only a throwing function whose entire return type is exactly ModelContext, plus calls to the removed newContext(). A direct probe showed:
Factory shape
SwiftLint result
func f() throws -> ModelContext
Rejected
func f() -> ModelContext (for example using try!)
Accepted
func f() throws -> (Service, ModelContext)
Accepted
func f() throws -> Env where Env stores only a context/service
Accepted
The tuple/setup exemptions are safe in the migrated code only because those implementations call TestModelContainer(), whose static registry retains the container. The rule cannot enforce that premise. A future helper can call the public, explicitly unregistered TestModelContainer.newContainer() (or construct ModelContainer directly), return a tuple/setup object without its owner, pass lint, and reintroduce the same crash.
Fix direction: enforce ownership rather than one signature spelling. Options include restricting raw container/context construction to an allowlisted fixture file, making the unregistered escape hatch harder to misuse, adding a broader source guard for context-bearing factory returns, and adding rule-fixture tests that prove both accepted and rejected shapes.
P3 · Documentation
docs/agent-notes/reports.md:77 still recommends the removed makeReportTestContext()
The migration replaces that helper with makeReportTestContainer(), but the active reports agent note still says GenerateReportIntentTests uses makeReportTestContext(). This undermines the new ownership guidance and will send future changes toward a nonexistent, unsafe API shape. Update it as part of this migration. Historical changelog/spec references do not need rewriting.
Audit details
Migration completeness
All TestModelContainer.newContext() and makeReportTestContext() references are gone from Transit/TransitTests.
Representative direct tests now hold a local TestModelContainer; report tests return the owning fixture; ProjectEntityTests and TaskEntityQueryTests store the fixture in their environment structs.
The one current TestModelContainer.newContainer() caller, MilestoneCrossDeviceUniquenessTests.Environment, stores the returned ModelContainer and therefore preserves ownership.
Two direct temporary extractions (BoolAsIntIdRejectionTests and ProjectServiceTests) rely on the documented static retention backstop. They are lifetime-safe, though they demonstrate why the central registry is semantically required.
SwiftData ownership and retention tradeoff
TestModelContainer owns both ModelContainer and ModelContext, creates a unique in-memory configuration, disables CloudKit, and registers every fixture-created container on the main actor.
The static registry intentionally keeps all fixture stores—and their persisted test objects—alive until the test process exits. This trades peak test-host memory for deterministic lifetime safety. The suite completed successfully under this policy, so the current cost is operationally acceptable.
The registry is not merely defensive: many setup helpers return services/contexts rather than the owning fixture, and Swift may release an otherwise unused local after its last use. Removing or clearing the registry without migrating those APIs would reopen the defect.
newContainer() is deliberately unregistered for multi-context tests. Its ownership contract is documented and correctly followed by the sole current caller, but it is the main future misuse surface and should be reflected in guard coverage.
Regression tests
TestModelContainerLifetimeTests.escapedContextKeepsBackingContainerAvailableForItsFullUse creates a fixture in a helper, returns only its context and a weak container reference, then proves the container remains alive and model insertion/access works.
This accurately locks in the central-retention behavior. It does not measure memory growth or test the unregistered newContainer() contract; those are design/guard concerns rather than failures of the lifetime regression.
All unit tests completed; six UI tests failed while Xcode repeatedly emitted DebuggerLLDB.DebuggerVersionStore.StoreError / no debugger version. Full log: /tmp/transit-pr192-make-test-20260801-205150.log; xcresult: DerivedData/Logs/Test/Test-Transit-2026.08.01_20-51-56-+1000.xcresult.
Static migration searches
Pass
No legacy helper calls in test sources; sole raw fixture escape hatch retains its container.
The UI failures are not attributed to this PR: the merge-base diff changes only test-target sources, lint/docs/changelog/spec files, while the failures occur in the separate UI-test target and coincide with repeated debugger-version launch errors. They remain recorded because the documented pre-push suite did not return success.
Recommended push gate
Strengthen the prevention mechanism and add executable rule fixtures for bypass shapes.
Update docs/agent-notes/reports.md to the owning-container helper.
Re-run make lint, the lifetime test/macOS suite, and the full iOS suite when the simulator/debugger environment is healthy.