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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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") },
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user