Office-hours revised2026-05-22

Test Coverage Plan — Path to 80%

Owner: Gercek (CEO) · Linear RAK-355 (this plan) · RAK-339 (umbrella) · Source data: master sha 3546eece, 2026-05-22

Office-hours outcome (2026-05-22)

The CEO set a target of 80% line coverage. We're at 1.5%. Closing that gap on a 9-year-old codebase with ~186k coverable lines, no mocking framework, and ~200 controllers that new RakletDb() inline is a multi-quarter initiative — not a sprint. This artifact lays out the cost honestly so the CEO can pick a target curve before any test gets written.

1. Where we actually are

Headline number

1.5% line coverage today

2,906 of 186,449 lines · 383 MSTest tests · 31 assemblies in report

Coverage on code we own

~25 third-party NuGet assemblies (AWSSDK, Intuit.Ipp, etc.) inflate the headline denominator by 5-10×. Real picture below.

Models
6.7%
Services
2.6%
Application
0.3%
Raklet.Webjob.Common
0%
Raklet.WebFramework
0%
Raklet.Money / .Sms / .Email
0%
What the 383 existing tests do: Pure POCO builder pattern. Zero mocking framework. All synchronous. Direct instantiation of services with hand-built input objects. Zero RakletDb usage. Fine for pure-math service helpers — does not extend to controllers, DB-bound services, async pipelines, or HTTP boundary logic.

2. The math of 80%

Target Lines covered Net new lines Approx new tests
@ 3-5 lines/test
Today2,906
10%18,645+15,7393,000-5,000
25%46,612+43,7069,000-15,000
50%93,225+90,31918,000-30,000
80%149,159+146,25330,000-50,000
At an optimistic 30 tests/engineer/day (assumes infrastructure exists and controllers are already refactored), 30,000–50,000 new tests is 1,000–1,700 engineer-days. One engineer full-time: 4–7 years. Three engineers full-time: 1.3–2.3 years.

3. What blocks us today

3.1 No mocking framework

packages.config has only MSTest + EF. No Moq, NSubstitute, FakeItEasy. Decision: pick one and adopt.

3.2 No in-memory DbContext for EF6

EF Core's UseInMemoryDatabase doesn't exist for EF6. Options: Effort.EF6 (unmaintained), hand-rolled IDbSet<T> fakes (recommended), or real LocalDB (slow).

3.3 Controllers do new RakletDb() inline

Top 5 controllers (AccountController is 1,251 LOC) instantiate services directly. Not testable as-is — need constructor-injection refactor first.

3.4 No async test patterns

All 383 existing tests are sync. Controllers are async-heavy. Need one-time pattern decision before async controller tests can land.

3.5 No ControllerTestBase

Every controller test needs stubbed ControllerContext + mock HttpContextBase + principal + route data. Without a base class, each test re-implements this.

3.6 Tooling already works

Coverlet measurement landed in #13815. Permission fix in #13833. Per-PR coverage summaries already appearing on Tests workflow runs. Foundation is laid.

4. Phased roadmap

01 day · 1 eng

Cleanup & honest baseline

Add NuGet excludes to coverlet. Honest headline becomes ~3-5%. Trend output. Re-tag stragglers.

End state: real baseline visible · No new tests written
13-5 days · 1 eng

Foundation

Add Moq. FakeDbSet<T> + RakletDbFake + ControllerTestBase + MockHttpContextBuilder. Async test pattern documented. PaymentsController as canonical reference.

End state: other engineers + AI agents can copy the pattern
23-5 weeks
1-2 eng

Tier 1 controllers

Top 5 by traffic: AccountController (1,251 LOC, 5-8d), EventsController (3-4d), ContactController (2-3d), SubscriptionController (2-3d), PaymentsController (done in phase 1).

End state: Raklet.Api assembly 8-15% coverage · Application still ~0.3%
31 day · 1 eng

Coverage trend gate

Once phase 2 has 2+ weeks of data, CI check fails PRs that drop coverage by >0.5%. Forces phase 4 not to regress.

End state: regression-prevention mechanism in place
43-6 months
2-3 eng

Tier 2 fan-out (~50 controllers)

Next 25-50 highest-traffic controllers across Raklet.Api + Application. Same recipe: refactor for injection, write happy + auth + edge tests.

End state: combined coverage in 20-35% range — realistic mid target
53-8 months
1-2 eng

Service layer

~149 service files at 2.6%. DB-and-external-API-bound (payment processors, email senders, Cosmos). Same patterns + interface stubs for HTTP clients.

End state: another +15-25% on coverage
612+ months
multi eng

Long tail to 80%

Models methods, Raklet.WebFramework, Webjob.Common, Queues, utility libs, edge controllers. Diminishing returns kick in hard. Marginal value of test #30,000 ≪ test #5,000.

End state: 80% — if it still makes sense by then
73-5 days · 1 eng
(parallel)

Backend TypeScript

Raklet.Backend has 611 .ts files with 4 Jest test files. Smaller scope, separate stack. Can run in parallel with phase 1+ on a different engineer.

End state: TS coverage measurable & growing

5. Cost model

Phase Engineer-days Calendar (1 eng) Calendar (3 eng) Coverage at end
0. Honest baseline11d1d~3-5%
1. Foundation3-51w3-5d~5-6%
2. Tier 1 (top 5)15-253-5w1-2w~10-15%
3. Trend gate11d1d(mechanism)
4. Tier 2 (~50 controllers)100-2505-12mo2-4mo~25-40%
5. Service layer50-1503-8mo5-12w+15-25%
6. Long tail to 80%200-5001-2y5-12mo80%
7. TS backend (parallel)3-51wseparate stack
Total to 80%: roughly 18-30 months with 1 engineer full-time, or 6-12 months with 3 engineers full-time. Plus the opportunity cost of those engineers not shipping product.

6. Office-hours outcome (2026-05-22)

Q1 — Demand reality: RESOLVED

Not external audit/sale/cert. Real driver is shipping confidence for AI-written PRs. Coverage % is downstream of this goal.

Q3 — Data correction

CEO challenged the "high-traffic controllers" claim — correctly. Names were picked by file size and pattern-matching, not real telemetry. Access incoming for App Insights, GA, DB query patterns; chunks 2-N re-rank when data lands.

Q4 — Wedge: RESOLVED

PaymentsController (83 LOC, 1 method, thin orchestration over OrganisationMembershipService). Foundation scaffolding + first test pack in one PR, sized 5-7 days.

Q5 — Observation: none yet

No prior data on codex/cursor writing tests against Raklet specifically. Therefore wedge PR is intentionally a learning experiment. Its "What we learned" section is the highest-value artifact of the whole initiative.

Premise 1 challenged → resolved

Old plan: optimize for line coverage %. Revised: chunk by hot-path priority. Coverage % stays as a tracked metric but isn't the optimization target. Source of priority: real traffic data, not file-size guessing.

Chunking approach: locked (Approach B)

One controller per chunk. Linear card → branch → PR → AI agent. Codified in test-coverage-task-template.md (this PR). AccountController (1,251 LOC) gets sub-chunked. Chunk 0 (Foundation + PaymentsController wedge) ships first, blocking all others until the canonical pattern is proven.

7. Original decision points (kept for reference)

6.1 Is 80% the right target?

80% is optimized for greenfield codebases. Brownfield codebases of this vintage typically see high ROI at 40-60% with diminishing returns above that.

Ask: is 80% driven by a specific need (audit, sale, certification) or aspirational? The plan changes meaningfully.

6.2 What's the time budget?

1 engineer full-time = phases 0-3 in ~2 months, then 4 rolling 6-12 months. 3 engineers for 6 months = phases 0-5 done (40-60% coverage). Whole team for a quarter = feasible but costs product velocity.

Ask: what headcount can be carved out, and against what other priorities?

6.3 Refactor for testability, or not?

Phase 2's main cost is refactoring controllers from new RakletDb() inline to constructor injection. Yes-refactor makes future tests cheap but costs 5-10d per top-tier controller. No-refactor needs heavier per-test fixtures.

Ask: are we willing to touch production code paths for testability? Risk vs long-term cost.

6.4 AI-generated tests?

Three positions: no (all handwritten), yes-with-review (3-5× throughput, review risk), yes-autonomous (highest throughput, "tests that always pass" risk). CEO's stated AI-adoption goals point toward yes — but as a deliberate choice.

Ask: which mode, and what's the sample-review discipline to catch slop?

6.5 What gets deprioritized to make room?

The hard question. 18-30 engineer-months don't exist unless something else gets deprioritized. Office-hours should pin down what — features, ops, sales engineering, customer support engineering?

Ask: pick a specific roadmap item to push out by N months to fund this initiative.

7. Open risks

8. What happens next

  1. Now: CEO grants access to App Insights, GA, and DB query patterns (per Q3 above).
  2. Now (parallel): Open Linear sub-issue for Chunk 0 (Foundation + PaymentsController wedge). Dispatch to codex OR cursor with test-coverage-task-template.md as the brief.
  3. Now (parallel): Open Linear sub-issue for Chunk 1 (move coverage to nightly + per-PR delta). Small change, independent of Chunk 0.
  4. When data lands: Re-rank chunks 2-N by (hourly hits × severity-if-broken). Create Linear cards for the top 5. Don't pre-create the rest.
  5. After Chunk 0 lands: Read its "What we learned" section. Decide if AI tests are net-positive. Continue chunks 2-N or revisit the whole initiative.
  6. Re-assess after 5 chunks land. If AI tests are net-negative on engineer time, pause and re-think.
Generated by an AI agent (Claude) against master sha 3546eece on 2026-05-22. Coverage numbers from coverlet.console output uploaded as the dotnet-coverage artifact on Tests workflow run 26253865435. No tests have been written. This is the plan, not the work.