feat(coverage): L0 unification — close test gaps in nav, media, mp

Closes the per-layer threshold gaps surfaced by the 2026-05-13 PRD
audit. After this commit all five features pass their declared
100%/100%/95%/100% bands on entities + use-cases + controllers.

media (was: missing @vitest/coverage-v8 + missing vitest config block +
  one controller at 86.66% lines / 75% branches)
  - Added @vitest/coverage-v8 dev dep
  - Applied the standard helper-driven vitest config
  - Declared the coverage section in feature.manifest.ts
  - Added 2 tests to list-media.controller.test.ts covering the
    InputParseError branch (unknown fields + invalid limit)
  - Now: 16 files / 80 tests / 97.12% / controllers 100%

marketing-pages (was: get-site-settings.controller at 93.54% lines /
  90.9% branches)
  - Added 1 test to get-site-settings.controller.test.ts covering the
    InputParseError branch on unknown fields
  - Now: 22 files / 68 tests / 95.66% / controllers 100%

navigation (was: entities/errors/common.ts at 50% function hits +
  get-header.controller at 86.66% lines / 80% branches)
  - Root cause: InputParseError class never instantiated in any test
  - Added 2 tests to get-header.controller.test.ts covering the
    InputParseError branch + verifying the Zod cause is preserved.
    One test exercises both gap files at once (controller throws,
    InputParseError class is constructed).
  - Wired navigation/vitest.config.ts through the shared helper
  - Declared the coverage section in feature.manifest.ts
  - Now: 11 files / 45 tests / 98.04% / entities + controllers 100%

All 5 features now drive thresholds from the manifest via the helper.
The duplication problem the keystone eliminates is gone.

Repo-wide via `pnpm coverage:aggregate`:
  - statements 95.87% (lh 2994 / lf 3123)
  - branches   88.91% (brh 433 / brf 487)
  - functions  100%   (fnh 142 / fnf 142)
  - lines      95.87%

`pnpm coverage:diff -- --base HEAD~1` reports status: pass.

coverage/summary.json refreshed in the same commit so the trend
captures the post-unification state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 16:39:08 +02:00
parent 6428f10b82
commit bf0b049583
10 changed files with 191 additions and 56 deletions

View File

@@ -1,61 +1,89 @@
{
"generatedAt": "2026-05-13T12:08:33.303Z",
"commit": "412d994",
"generatedAt": "2026-05-13T14:38:41.601Z",
"commit": "6428f10",
"repo": {
"statements": 95.22,
"branches": 90.52,
"functions": 98.77,
"lines": 95.22,
"statements": 95.87,
"branches": 88.91,
"functions": 100,
"lines": 95.87,
"counts": {
"lf": 1633,
"lh": 1555,
"brf": 232,
"brh": 210,
"fnf": 81,
"fnh": 80
"lf": 3123,
"lh": 2994,
"brf": 487,
"brh": 433,
"fnf": 142,
"fnh": 142
}
},
"byPackage": {
"@repo/auth": {
"statements": 93.77,
"statements": 93.85,
"branches": 90.82,
"functions": 100,
"lines": 93.77,
"lines": 93.85,
"counts": {
"lf": 787,
"lh": 738,
"lf": 797,
"lh": 748,
"brf": 98,
"brh": 89,
"fnf": 44,
"fnh": 44
}
},
"@repo/media": {
"statements": 96.72,
"branches": 89.47,
"@repo/blog": {
"statements": 96.39,
"branches": 88.65,
"functions": 100,
"lines": 96.72,
"lines": 96.39,
"counts": {
"lf": 548,
"lh": 530,
"lf": 747,
"lh": 720,
"brf": 141,
"brh": 125,
"fnf": 29,
"fnh": 29
}
},
"@repo/marketing-pages": {
"statements": 95.66,
"branches": 83.93,
"functions": 100,
"lines": 95.66,
"counts": {
"lf": 715,
"lh": 684,
"brf": 112,
"brh": 94,
"fnf": 32,
"fnh": 32
}
},
"@repo/media": {
"statements": 97.13,
"branches": 90.53,
"functions": 100,
"lines": 97.13,
"counts": {
"lf": 557,
"lh": 541,
"brf": 95,
"brh": 85,
"brh": 86,
"fnf": 24,
"fnh": 24
}
},
"@repo/navigation": {
"statements": 96.31,
"branches": 92.31,
"functions": 92.31,
"lines": 96.31,
"statements": 98.05,
"branches": 95.12,
"functions": 100,
"lines": 98.05,
"counts": {
"lf": 298,
"lh": 287,
"brf": 39,
"brh": 36,
"lf": 307,
"lh": 301,
"brf": 41,
"brh": 39,
"fnf": 13,
"fnh": 12
"fnh": 13
}
}
}

View File

@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import { getSiteSettingsController } from "@/interface-adapters/controllers/get-site-settings.controller";
import { getSiteSettingsUseCase } from "@/application/use-cases/get-site-settings.use-case";
import { MockSiteSettingsRepository } from "@/infrastructure/repositories/site-settings.repository.mock";
import { InputParseError } from "@/entities/errors/common";
describe("getSiteSettingsController", () => {
it("returns site settings", async () => {
@@ -12,4 +13,14 @@ describe("getSiteSettingsController", () => {
const result = await controller({});
expect(result.siteName).toBe("My App");
});
it("throws InputParseError on unknown fields (strict schema)", async () => {
const repo = new MockSiteSettingsRepository();
const useCase = getSiteSettingsUseCase(repo);
const controller = getSiteSettingsController(useCase);
await expect(controller({ unknownField: 1 })).rejects.toThrow(
InputParseError,
);
});
});

View File

@@ -30,6 +30,7 @@
"@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.2.4",
"vitest": "^3.1.0"
}
}

View File

@@ -13,6 +13,25 @@ export const mediaManifest = defineFeature({
},
realtimeChannels: [],
jobs: [],
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: { statements: 100, branches: 100, functions: 100, lines: 100 },
"use-cases": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
controllers: {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
},
mutationTargets: ["entities", "use-cases"],
},
} as const);
export type MediaManifest = typeof mediaManifest;

View File

@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import { listMediaController } from "@/interface-adapters/controllers/list-media.controller";
import { listMediaUseCase } from "@/application/use-cases/list-media.use-case";
import { MockMediaRepository } from "@/infrastructure/repositories/media.repository.mock";
import { InputParseError } from "@/entities/errors/common";
import { mediaFactory } from "@/__factories__/media.factory";
describe("listMediaController", () => {
@@ -38,4 +39,22 @@ describe("listMediaController", () => {
const result = await controller({ limit: 2, offset: 1 });
expect(result).toHaveLength(2);
});
it("throws InputParseError on unknown fields (strict schema)", async () => {
const repo = new MockMediaRepository();
const useCase = listMediaUseCase(repo);
const controller = listMediaController(useCase);
await expect(controller({ unknownField: 1 })).rejects.toThrow(
InputParseError,
);
});
it("throws InputParseError when limit is not a positive integer", async () => {
const repo = new MockMediaRepository();
const useCase = listMediaUseCase(repo);
const controller = listMediaController(useCase);
await expect(controller({ limit: -1 })).rejects.toThrow(InputParseError);
});
});

View File

@@ -1,8 +1,32 @@
import path from "node:path";
import { mergeConfig } from "vitest/config";
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
import {
DEFAULT_COVERAGE_BANDS,
vitestThresholdsFromBands,
} from "@repo/core-shared/conformance/coverage";
// Coverage thresholds derived from DEFAULT_COVERAGE_BANDS via the shared
// helper (ADR-020). The feature.manifest.ts `coverage.bands` section
// declares these for boot-time `assertFeatureConformance`. Edit the
// manifest when adjusting per-feature bands.
export default mergeConfig(nodeVitestConfig, {
test: {
coverage: {
exclude: [
// DI bootstrap — wires InversifyJS at app startup; not unit-testable
"src/di/bind-production.ts",
// Pure TypeScript interface files — not executable
"src/application/repositories/**",
"src/application/services/**",
// Payload CMS collection config — declarative data, tested via Payload integration
"src/integrations/cms/**",
// React Query option builders — integration-tested in apps
"src/ui/**",
],
thresholds: vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS),
},
},
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},

View File

@@ -11,6 +11,25 @@ export const navigationManifest = defineFeature({
},
realtimeChannels: [],
jobs: [],
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: { statements: 100, branches: 100, functions: 100, lines: 100 },
"use-cases": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
controllers: {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
},
mutationTargets: ["entities", "use-cases"],
},
} as const);
export type NavigationManifest = typeof navigationManifest;

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { getHeaderController } from "@/interface-adapters/controllers/get-header.controller";
import { getHeaderUseCase } from "@/application/use-cases/get-header.use-case";
import { MockHeaderRepository } from "@/infrastructure/repositories/header.repository.mock";
import { InputParseError } from "@/entities/errors/common";
describe("getHeaderController", () => {
it("returns the header with items", async () => {
@@ -14,4 +15,29 @@ describe("getHeaderController", () => {
expect(result.items.length).toBeGreaterThan(0);
expect(result.items[0]?.label).toBe("Home");
});
it("throws InputParseError on unknown fields (strict schema)", async () => {
const repo = new MockHeaderRepository();
const useCase = getHeaderUseCase(repo);
const controller = getHeaderController(useCase);
await expect(controller({ unknownField: 1 })).rejects.toThrow(
InputParseError,
);
});
it("InputParseError preserves the cause from Zod", async () => {
const repo = new MockHeaderRepository();
const useCase = getHeaderUseCase(repo);
const controller = getHeaderController(useCase);
try {
await controller({ unknownField: 1 });
expect.fail("expected InputParseError");
} catch (err) {
expect(err).toBeInstanceOf(InputParseError);
expect((err as Error).name).toBe("InputParseError");
expect((err as InputParseError).cause).toBeDefined();
}
});
});

View File

@@ -1,7 +1,15 @@
import path from "node:path";
import { mergeConfig } from "vitest/config";
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
import {
DEFAULT_COVERAGE_BANDS,
vitestThresholdsFromBands,
} from "@repo/core-shared/conformance/coverage";
// Coverage thresholds derived from DEFAULT_COVERAGE_BANDS via the shared
// helper (ADR-020). The feature.manifest.ts `coverage.bands` section
// declares these for boot-time `assertFeatureConformance`. Edit the
// manifest when adjusting per-feature bands.
export default mergeConfig(nodeVitestConfig, {
test: {
coverage: {
@@ -15,30 +23,7 @@ export default mergeConfig(nodeVitestConfig, {
// React Query option builders — integration-tested in apps
"src/ui/**",
],
thresholds: {
"src/entities/**": {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
"src/application/use-cases/**": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
"src/interface-adapters/controllers/**": {
statements: 100,
branches: 95,
functions: 100,
lines: 100,
},
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
thresholds: vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS),
},
},
resolve: {

3
pnpm-lock.yaml generated
View File

@@ -812,6 +812,9 @@ importers:
"@types/node":
specifier: ^22.0.0
version: 22.19.17
"@vitest/coverage-v8":
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
vitest:
specifier: ^3.1.0
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)