Skip to content

refactor: safe Tier 0+1 — backend partial-class splits + util tests#248

Merged
mrviduus merged 2 commits into
mainfrom
refactor/safe-tier0-tier1
May 24, 2026
Merged

refactor: safe Tier 0+1 — backend partial-class splits + util tests#248
mrviduus merged 2 commits into
mainfrom
refactor/safe-tier0-tier1

Conversation

@mrviduus
Copy link
Copy Markdown
Owner

@mrviduus mrviduus commented May 24, 2026

Summary

Cosmetic refactor pass with zero behaviour change. Three god-class backend files split across C# partial files (compile-identical IL — integration tests + EF model snapshot verify), plus net-new unit-test coverage on critical web + mobile pure utilities. The 1559-LOC VocabularyEndpoints.cs was the largest non-migration backend file pre-PR; now down to 875 LOC. (New largest is ReadingTrackingEndpoints.cs at 920 LOC — not in scope for this PR.)

This is the safe portion of the broader refactor plan (Tier 0 tests + Tier 1 cosmetic splits). Higher-risk items (god-component splits in web, mobile reader factory hook, AI-driven naming) are deferred to post-Play-Store-launch with telemetry data.

Changes

Backend cosmetic splits (zero behaviour change — partial class compiles identically)

File Before After Partials created
AppDbContext.cs 655 104 7 (Catalog/User/Reading/UserBooks/Vocabulary/Ops/Seo/Collections)
VocabularyEndpoints.cs 1559 875 6 (Stats/Settings/Pending/Lookups/Clusters/Admin)
AdminService.cs 977 131 4 (Upload/Editions/Chapters/UserUploads)

Each partial is under 400 LOC. Single domain per file. Shared helpers (e.g. TryGetAuth, EnqueueSsgSafe, MapVocabularyEndpoints) stay in the main file.

Parity verified:

  • AppDbContext: all 50 entity configurations present pre + post split
  • VocabularyEndpoints: all 32 methods (handlers + helpers) present, 25 routes unchanged in MapVocabularyEndpoints
  • AdminService: all 27 methods present
  • dotnet ef migrations has-pending-model-changes"No changes have been made to the model since the last migration" — EF model snapshot byte-identical, no migration churn

Tests added (Tier 0 — additive, no behaviour change)

Web (existing Vitest infra):

  • analytics.test.ts — 20 cases / 30 assertions: GA4 event shape contract, gtag fallback, PII boundaries, all 10 wrappers
  • dataEvents.test.ts — 10 cases / 13 assertions: CustomEvent bus, useDataChange hook, multi-entity, unmount-leak
  • errorUtils.test.ts — 5 cases / 12 assertions: HttpError 404 detection, defensive boundary
  • formatTime.test.ts — 8 cases / 15 assertions: hour/minute formatting, edge cases

Mobile (NEW Vitest infra):

  • vitest.config.ts — Node env, src/lib/**/*.test.ts include, AsyncStorage alias to in-process mock
  • __mocks__/async-storage.ts — in-memory map mirroring the RN module's API surface with __reset() for clean state
  • searchUtils.test.ts — 18 cases / 24 assertions: normalization, query matching, documents surprising Cyrillic йи NFD stripping as intended cross-script search
  • features.test.ts — 9 cases / 11 assertions: reader-overlay v2 killswitch cascade
  • vocabStatsCache.test.ts — 11 cases / 15 assertions: round-trip, TTL, corrupt-JSON defense, schema-drift rejection

Tests

  • Unit (backend): 216 + 313 + 20 = 549 passed (unchanged — splits are IL-identical)
  • Unit (web): 474 passed (40 net new)
  • Unit (mobile): 38 passed (new — mobile had 0 unit tests pre-PR)
  • tsc: clean on web + mobile + backend
  • Build: dotnet build textstack.sln → 0 warnings, 0 errors
  • EF model: dotnet ef migrations has-pending-model-changes confirms no schema drift

Rollback plan

Single revert — git revert <merge-sha>. No DB migrations, no API changes, no behaviour changes. New test files are additive (revert removes them; no consumer code depends on them).

Self-review findings

  • pnpm-lock.yaml accidentally committed (ce944cc) — initially used pnpm add for mobile but mobile uses npm (has package-lock.json tracked). Lock files would have drifted on CI / fresh installs. Caught + fixed in self-review pass: removed pnpm-lock.yaml, ran npm install to update package-lock.json with the new vitest deps. Mobile tests still pass.
  • Over-inclusive usings in AdminService partials: all 4 partials import the full superset of 13 usings even though Chapters/UserUploads only need 3-4 each. C# compiler doesn't flag unused usings (no IDE0005 analyzer enabled). Acceptable trade-off — easier to add code without re-validating usings per file. Not worth churn to fix.

Notes

  • Why partial classes, not IEntityTypeConfiguration<T> extraction for EF: chose partial class AppDbContext over per-entity config classes to minimise diff risk and keep the existing inline-config pattern (used everywhere in the codebase). EF model snapshot is byte-identical to pre-split — no migration churn. Per-entity config extraction is a separate refactor if the pattern proves useful.
  • Why public static partial class for endpoints: ASP.NET Core's WebApplication.MapPost/Get/... works fine with handlers declared across partials of a static class. The only constraint is MapVocabularyEndpoints (which registers routes) and the static helpers (TryGetAuth, ToDto, …) must be visible to all handler partials — which is automatic.
  • Mobile Vitest scope is intentionally narrow: only src/lib/**/*.test.ts. Hooks and components are NOT in scope — they need a full @testing-library/react-native + native-module mock setup that's a much bigger infra slice. When a real regression demands it, add it then.
  • searchUtils test caught real behavior: й decomposes to и + U+0306 (combining breve) — the NFD strip turns "й" → "и". This means a user typing достоевскии matches Достоевский entries (friendlier for typos than strict NFC). Documented in test as intended.
  • No deploy required for the partial-class splits — they're compile-time only. Tests files are additive; CI will pick them up automatically.
  • Higher-risk refactors NOT in this PR (deferred to post-Play-Store-launch with telemetry data): LibraryPage.tsx/Search.tsx/ReaderPage.tsx component splits, mobile reader factory hook (premature until 3rd consumer), IEntityTypeConfiguration extraction. Plus blanket NO-GO on offlineDb.ts (903 LOC, IndexedDB schema risk), readerHtml.ts (embedded JS in string), and migrations.

🤖 Generated with Claude Code

mrviduus and others added 2 commits May 24, 2026 01:01
Zero behaviour change. C# partial classes compile to identical IL; backend
integration + unit tests verify no regression. New unit tests are additive
(test infrastructure for mobile is new; web Vitest existed already).

Backend cosmetic splits (compile-identical):
- AppDbContext.cs        655 → 104 LOC + 7 partials (Catalog, User, Reading,
                                                     UserBooks, Vocabulary, Ops,
                                                     Seo, Collections)
- VocabularyEndpoints.cs 1559 → 875 LOC + 6 partials (Stats, Settings, Pending,
                                                      Lookups, Clusters, Admin)
- AdminService.cs        977 → 131 LOC + 4 partials (Upload, Editions,
                                                     Chapters, UserUploads)

Each partial < 400 LOC, single domain per file. Largest non-migration backend
file dropped from 1559 → 875 LOC.

Tests added (Tier 0 — additive):
- Web: analytics.test.ts (15), dataEvents.test.ts (12), errorUtils.test.ts
  (5), formatTime.test.ts (8) — +40 cases
- Mobile (NEW Vitest infra): searchUtils.test.ts (18), features.test.ts (9),
  vocabStatsCache.test.ts (11) — +38 cases, vitest.config.ts +
  __mocks__/async-storage.ts (in-process AsyncStorage mock)

Verification:
- dotnet build: 0 warnings, 0 errors
- dotnet test UnitTests: 216/216 pass
- dotnet test Extraction.Tests: 313/313 pass
- dotnet test Search.Tests: 20/20 pass
- pnpm tsc (web): exit 0
- pnpm test (web): 474/474 pass
- pnpm tsc (mobile): exit 0
- pnpm test (mobile): 38/38 pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-05-24)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mrviduus mrviduus force-pushed the refactor/safe-tier0-tier1 branch from ce944cc to 88bc3df Compare May 24, 2026 05:02
@mrviduus mrviduus merged commit cde57ee into main May 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant