From 9d7c689d81082e4a9c4c505e2c91465dda9158b7 Mon Sep 17 00:00:00 2001 From: Kastriot Salihu Date: Thu, 28 May 2026 15:06:28 +0200 Subject: [PATCH] SP-778: tighten content-cli SonarCloud coverage scope Exclude only inherently un-testable source files from the coverage calculation: type-only declaration files (*.interface.ts / *.interfaces.ts), constant / enum maps (*.constants.ts), and the CLI entry point (src/content-cli.ts). Files with real executable logic but no current tests (factories, untested module.ts wiring) stay in scope so coverage continues to surface them as gaps. Mirror the same negation set in jest.config.ts so local `jest --coverage` and SonarCloud measure the same files. Drop dead `**/*.test.ts` references from sonar-project.properties (the project uses .spec.ts only) and add `**/*.d.ts` + `dist-bundle/**` to `sonar.exclusions`. Add a register() smoke test for configuration-management/module.ts via a new chainable Configurator mock in tests/utls/configurator-mock.ts. This lifts that module from 40.30% -> 92.61% line coverage and demonstrates a pattern other modules can adopt to cover their Commander wiring without writing per-command spec scaffolding. Includes-AI-Code: true Co-authored-by: Cursor --- jest.config.ts | 8 +++- sonar-project.properties | 21 +++++++--- .../configuration-management/module.spec.ts | 27 ++++++++++++ tests/utls/configurator-mock.ts | 41 +++++++++++++++++++ 4 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 tests/utls/configurator-mock.ts diff --git a/jest.config.ts b/jest.config.ts index ce583d1f..8e369684 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -18,11 +18,17 @@ const config: Config.InitialOptions = { "/tests/jest.setup.ts", ], // Coverage configuration + // Keep the negation list in sync with sonar.coverage.exclusions in sonar-project.properties + // so local `jest --coverage` matches what SonarCloud counts. collectCoverageFrom: [ "src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/**/*.spec.ts", - "!src/**/index.ts" + "!src/**/index.ts", + "!src/**/*.interface.ts", + "!src/**/*.interfaces.ts", + "!src/**/*.constants.ts", + "!src/content-cli.ts", ], coverageDirectory: "coverage", coverageReporters: ["text", "lcov", "html"], diff --git a/sonar-project.properties b/sonar-project.properties index df536b58..b5154325 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -15,12 +15,21 @@ sonar.sourceEncoding=UTF-8 # Coverage reporting (from Jest / ts-jest) sonar.javascript.lcov.reportPaths=coverage/lcov.info -# Exclusions (optional, adjust as needed) -# Ignore built files and configs -sonar.exclusions=dist/**, node_modules/**, coverage/**, **/*.test.ts +# Build output and generated artifacts — excluded from all analysis +sonar.exclusions=dist/**, dist-bundle/**, node_modules/**, coverage/**, **/*.d.ts -# Test file inclusions -sonar.test.inclusions=**/*.test.ts, **/*.spec.ts +# Excluded from coverage only — still scanned for bugs and code smells. +# These files contain no executable logic that unit tests can meaningfully cover: +# - *.interface.ts / *.interfaces.ts : type-only declarations +# - *.constants.ts : enum / constant maps +# - src/content-cli.ts : CLI entry point (top-level execution) +sonar.coverage.exclusions=src/content-cli.ts, \ + **/*.interface.ts, \ + **/*.interfaces.ts, \ + **/*.constants.ts + +# Test file inclusions (only .spec.ts is used in this repo) +sonar.test.inclusions=**/*.spec.ts # Report verbose output -sonar.verbose=true \ No newline at end of file +sonar.verbose=true diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 39441346..0ecb2b99 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -4,6 +4,7 @@ import { ConfigCommandService } from "../../../src/commands/configuration-manage import { NodeDependencyService } from "../../../src/commands/configuration-management/node-dependency.service"; import { PackageVersionCommandService } from "../../../src/commands/configuration-management/package-version-command.service"; import { testContext } from "../../utls/test-context"; +import { createMockConfigurator } from "../../utls/configurator-mock"; jest.mock("../../../src/commands/configuration-management/config-command.service"); jest.mock("../../../src/commands/configuration-management/node-dependency.service"); @@ -696,6 +697,32 @@ describe("Configuration Management Module - Action Validations", () => { }); }); + describe("register", () => { + it("registers all expected top-level command groups without throwing", () => { + const mockConfigurator = createMockConfigurator(); + + expect(() => new Module().register(testContext, mockConfigurator)).not.toThrow(); + + // Top-level groups attached to the root configurator + expect(mockConfigurator.command).toHaveBeenCalledWith("config"); + expect(mockConfigurator.command).toHaveBeenCalledWith("list"); + }); + + it("wires an action handler for every leaf subcommand", () => { + const mockConfigurator = createMockConfigurator(); + + new Module().register(testContext, mockConfigurator); + + // Each leaf command terminates the fluent chain with .action(handler). + // Keep this count in sync when adding or removing commands in module.ts. + const expectedLeafCommands = 17; + expect(mockConfigurator.action).toHaveBeenCalledTimes(expectedLeafCommands); + for (const call of mockConfigurator.action.mock.calls) { + expect(typeof call[0]).toBe("function"); + } + }); + }); + describe("listNodeDependencies", () => { it("should call listNodeDependencies with correct parameters", async () => { const options: OptionValues = { diff --git a/tests/utls/configurator-mock.ts b/tests/utls/configurator-mock.ts new file mode 100644 index 00000000..a22fe19d --- /dev/null +++ b/tests/utls/configurator-mock.ts @@ -0,0 +1,41 @@ +import { Configurator } from "../../src/core/command/module-handler"; + +/** + * A chainable fake of Configurator + CommandConfig used by module-level smoke tests. + * + * Modules register their Commander commands via fluent chains + * (`.command(...).option(...).action(...)`). To exercise the register() body without + * spinning up Commander, every builder method on the chain returns the same proxy + * instance and `.action()` is a no-op spy. This lets a test call + * `module.register(context, mockConfigurator)` and assert on which commands were + * registered, which flips the entire register() body to covered. + */ +export interface MockConfigurator extends Configurator { + command: jest.Mock; + description: jest.Mock; + option: jest.Mock; + requiredOption: jest.Mock; + alias: jest.Mock; + argument: jest.Mock; + betaOption: jest.Mock; + deprecationNotice: jest.Mock; + beta: jest.Mock; + action: jest.Mock; +} + +export function createMockConfigurator(): MockConfigurator { + const chain: Partial = {}; + + chain.command = jest.fn(() => chain); + chain.description = jest.fn(() => chain); + chain.option = jest.fn(() => chain); + chain.requiredOption = jest.fn(() => chain); + chain.alias = jest.fn(() => chain); + chain.argument = jest.fn(() => chain); + chain.betaOption = jest.fn(() => chain); + chain.deprecationNotice = jest.fn(() => chain); + chain.beta = jest.fn(() => chain); + chain.action = jest.fn(); + + return chain as MockConfigurator; +}