diff --git a/coverage/summary.json b/coverage/summary.json index d155d0f..7d87bf7 100644 --- a/coverage/summary.json +++ b/coverage/summary.json @@ -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 } } } diff --git a/packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.test.ts b/packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.test.ts index 9958745..03bdf5f 100644 --- a/packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.test.ts +++ b/packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.test.ts @@ -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, + ); + }); }); diff --git a/packages/media/package.json b/packages/media/package.json index 8adc0f7..86389d3 100644 --- a/packages/media/package.json +++ b/packages/media/package.json @@ -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" } } diff --git a/packages/media/src/feature.manifest.ts b/packages/media/src/feature.manifest.ts index 3f227a6..4efffe8 100644 --- a/packages/media/src/feature.manifest.ts +++ b/packages/media/src/feature.manifest.ts @@ -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; diff --git a/packages/media/src/interface-adapters/controllers/list-media.controller.test.ts b/packages/media/src/interface-adapters/controllers/list-media.controller.test.ts index 250dc94..dc3aeb5 100644 --- a/packages/media/src/interface-adapters/controllers/list-media.controller.test.ts +++ b/packages/media/src/interface-adapters/controllers/list-media.controller.test.ts @@ -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); + }); }); diff --git a/packages/media/vitest.config.ts b/packages/media/vitest.config.ts index 2ee07c1..56b5876 100644 --- a/packages/media/vitest.config.ts +++ b/packages/media/vitest.config.ts @@ -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") }, }, diff --git a/packages/navigation/src/feature.manifest.ts b/packages/navigation/src/feature.manifest.ts index 98b9300..f7f31a2 100644 --- a/packages/navigation/src/feature.manifest.ts +++ b/packages/navigation/src/feature.manifest.ts @@ -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; diff --git a/packages/navigation/src/interface-adapters/controllers/get-header.controller.test.ts b/packages/navigation/src/interface-adapters/controllers/get-header.controller.test.ts index a028a49..039d0c8 100644 --- a/packages/navigation/src/interface-adapters/controllers/get-header.controller.test.ts +++ b/packages/navigation/src/interface-adapters/controllers/get-header.controller.test.ts @@ -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(); + } + }); }); diff --git a/packages/navigation/vitest.config.ts b/packages/navigation/vitest.config.ts index 357ca55..ae607af 100644 --- a/packages/navigation/vitest.config.ts +++ b/packages/navigation/vitest.config.ts @@ -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: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e21e304..66940fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)