From 02f61ee53ed2730c4d27e1d46603000faa2484a0 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 18 Mar 2026 10:15:39 +0000 Subject: [PATCH 01/17] CCM-14615: add temp workflow for unit tests only --- .github/workflows/temp-unit-tests-only.yaml | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/temp-unit-tests-only.yaml diff --git a/.github/workflows/temp-unit-tests-only.yaml b/.github/workflows/temp-unit-tests-only.yaml new file mode 100644 index 000000000..5b53fad61 --- /dev/null +++ b/.github/workflows/temp-unit-tests-only.yaml @@ -0,0 +1,62 @@ +name: "Temp unit tests only" + +on: + workflow_dispatch: + inputs: + nodejs_version: + description: "Node.js version, set by the CI/CD pipeline workflow" + required: true + type: string + python_version: + description: "Python version, set by the CI/CD pipeline workflow" + required: true + type: string + +env: + AWS_REGION: eu-west-2 + TERM: xterm-256color + +jobs: + test-unit: + name: "Unit tests" + runs-on: ubuntu-latest + timeout-minutes: 7 + permissions: + contents: read + packages: read + steps: + - name: "Checkout code" + uses: actions/checkout@v5 + - uses: ./.github/actions/node-install + with: + node-version: ${{ inputs.nodejs_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Setup Python" + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python_version }} + cache: 'pip' + cache-dependency-path: '**/requirements*.txt' + - name: "Run unit test suite" + run: | + make test-unit + - name: "Save the result of fast test suite" + uses: actions/upload-artifact@v4 + with: + name: unit-tests + path: "**/.reports/unit" + include-hidden-files: true + if: always() + - name: "Save the result of code coverage" + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: ".reports/lcov.info" + - name: "Save Python coverage reports" + uses: actions/upload-artifact@v4 + with: + name: python-coverage-reports + path: | + src/**/coverage.xml + utils/**/coverage.xml + lambdas/**/coverage.xml From 6d198635c73e26a3d01f206b66fed10aeb351088 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 10:30:34 +0000 Subject: [PATCH 02/17] CCM-14615: add temp workflow for unit tests only --- .github/workflows/temp-unit-tests-only.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/temp-unit-tests-only.yaml b/.github/workflows/temp-unit-tests-only.yaml index 5b53fad61..fc790c360 100644 --- a/.github/workflows/temp-unit-tests-only.yaml +++ b/.github/workflows/temp-unit-tests-only.yaml @@ -1,7 +1,7 @@ name: "Temp unit tests only" on: - workflow_dispatch: + workflow_dispatch: inputs: nodejs_version: description: "Node.js version, set by the CI/CD pipeline workflow" @@ -11,6 +11,9 @@ on: description: "Python version, set by the CI/CD pipeline workflow" required: true type: string + push: + branches: + - feature/CCM-14615_unit-test-quickening env: AWS_REGION: eu-west-2 From 3ab0de75ea90ebc0d2002e239d0b0635eaec98ba Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 10:31:41 +0000 Subject: [PATCH 03/17] CCM-14615: run parrallel using jest workspaces --- jest.config.cjs | 151 ++++++++++++++++++ .../__tests__/app/notify-api-client.test.ts | 8 +- .../src/__tests__/domain/mapper.test.ts | 8 +- package.json | 1 + scripts/tests/unit.sh | 92 ++++++++--- .../builder/__tests__/build-schema.test.ts | 28 ++-- 6 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 jest.config.cjs diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 000000000..07f9b7230 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,151 @@ +/** + * Root Jest config — runs all TypeScript/JavaScript workspace test suites in + * parallel via Jest's native `projects` support. + * + * Written as CJS (.cjs) so Jest can load it without needing a root tsconfig.json. + * The base config is inlined from jest.config.base.ts to keep this file + * self-contained and avoid any TypeScript compilation at load time, which would + * require a root tsconfig.json and risk interfering with workspace ts-jest runs. + * + * When jest.config.base.ts changes, this file must be kept in sync manually. + * + * Each project's rootDir is set to its workspace directory so that relative + * paths (coverageDirectory, HTML reporter outputPath, etc.) resolve relative + * to the workspace, not the repo root. + * + * Note: src/cloudevents has a hand-rolled jest.config.cjs; it is included via + * its directory path so Jest discovers that file directly. + * + * Note: src/digital-letters-events and tests/playwright have no Jest tests + * and are intentionally excluded. + */ + +const base = { + preset: 'ts-jest', + clearMocks: true, + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/__tests__/**', + '!src/**/*.test.{ts,tsx}', + '!src/**/*.spec.{ts,tsx}', + ], + coverageDirectory: './.reports/unit/coverage', + coverageProvider: 'babel', + coverageThreshold: { + global: { branches: 100, functions: 100, lines: 100, statements: -10 }, + }, + coveragePathIgnorePatterns: ['/__tests__/'], + transform: { '^.+\\.ts$': 'ts-jest' }, + testPathIgnorePatterns: ['.build'], + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + reporters: [ + 'default', + [ + 'jest-html-reporter', + { + pageTitle: 'Test Report', + outputPath: './.reports/unit/test-report.html', + includeFailureMsg: true, + }, + ], + ], + testEnvironment: 'node', + moduleDirectories: ['node_modules', 'src'], +}; + +// Workspaces that use the base config with no overrides +const standardWorkspaces = [ + 'lambdas/file-scanner-lambda', + 'lambdas/key-generation', + 'lambdas/refresh-apim-access-token', + 'lambdas/pdm-mock-lambda', + 'lambdas/pdm-poll-lambda', + 'lambdas/ttl-create-lambda', + 'lambdas/ttl-handle-expiry-lambda', + 'lambdas/ttl-poll-lambda', + 'lambdas/pdm-uploader-lambda', + 'lambdas/print-sender-lambda', + 'lambdas/print-analyser', + 'lambdas/report-scheduler', + 'lambdas/report-event-transformer', + 'lambdas/move-scanned-files-lambda', + 'lambdas/report-generator', + 'utils/sender-management', +]; + +/** @type {import('jest').Config} */ +module.exports = { + projects: [ + // Standard workspaces — no overrides + ...standardWorkspaces.map((ws) => ({ + ...base, + rootDir: `/${ws}`, + displayName: ws, + })), + + // utils/utils — relaxed coverage thresholds + exclude index.ts + { + ...base, + rootDir: '/utils/utils', + displayName: 'utils/utils', + coverageThreshold: { + global: { branches: 85, functions: 85, lines: 85, statements: -10 }, + }, + coveragePathIgnorePatterns: [...base.coveragePathIgnorePatterns, 'index.ts'], + }, + + // lambdas/core-notifier-lambda — moduleNameMapper unifies `crypto` and + // `node:crypto` in Jest's registry so that jest.mock('node:crypto') in the + // test files also intercepts the bare require('crypto') call made by + // node-jose at module-load time, preventing an undefined helpers.nodeCrypto + // crash in ecdsa.js. + { + ...base, + rootDir: '/lambdas/core-notifier-lambda', + displayName: 'lambdas/core-notifier-lambda', + }, + + // lambdas/print-status-handler — @nhsdigital/nhs-notify-event-schemas-supplier-api + // ships ESM source; it must be transformed by ts-jest rather than skipped. + { + ...base, + rootDir: '/lambdas/print-status-handler', + displayName: 'lambdas/print-status-handler', + transformIgnorePatterns: [ + 'node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)', + ], + }, + + // src/python-schema-generator — excludes merge-allof CLI entry point + { + ...base, + rootDir: '/src/python-schema-generator', + displayName: 'src/python-schema-generator', + coveragePathIgnorePatterns: [ + ...base.coveragePathIgnorePatterns, + 'src/merge-allof-cli.ts', + ], + }, + + // src/typescript-schema-generator — excludes CLI entry points. + // Requires --experimental-vm-modules (set via NODE_OPTIONS in the + // test:unit:parallel script) because json-schema-to-typescript uses a + // dynamic import() of prettier at runtime, which Node.js rejects inside a + // Jest VM context without the flag. + { + ...base, + rootDir: '/src/typescript-schema-generator', + displayName: 'src/typescript-schema-generator', + coveragePathIgnorePatterns: [ + ...base.coveragePathIgnorePatterns, + 'src/generate-types-cli.ts', + 'src/generate-validators-cli.ts', + ], + }, + + // src/cloudevents — uses its own jest.config.cjs (hand-rolled ts-jest options) + '/src/cloudevents', + ], +}; diff --git a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts index 59af51fd2..80077f41e 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts @@ -12,7 +12,13 @@ import { IAccessTokenRepository, NotifyClient } from 'app/notify-api-client'; import { RequestAlreadyReceivedError } from 'domain/request-already-received-error'; jest.mock('utils'); -jest.mock('node:crypto'); +// Use a partial manual mock so that node-jose's require('crypto') still gets +// the real crypto implementation (needed for getHashes() etc.) while +// randomUUID is replaced with a jest.fn() for test control. +jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), + randomUUID: jest.fn(), +})); jest.mock('axios', () => { const original: AxiosStatic = jest.requireActual('axios'); diff --git a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts index e1bde3bc7..cf2c37fd9 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts @@ -9,7 +9,13 @@ import { PDMResourceAvailable } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; jest.mock('utils'); -jest.mock('node:crypto'); +// Use a partial manual mock so that node-jose's require('crypto') still gets +// the real crypto implementation (needed for getHashes() etc.) while +// randomUUID is replaced with a jest.fn() for test control. +jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), + randomUUID: jest.fn(), +})); const mockLogger = jest.mocked(logger); const mockRandomUUID = jest.mocked(randomUUID); diff --git a/package.json b/package.json index ca8080a48..81b845389 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "lint:fix": "npm run lint:fix --workspaces", "start": "npm run start --workspace frontend", "test:unit": "npm run test:unit --workspaces", + "test:unit:parallel": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --config jest.config.cjs", "typecheck": "npm run typecheck --workspaces" }, "version": "0.0.1", diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 1a4a23e6c..67ee6037c 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -17,59 +17,103 @@ cd "$(git rev-parse --show-toplevel)" # tests from here. If you want to run other test suites, see the predefined # tasks in scripts/test.mk. +# Timing helpers — records wall-clock duration for each labelled step and prints +# a summary table at exit so it's easy to see where the time is going. +_timer_labels=() +_timer_seconds=() + +run_timed() { + local label="$1" + shift + local start + start=$(date +%s) + local rc=0 + "$@" || rc=$? + local end + end=$(date +%s) + _timer_labels+=("$label") + _timer_seconds+=("$((end - start))") + return "$rc" +} + +print_timing_summary() { + echo "" + echo "===== Timing Summary =====" + local total=0 + for i in "${!_timer_labels[@]}"; do + printf " %-55s %4ds\n" "${_timer_labels[$i]}" "${_timer_seconds[$i]}" + total=$((total + _timer_seconds[$i])) + done + echo " ---------------------------------------------------------" + printf " %-55s %4ds\n" "TOTAL" "$total" + echo "==========================" +} + +trap print_timing_summary EXIT + # run tests # TypeScript/JavaScript projects (npm workspace) -# Note: src/cloudevents is included in workspaces, so it will be tested here -npm ci -npm run generate-dependencies -npm run test:unit --workspaces +# Runs all Jest workspaces in parallel via the root jest.config.cjs projects +# config, which is faster than sequential `npm run test:unit --workspaces`. +# Note: src/cloudevents is included in the projects list in jest.config.cjs. +# Use || to capture any Jest failure so that Python tests always run; the exit +# code is propagated at the end of the script. +run_timed "npm ci" npm ci +run_timed "npm run generate-dependencies" npm run generate-dependencies +run_timed "npm run test:unit:parallel" npm run test:unit:parallel || jest_exit=$? # Python projects - asyncapigenerator echo "Setting up and running asyncapigenerator tests..." -make -C ./src/asyncapigenerator install-dev -make -C ./src/asyncapigenerator coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "asyncapigenerator: install-dev" make -C ./src/asyncapigenerator install-dev +run_timed "asyncapigenerator: coverage" make -C ./src/asyncapigenerator coverage # Python projects - cloudeventjekylldocs echo "Setting up and running cloudeventjekylldocs tests..." -make -C ./src/cloudeventjekylldocs install-dev -make -C ./src/cloudeventjekylldocs coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "cloudeventjekylldocs: install-dev" make -C ./src/cloudeventjekylldocs install-dev +run_timed "cloudeventjekylldocs: coverage" make -C ./src/cloudeventjekylldocs coverage # Python projects - eventcatalogasyncapiimporter echo "Setting up and running eventcatalogasyncapiimporter tests..." -make -C ./src/eventcatalogasyncapiimporter install-dev -make -C ./src/eventcatalogasyncapiimporter coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "eventcatalogasyncapiimporter: install-dev" make -C ./src/eventcatalogasyncapiimporter install-dev +run_timed "eventcatalogasyncapiimporter: coverage" make -C ./src/eventcatalogasyncapiimporter coverage # Python utility packages - py-utils echo "Setting up and running py-utils tests..." -make -C ./utils/py-utils install-dev -make -C ./utils/py-utils coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "py-utils: install-dev" make -C ./utils/py-utils install-dev +run_timed "py-utils: coverage" make -C ./utils/py-utils coverage # Python projects - python-schema-generator echo "Setting up and running python-schema-generator tests..." -make -C ./src/python-schema-generator install-dev -make -C ./src/python-schema-generator coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "python-schema-generator: install-dev" make -C ./src/python-schema-generator install-dev +run_timed "python-schema-generator: coverage" make -C ./src/python-schema-generator coverage # Python Lambda - mesh-acknowledge echo "Setting up and running mesh-acknowledge tests..." -make -C ./lambdas/mesh-acknowledge install-dev -make -C ./lambdas/mesh-acknowledge coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-acknowledge: install-dev" make -C ./lambdas/mesh-acknowledge install-dev +run_timed "mesh-acknowledge: coverage" make -C ./lambdas/mesh-acknowledge coverage # Python Lambda - mesh-poll echo "Setting up and running mesh-poll tests..." -make -C ./lambdas/mesh-poll install-dev -make -C ./lambdas/mesh-poll coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-poll: install-dev" make -C ./lambdas/mesh-poll install-dev +run_timed "mesh-poll: coverage" make -C ./lambdas/mesh-poll coverage # Python Lambda - mesh-download echo "Setting up and running mesh-download tests..." -make -C ./lambdas/mesh-download install-dev -make -C ./lambdas/mesh-download coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "mesh-download: install-dev" make -C ./lambdas/mesh-download install-dev +run_timed "mesh-download: coverage" make -C ./lambdas/mesh-download coverage # Python Lambda - report-sender echo "Setting up and running report-sender tests..." -make -C ./lambdas/report-sender install-dev -make -C ./lambdas/report-sender coverage # Run with coverage to generate coverage.xml for SonarCloud +run_timed "report-sender: install-dev" make -C ./lambdas/report-sender install-dev +run_timed "report-sender: coverage" make -C ./lambdas/report-sender coverage # merge coverage reports -mkdir -p .reports -TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger "**/.reports/unit/coverage/lcov.info" ".reports/lcov.info" --ignore "node_modules" --prepend-source-files --prepend-path-fix "../../.." +run_timed "lcov-result-merger" \ + bash -c 'mkdir -p .reports && TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger "**/.reports/unit/coverage/lcov.info" ".reports/lcov.info" --ignore "node_modules" --prepend-source-files --prepend-path-fix "../../.."' + +# Propagate any Jest failure now that all other test suites have completed +if [ "${jest_exit:-0}" -ne 0 ]; then + echo "Jest tests failed with exit code ${jest_exit}" + exit "${jest_exit}" +fi diff --git a/src/cloudevents/tools/builder/__tests__/build-schema.test.ts b/src/cloudevents/tools/builder/__tests__/build-schema.test.ts index f7c35f908..7c7fecf9b 100644 --- a/src/cloudevents/tools/builder/__tests__/build-schema.test.ts +++ b/src/cloudevents/tools/builder/__tests__/build-schema.test.ts @@ -9,6 +9,10 @@ import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; +// Resolve paths relative to this test file so the suite works whether Jest +// runs from the workspace directory or from the repository root. +const cloudEventsRoot = path.resolve(__dirname, '..', '..', '..'); + describe('build-schema CLI', () => { let testDir: string; let sourceDir: string; @@ -16,7 +20,7 @@ describe('build-schema CLI', () => { beforeAll(() => { // Create test directories - testDir = path.join(process.cwd(), 'test-build-' + Date.now()); + testDir = path.join(cloudEventsRoot, 'test-build-' + Date.now()); sourceDir = path.join(testDir, 'src'); outputDir = path.join(testDir, 'output'); @@ -52,7 +56,7 @@ describe('build-schema CLI', () => { execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -91,7 +95,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -120,7 +124,7 @@ properties: const result = execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe', encoding: 'utf-8' } @@ -139,7 +143,7 @@ properties: execSync( 'tsx tools/builder/build-schema.ts', { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -160,7 +164,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${inputFile}" "${outputDir}" "https://example.com"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -198,7 +202,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${mainFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -224,7 +228,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${yamlFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -246,7 +250,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "nonexistent.json" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -266,7 +270,7 @@ properties: execSync( `tsx tools/builder/build-schema.ts "${invalidFile}" "${outputDir}"`, { - cwd: process.cwd(), + cwd: cloudEventsRoot, stdio: 'pipe' } ); @@ -282,7 +286,7 @@ properties: describe('module structure', () => { it('should export expected functions (if any)', () => { // build-schema-cli.ts contains the testable logic - const buildSchemaCliPath = path.join(process.cwd(), 'tools/builder/build-schema-cli.ts'); + const buildSchemaCliPath = path.join(cloudEventsRoot, 'tools/builder/build-schema-cli.ts'); expect(fs.existsSync(buildSchemaCliPath)).toBe(true); // Verify file has expected structure @@ -293,7 +297,7 @@ properties: }); it('should have proper imports', () => { - const buildSchemaCliPath = path.join(process.cwd(), 'tools/builder/build-schema-cli.ts'); + const buildSchemaCliPath = path.join(cloudEventsRoot, 'tools/builder/build-schema-cli.ts'); const content = fs.readFileSync(buildSchemaCliPath, 'utf-8'); expect(content).toContain('import fs from'); From 52c6caf5c9fd334752d7c915813d821932a17450 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 11:18:55 +0000 Subject: [PATCH 04/17] CCM-14615: run pytets in parrallel --- lambdas/mesh-acknowledge/pytest.ini | 1 + lambdas/mesh-download/pytest.ini | 1 + lambdas/mesh-poll/pytest.ini | 1 + lambdas/report-sender/pytest.ini | 1 + scripts/tests/unit.sh | 106 ++++++++++++-------- src/asyncapigenerator/pytest.ini | 1 + src/cloudeventjekylldocs/pytest.ini | 1 + src/eventcatalogasyncapiimporter/pytest.ini | 1 + src/python-schema-generator/pytest.ini | 1 + utils/py-utils/pytest.ini | 1 + 10 files changed, 71 insertions(+), 44 deletions(-) diff --git a/lambdas/mesh-acknowledge/pytest.ini b/lambdas/mesh-acknowledge/pytest.ini index e19306a77..7f80cf1af 100644 --- a/lambdas/mesh-acknowledge/pytest.ini +++ b/lambdas/mesh-acknowledge/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-acknowledge/.coverage omit = */mesh_acknowledge/__tests__/* */test_*.py diff --git a/lambdas/mesh-download/pytest.ini b/lambdas/mesh-download/pytest.ini index 303659aad..b2483b3c0 100644 --- a/lambdas/mesh-download/pytest.ini +++ b/lambdas/mesh-download/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-download/.coverage omit = */tests/* */test_*.py diff --git a/lambdas/mesh-poll/pytest.ini b/lambdas/mesh-poll/pytest.ini index 933720312..8657f96d1 100644 --- a/lambdas/mesh-poll/pytest.ini +++ b/lambdas/mesh-poll/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/mesh-poll/.coverage omit = */mesh_poll/__tests__/* */test_*.py diff --git a/lambdas/report-sender/pytest.ini b/lambdas/report-sender/pytest.ini index 91879c293..9402fd11a 100644 --- a/lambdas/report-sender/pytest.ini +++ b/lambdas/report-sender/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = lambdas/report-sender/.coverage omit = */report_sender/__tests__/* */test_*.py diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 67ee6037c..63a24fd38 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -63,50 +63,60 @@ run_timed "npm ci" npm ci run_timed "npm run generate-dependencies" npm run generate-dependencies run_timed "npm run test:unit:parallel" npm run test:unit:parallel || jest_exit=$? -# Python projects - asyncapigenerator -echo "Setting up and running asyncapigenerator tests..." -run_timed "asyncapigenerator: install-dev" make -C ./src/asyncapigenerator install-dev -run_timed "asyncapigenerator: coverage" make -C ./src/asyncapigenerator coverage - -# Python projects - cloudeventjekylldocs -echo "Setting up and running cloudeventjekylldocs tests..." -run_timed "cloudeventjekylldocs: install-dev" make -C ./src/cloudeventjekylldocs install-dev -run_timed "cloudeventjekylldocs: coverage" make -C ./src/cloudeventjekylldocs coverage - -# Python projects - eventcatalogasyncapiimporter -echo "Setting up and running eventcatalogasyncapiimporter tests..." -run_timed "eventcatalogasyncapiimporter: install-dev" make -C ./src/eventcatalogasyncapiimporter install-dev -run_timed "eventcatalogasyncapiimporter: coverage" make -C ./src/eventcatalogasyncapiimporter coverage - -# Python utility packages - py-utils -echo "Setting up and running py-utils tests..." -run_timed "py-utils: install-dev" make -C ./utils/py-utils install-dev -run_timed "py-utils: coverage" make -C ./utils/py-utils coverage - -# Python projects - python-schema-generator -echo "Setting up and running python-schema-generator tests..." -run_timed "python-schema-generator: install-dev" make -C ./src/python-schema-generator install-dev -run_timed "python-schema-generator: coverage" make -C ./src/python-schema-generator coverage - -# Python Lambda - mesh-acknowledge -echo "Setting up and running mesh-acknowledge tests..." -run_timed "mesh-acknowledge: install-dev" make -C ./lambdas/mesh-acknowledge install-dev -run_timed "mesh-acknowledge: coverage" make -C ./lambdas/mesh-acknowledge coverage - -# Python Lambda - mesh-poll -echo "Setting up and running mesh-poll tests..." -run_timed "mesh-poll: install-dev" make -C ./lambdas/mesh-poll install-dev -run_timed "mesh-poll: coverage" make -C ./lambdas/mesh-poll coverage - -# Python Lambda - mesh-download -echo "Setting up and running mesh-download tests..." -run_timed "mesh-download: install-dev" make -C ./lambdas/mesh-download install-dev -run_timed "mesh-download: coverage" make -C ./lambdas/mesh-download coverage - -# Python Lambda - report-sender -echo "Setting up and running report-sender tests..." -run_timed "report-sender: install-dev" make -C ./lambdas/report-sender install-dev -run_timed "report-sender: coverage" make -C ./lambdas/report-sender coverage +# Python projects - run all install-dev steps sequentially (they share the same +# pip environment so cannot be parallelised), then run all coverage (pytest) +# steps in parallel since each writes to its own isolated output directory. + +# ---- Phase 1: install all Python dev dependencies (sequential, shared pip env) ---- +echo "Installing Python dev dependencies..." +_python_projects=( + ./src/asyncapigenerator + ./src/cloudeventjekylldocs + ./src/eventcatalogasyncapiimporter + ./utils/py-utils + ./src/python-schema-generator + ./lambdas/mesh-acknowledge + ./lambdas/mesh-poll + ./lambdas/mesh-download + ./lambdas/report-sender +) +for proj in "${_python_projects[@]}"; do + run_timed "${proj}: install-dev" make -C "$proj" install-dev +done + +# ---- Phase 2: run all coverage steps in parallel ---- +# Each job writes output to a temp file; we print them sequentially on +# completion so the log is readable. Non-zero exit codes are all collected and +# the script fails at the end if any job failed. +echo "Running Python coverage in parallel..." + +_py_pids=() +_py_labels=() +_py_logs=() +_py_exits=() + +for proj in "${_python_projects[@]}"; do + label="${proj}: coverage" + logfile=$(mktemp) + make -C "$proj" coverage >"$logfile" 2>&1 & + _py_pids+=("$!") + _py_labels+=("$label") + _py_logs+=("$logfile") +done + +# Collect results in launch order (preserves deterministic output) +_py_start=$(date +%s) +for i in "${!_py_pids[@]}"; do + wait "${_py_pids[$i]}" + _py_exits+=("$?") + echo "" + echo "--- ${_py_labels[$i]} ---" + cat "${_py_logs[$i]}" + rm -f "${_py_logs[$i]}" +done +_py_end=$(date +%s) +_timer_labels+=("Python coverage (parallel)") +_timer_seconds+=("$((_py_end - _py_start))") # merge coverage reports run_timed "lcov-result-merger" \ @@ -117,3 +127,11 @@ if [ "${jest_exit:-0}" -ne 0 ]; then echo "Jest tests failed with exit code ${jest_exit}" exit "${jest_exit}" fi + +# Propagate any Python coverage failure +for i in "${!_py_exits[@]}"; do + if [ "${_py_exits[$i]}" -ne 0 ]; then + echo "${_py_labels[$i]} failed with exit code ${_py_exits[$i]}" + exit "${_py_exits[$i]}" + fi +done diff --git a/src/asyncapigenerator/pytest.ini b/src/asyncapigenerator/pytest.ini index 94bd46ada..fcdb5a3fd 100644 --- a/src/asyncapigenerator/pytest.ini +++ b/src/asyncapigenerator/pytest.ini @@ -19,6 +19,7 @@ markers = [coverage:run] relative_files = True +data_file = src/asyncapigenerator/.coverage omit = */tests/* */test_*.py diff --git a/src/cloudeventjekylldocs/pytest.ini b/src/cloudeventjekylldocs/pytest.ini index dba4156ce..b79a593f4 100644 --- a/src/cloudeventjekylldocs/pytest.ini +++ b/src/cloudeventjekylldocs/pytest.ini @@ -18,6 +18,7 @@ markers = [coverage:run] relative_files = True +data_file = src/cloudeventjekylldocs/.coverage omit = */tests/* */test_*.py diff --git a/src/eventcatalogasyncapiimporter/pytest.ini b/src/eventcatalogasyncapiimporter/pytest.ini index 0b2a45512..41b39ff49 100644 --- a/src/eventcatalogasyncapiimporter/pytest.ini +++ b/src/eventcatalogasyncapiimporter/pytest.ini @@ -20,6 +20,7 @@ testpaths = tests [coverage:run] relative_files = True +data_file = src/eventcatalogasyncapiimporter/.coverage omit = */tests/* */test_*.py diff --git a/src/python-schema-generator/pytest.ini b/src/python-schema-generator/pytest.ini index 0d63d6be4..001fba94a 100644 --- a/src/python-schema-generator/pytest.ini +++ b/src/python-schema-generator/pytest.ini @@ -9,6 +9,7 @@ addopts = [coverage:run] relative_files = True +data_file = src/python-schema-generator/.coverage omit = */tests/* */test_*.py diff --git a/utils/py-utils/pytest.ini b/utils/py-utils/pytest.ini index f704cd777..b5bbd23b4 100644 --- a/utils/py-utils/pytest.ini +++ b/utils/py-utils/pytest.ini @@ -7,6 +7,7 @@ addopts = -v --tb=short [coverage:run] relative_files = True +data_file = utils/py-utils/.coverage omit = */dl_utils/__tests__/* */test_*.py From b6dadac7a67aafbced5581e4cb460b9c667a6fd3 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Fri, 20 Mar 2026 12:10:01 +0000 Subject: [PATCH 05/17] CCM-14615: more parrallels --- src/cloudevents/domains/common.mk | 32 +++++++++++-------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/cloudevents/domains/common.mk b/src/cloudevents/domains/common.mk index e96dcd34e..e48919191 100644 --- a/src/cloudevents/domains/common.mk +++ b/src/cloudevents/domains/common.mk @@ -56,31 +56,23 @@ build-no-bundle: @echo "Building $(DOMAIN) schemas to output/..." @if [ -n "$(PROFILE_NAMES)" ]; then \ echo "Building profile schemas..."; \ - for schema in $(PROFILE_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/$$schema.schema.yaml $(OUTPUT_DIR) || exit 1; \ - done; \ + printf '%s\n' $(PROFILE_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/{}.schema.yaml $(OUTPUT_DIR) || exit 1'; \ fi @if [ -n "$(DEFS_NAMES)" ]; then \ echo "Building defs schemas..."; \ - for schema in $(DEFS_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/defs/$$schema.yaml $(OUTPUT_DIR)/defs || exit 1; \ - done; \ + printf '%s\n' $(DEFS_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/defs/{}.yaml $(OUTPUT_DIR)/defs || exit 1'; \ fi @if [ -n "$(DATA_NAMES)" ]; then \ echo "Building data schemas..."; \ - for schema in $(DATA_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/data/$$schema.yaml $(OUTPUT_DIR)/data || exit 1; \ - done; \ + printf '%s\n' $(DATA_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/data/{}.yaml $(OUTPUT_DIR)/data || exit 1'; \ fi @if [ -n "$(EVENT_NAMES)" ]; then \ echo "Building event schemas..."; \ - for schema in $(EVENT_NAMES); do \ - echo " - $$schema"; \ - cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/events/$$schema.schema.yaml $(OUTPUT_DIR)/events || exit 1; \ - done; \ + printf '%s\n' $(EVENT_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run build -- --root-dir $(ROOT_DIR) $(SRC_DIR)/events/{}.schema.yaml $(OUTPUT_DIR)/events || exit 1'; \ fi publish-json: @@ -138,11 +130,9 @@ publish-json: publish-bundled-json: @if [ -n "$(EVENT_NAMES)" ]; then \ - @echo "Flattening published event schemas..."; \ - for schema in $(EVENT_NAMES); do \ - echo " - $$schema (flatten)"; \ - cd $(CLOUD_EVENTS_DIR) && npm run bundle -- --flatten --root-dir $(ROOT_DIR) --base-url $(SCHEMA_BASE_URL) $(OUTPUT_DIR)/events/$$schema.schema.json $(SCHEMAS_DIR)/events/$$schema.flattened.schema.json || exit 1; \ - done; \ + echo "Flattening published event schemas..."; \ + printf '%s\n' $(EVENT_NAMES) | xargs -P 0 -I{} sh -c \ + 'cd $(CLOUD_EVENTS_DIR) && npm run bundle -- --flatten --root-dir $(ROOT_DIR) --base-url $(SCHEMA_BASE_URL) $(OUTPUT_DIR)/events/{}.schema.json $(SCHEMAS_DIR)/events/{}.flattened.schema.json || exit 1'; \ fi publish-yaml: From c47ac23c66c22faa1dae091ee56bcf5857ada0f5 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Mon, 23 Mar 2026 13:32:46 +0000 Subject: [PATCH 06/17] CCM-14615: clean ups --- .github/workflows/temp-unit-tests-only.yaml | 65 - .gitignore | 3 + jest.config.cjs | 1322 +++++++++++++++-- .../__tests__/app/notify-api-client.test.ts | 3 - .../src/__tests__/domain/mapper.test.ts | 3 - package.json | 2 +- scripts/generate-parallel-jest-config.ts | 153 ++ scripts/tests/unit.sh | 54 +- src/cloudevents/jest.config.cjs | 57 - src/cloudevents/jest.config.ts | 57 + 10 files changed, 1427 insertions(+), 292 deletions(-) delete mode 100644 .github/workflows/temp-unit-tests-only.yaml create mode 100644 scripts/generate-parallel-jest-config.ts delete mode 100644 src/cloudevents/jest.config.cjs create mode 100644 src/cloudevents/jest.config.ts diff --git a/.github/workflows/temp-unit-tests-only.yaml b/.github/workflows/temp-unit-tests-only.yaml deleted file mode 100644 index fc790c360..000000000 --- a/.github/workflows/temp-unit-tests-only.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: "Temp unit tests only" - -on: - workflow_dispatch: - inputs: - nodejs_version: - description: "Node.js version, set by the CI/CD pipeline workflow" - required: true - type: string - python_version: - description: "Python version, set by the CI/CD pipeline workflow" - required: true - type: string - push: - branches: - - feature/CCM-14615_unit-test-quickening - -env: - AWS_REGION: eu-west-2 - TERM: xterm-256color - -jobs: - test-unit: - name: "Unit tests" - runs-on: ubuntu-latest - timeout-minutes: 7 - permissions: - contents: read - packages: read - steps: - - name: "Checkout code" - uses: actions/checkout@v5 - - uses: ./.github/actions/node-install - with: - node-version: ${{ inputs.nodejs_version }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: "Setup Python" - uses: actions/setup-python@v6 - with: - python-version: ${{ inputs.python_version }} - cache: 'pip' - cache-dependency-path: '**/requirements*.txt' - - name: "Run unit test suite" - run: | - make test-unit - - name: "Save the result of fast test suite" - uses: actions/upload-artifact@v4 - with: - name: unit-tests - path: "**/.reports/unit" - include-hidden-files: true - if: always() - - name: "Save the result of code coverage" - uses: actions/upload-artifact@v4 - with: - name: code-coverage-report - path: ".reports/lcov.info" - - name: "Save Python coverage reports" - uses: actions/upload-artifact@v4 - with: - name: python-coverage-reports - path: | - src/**/coverage.xml - utils/**/coverage.xml - lambdas/**/coverage.xml diff --git a/.gitignore b/.gitignore index cf7d4b6ab..8732a35f1 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ coverage-*/ **/playwright-report **/test-results plugin-cache + +# Generated by npm run test:unit:parallel — do not commit +jest.config.cjs diff --git a/jest.config.cjs b/jest.config.cjs index 07f9b7230..81b984e24 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,151 +1,1233 @@ /** - * Root Jest config — runs all TypeScript/JavaScript workspace test suites in + * Root Jest config — runs all TypeScript workspace test suites in * parallel via Jest's native `projects` support. * - * Written as CJS (.cjs) so Jest can load it without needing a root tsconfig.json. - * The base config is inlined from jest.config.base.ts to keep this file - * self-contained and avoid any TypeScript compilation at load time, which would - * require a root tsconfig.json and risk interfering with workspace ts-jest runs. + * ⚠️ THIS FILE IS AUTO-GENERATED. Do not edit it directly. * - * When jest.config.base.ts changes, this file must be kept in sync manually. - * - * Each project's rootDir is set to its workspace directory so that relative - * paths (coverageDirectory, HTML reporter outputPath, etc.) resolve relative - * to the workspace, not the repo root. - * - * Note: src/cloudevents has a hand-rolled jest.config.cjs; it is included via - * its directory path so Jest discovers that file directly. - * - * Note: src/digital-letters-events and tests/playwright have no Jest tests - * and are intentionally excluded. + * Generated by scripts/generate-parallel-jest-config.ts */ -const base = { - preset: 'ts-jest', - clearMocks: true, - collectCoverage: true, - collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', - '!src/**/*.d.ts', - '!src/**/__tests__/**', - '!src/**/*.test.{ts,tsx}', - '!src/**/*.spec.{ts,tsx}', - ], - coverageDirectory: './.reports/unit/coverage', - coverageProvider: 'babel', - coverageThreshold: { - global: { branches: 100, functions: 100, lines: 100, statements: -10 }, - }, - coveragePathIgnorePatterns: ['/__tests__/'], - transform: { '^.+\\.ts$': 'ts-jest' }, - testPathIgnorePatterns: ['.build'], - testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], - reporters: [ - 'default', - [ - 'jest-html-reporter', - { - pageTitle: 'Test Report', - outputPath: './.reports/unit/test-report.html', - includeFailureMsg: true, - }, - ], - ], - testEnvironment: 'node', - moduleDirectories: ['node_modules', 'src'], -}; - -// Workspaces that use the base config with no overrides -const standardWorkspaces = [ - 'lambdas/file-scanner-lambda', - 'lambdas/key-generation', - 'lambdas/refresh-apim-access-token', - 'lambdas/pdm-mock-lambda', - 'lambdas/pdm-poll-lambda', - 'lambdas/ttl-create-lambda', - 'lambdas/ttl-handle-expiry-lambda', - 'lambdas/ttl-poll-lambda', - 'lambdas/pdm-uploader-lambda', - 'lambdas/print-sender-lambda', - 'lambdas/print-analyser', - 'lambdas/report-scheduler', - 'lambdas/report-event-transformer', - 'lambdas/move-scanned-files-lambda', - 'lambdas/report-generator', - 'utils/sender-management', -]; - /** @type {import('jest').Config} */ module.exports = { projects: [ - // Standard workspaces — no overrides - ...standardWorkspaces.map((ws) => ({ - ...base, - rootDir: `/${ws}`, - displayName: ws, - })), + // lambdas/file-scanner-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/file-scanner-lambda", + "displayName": "lambdas/file-scanner-lambda", + }, + + // lambdas/key-generation + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + "lambda.ts", + "/config.ts", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/key-generation", + "displayName": "lambdas/key-generation", + }, + + // lambdas/refresh-apim-access-token + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 90, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + "cli.ts", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/refresh-apim-access-token", + "displayName": "lambdas/refresh-apim-access-token", + }, + + // lambdas/pdm-mock-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 90, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/pdm-mock-lambda", + "displayName": "lambdas/pdm-mock-lambda", + }, + + // lambdas/pdm-poll-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/pdm-poll-lambda", + "displayName": "lambdas/pdm-poll-lambda", + }, + + // lambdas/ttl-create-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/ttl-create-lambda", + "displayName": "lambdas/ttl-create-lambda", + }, + + // lambdas/ttl-handle-expiry-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/ttl-handle-expiry-lambda", + "displayName": "lambdas/ttl-handle-expiry-lambda", + }, + + // lambdas/ttl-poll-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/ttl-poll-lambda", + "displayName": "lambdas/ttl-poll-lambda", + }, + + // lambdas/pdm-uploader-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/pdm-uploader-lambda", + "displayName": "lambdas/pdm-uploader-lambda", + }, + + // lambdas/print-sender-lambda + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/print-sender-lambda", + "displayName": "lambdas/print-sender-lambda", + }, - // utils/utils — relaxed coverage thresholds + exclude index.ts + // lambdas/core-notifier-lambda { - ...base, - rootDir: '/utils/utils', - displayName: 'utils/utils', - coverageThreshold: { - global: { branches: 85, functions: 85, lines: 85, statements: -10 }, + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", }, - coveragePathIgnorePatterns: [...base.coveragePathIgnorePatterns, 'index.ts'], + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/core-notifier-lambda", + "displayName": "lambdas/core-notifier-lambda", }, - // lambdas/core-notifier-lambda — moduleNameMapper unifies `crypto` and - // `node:crypto` in Jest's registry so that jest.mock('node:crypto') in the - // test files also intercepts the bare require('crypto') call made by - // node-jose at module-load time, preventing an undefined helpers.nodeCrypto - // crash in ecdsa.js. + // lambdas/print-status-handler { - ...base, - rootDir: '/lambdas/core-notifier-lambda', - displayName: 'lambdas/core-notifier-lambda', + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "transformIgnorePatterns": [ + "node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)", + ], + "rootDir": "/lambdas/print-status-handler", + "displayName": "lambdas/print-status-handler", + }, + + // lambdas/print-analyser + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/print-analyser", + "displayName": "lambdas/print-analyser", }, - // lambdas/print-status-handler — @nhsdigital/nhs-notify-event-schemas-supplier-api - // ships ESM source; it must be transformed by ts-jest rather than skipped. + // lambdas/report-scheduler { - ...base, - rootDir: '/lambdas/print-status-handler', - displayName: 'lambdas/print-status-handler', - transformIgnorePatterns: [ - 'node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)', + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/report-scheduler", + "displayName": "lambdas/report-scheduler", }, - // src/python-schema-generator — excludes merge-allof CLI entry point + // lambdas/report-event-transformer { - ...base, - rootDir: '/src/python-schema-generator', - displayName: 'src/python-schema-generator', - coveragePathIgnorePatterns: [ - ...base.coveragePathIgnorePatterns, - 'src/merge-allof-cli.ts', + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/report-event-transformer", + "displayName": "lambdas/report-event-transformer", }, - // src/typescript-schema-generator — excludes CLI entry points. - // Requires --experimental-vm-modules (set via NODE_OPTIONS in the - // test:unit:parallel script) because json-schema-to-typescript uses a - // dynamic import() of prettier at runtime, which Node.js rejects inside a - // Jest VM context without the flag. + // lambdas/move-scanned-files-lambda { - ...base, - rootDir: '/src/typescript-schema-generator', - displayName: 'src/typescript-schema-generator', - coveragePathIgnorePatterns: [ - ...base.coveragePathIgnorePatterns, - 'src/generate-types-cli.ts', - 'src/generate-validators-cli.ts', + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/move-scanned-files-lambda", + "displayName": "lambdas/move-scanned-files-lambda", }, - // src/cloudevents — uses its own jest.config.cjs (hand-rolled ts-jest options) - '/src/cloudevents', + // lambdas/report-generator + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/lambdas/report-generator", + "displayName": "lambdas/report-generator", + }, + + // utils/utils + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 85, + "functions": 85, + "lines": 85, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + "index.ts", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/utils/utils", + "displayName": "utils/utils", + }, + + // utils/sender-management + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 84, + "functions": 91, + "lines": 90, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/utils/sender-management", + "displayName": "utils/sender-management", + }, + + // src/cloudevents + { + "preset": "ts-jest", + "testEnvironment": "node", + "roots": [ + "", + ], + "testMatch": [ + "**/__tests__/**/*.ts", + "**/?(*.)+(spec|test).ts", + ], + "transform": { + "^.+\\.ts$": [ + "ts-jest", + { + "tsconfig": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "module": "commonjs", + "target": "ES2020", + "moduleResolution": "node", + "noEmit": true, + }, + "diagnostics": { + "ignoreCodes": [ + 1343, + ], + }, + }, + ], + }, + "collectCoverageFrom": [ + "tools/**/*.{ts,js,cjs}", + "!tools/**/*.d.ts", + "!tools/**/__tests__/**", + "!tools/**/*.test.ts", + "!tools/**/*.spec.ts", + "!tools/builder/build-schema.ts", + "!tools/generator/generate-example.ts", + "!tools/generator/manual-bundle-schema.ts", + "!tools/validator/validate.ts", + ], + "coverageDirectory": "coverage", + "coverageReporters": [ + "text", + "lcov", + "html", + "cobertura", + ], + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/__tests__/", + ], + "coverageThreshold": { + "global": { + "branches": 60, + "functions": 60, + "lines": 60, + "statements": 60, + }, + }, + "moduleFileExtensions": [ + "ts", + "js", + "json", + ], + "moduleNameMapper": { + "^(.*)\\.ts$": "$1", + }, + "verbose": true, + "testTimeout": 10000, + "rootDir": "/src/cloudevents", + "displayName": "src/cloudevents", + }, + + // src/python-schema-generator + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + "src/merge-allof-cli.ts", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/src/python-schema-generator", + "displayName": "src/python-schema-generator", + }, + + // src/typescript-schema-generator + { + "preset": "ts-jest", + "clearMocks": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", + ], + "coverageDirectory": "./.reports/unit/coverage", + "coverageProvider": "babel", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": -10, + }, + }, + "coveragePathIgnorePatterns": [ + "/__tests__/", + "src/generate-types-cli.ts", + "src/generate-validators-cli.ts", + ], + "transform": { + "^.+\\.ts$": "ts-jest", + }, + "testPathIgnorePatterns": [ + ".build", + ], + "testMatch": [ + "**/?(*.)+(spec|test).[jt]s?(x)", + ], + "reporters": [ + "default", + [ + "jest-html-reporter", + { + "pageTitle": "Test Report", + "outputPath": "./.reports/unit/test-report.html", + "includeFailureMsg": true, + }, + ], + ], + "testEnvironment": "node", + "moduleDirectories": [ + "node_modules", + "src", + ], + "rootDir": "/src/typescript-schema-generator", + "displayName": "src/typescript-schema-generator", + } ], }; diff --git a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts index 80077f41e..e84eb293a 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/app/notify-api-client.test.ts @@ -12,9 +12,6 @@ import { IAccessTokenRepository, NotifyClient } from 'app/notify-api-client'; import { RequestAlreadyReceivedError } from 'domain/request-already-received-error'; jest.mock('utils'); -// Use a partial manual mock so that node-jose's require('crypto') still gets -// the real crypto implementation (needed for getHashes() etc.) while -// randomUUID is replaced with a jest.fn() for test control. jest.mock('node:crypto', () => ({ ...jest.requireActual('node:crypto'), randomUUID: jest.fn(), diff --git a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts index cf2c37fd9..dfb1d3bfa 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/domain/mapper.test.ts @@ -9,9 +9,6 @@ import { PDMResourceAvailable } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; jest.mock('utils'); -// Use a partial manual mock so that node-jose's require('crypto') still gets -// the real crypto implementation (needed for getHashes() etc.) while -// randomUUID is replaced with a jest.fn() for test control. jest.mock('node:crypto', () => ({ ...jest.requireActual('node:crypto'), randomUUID: jest.fn(), diff --git a/package.json b/package.json index 81b845389..459fe5fec 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "lint:fix": "npm run lint:fix --workspaces", "start": "npm run start --workspace frontend", "test:unit": "npm run test:unit --workspaces", - "test:unit:parallel": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --config jest.config.cjs", + "test:unit:parallel": "tsx scripts/generate-parallel-jest-config.ts && cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --config jest.config.cjs", "typecheck": "npm run typecheck --workspaces" }, "version": "0.0.1", diff --git a/scripts/generate-parallel-jest-config.ts b/scripts/generate-parallel-jest-config.ts new file mode 100644 index 000000000..ea7fe7714 --- /dev/null +++ b/scripts/generate-parallel-jest-config.ts @@ -0,0 +1,153 @@ +/** + * Generates jest.config.cjs from the individual workspace jest.config.ts files. + */ + +import { execFileSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +const repoRoot = path.resolve(__dirname, '..'); +const outputPath = path.join(repoRoot, 'jest.config.cjs'); + +interface PackageJson { + workspaces?: string[]; +} + +const rootPkg = JSON.parse( + fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), +) as PackageJson; + +const workspaces: string[] = rootPkg.workspaces ?? []; + +/** + * Inline TypeScript written to a temp .ts file and executed by tsx so each + * workspace jest.config.ts is evaluated in an isolated Node process with a + * fresh module registry (preventing shared mutable baseJestConfig state). + */ +const EVALUATOR = (configPath: string): string => ` +import config from ${JSON.stringify(configPath)}; +process.stdout.write(JSON.stringify(config)); +`; + +/** Serialise a plain JS value to source code, indented with the given prefix. */ +function serialise(value: unknown, indent = ' '): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'string') return JSON.stringify(value); + if (typeof value === 'number' || typeof value === 'boolean') + return String(value); + + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + const items = value + .map((v) => `${indent} ${serialise(v, indent + ' ')}`) + .join(',\n'); + return `[\n${items},\n${indent}]`; + } + + if (typeof value === 'object') { + const entries = Object.entries(value as Record).filter( + ([, v]) => v !== undefined, + ); + if (entries.length === 0) return '{}'; + const lines = entries + .map( + ([k, v]) => + `${indent} ${JSON.stringify(k)}: ${serialise(v, indent + ' ')}`, + ) + .join(',\n'); + return `{\n${lines},\n${indent}}`; + } + + return String(value); +} + +interface ProjectEntry { + workspace: string; + config: Record; +} + +function main(): void { + const projects: ProjectEntry[] = []; + + for (const ws of workspaces) { + const wsDir = path.join(repoRoot, ws); + + const hasCjs = fs.existsSync(path.join(wsDir, 'jest.config.cjs')); + const hasTs = fs.existsSync(path.join(wsDir, 'jest.config.ts')); + + if (hasCjs && !hasTs) { + throw new Error( + `${ws} has jest.config.cjs but no jest.config.ts. ` + + `Migrate it to jest.config.ts so the generator can handle it uniformly.`, + ); + } + + if (!hasTs) { + // No Jest config → no Jest tests (e.g. src/digital-letters-events) + continue; + } + + // Evaluate the workspace config in an isolated tsx subprocess so that the + // shared mutable `baseJestConfig` object is freshly initialised for every + // workspace. Dynamic import() in the parent process would share the cached + // module instance and accumulate mutations. + const configPath = path.join(wsDir, 'jest.config.ts'); + const tsxBin = path.join(repoRoot, 'node_modules', '.bin', 'tsx'); + const tmpFile = path.join(os.tmpdir(), `jest-config-eval-${Date.now()}.ts`); + let json: string; + try { + fs.writeFileSync(tmpFile, EVALUATOR(configPath), 'utf8'); + json = execFileSync(tsxBin, [tmpFile], { + cwd: repoRoot, + encoding: 'utf8', + }); + } finally { + fs.rmSync(tmpFile, { force: true }); + } + const wsConfig = JSON.parse(json) as Record; + + // Inject rootDir and displayName. Jest resolves all relative paths inside a + // project entry relative to that project's rootDir. + const entry: Record = { + ...wsConfig, + rootDir: `/${ws}`, + displayName: ws, + }; + + projects.push({ workspace: ws, config: entry }); + } + + // Build the projects array source + const projectLines = projects.map((p) => { + const body = serialise(p.config, ' '); + return ` // ${p.workspace}\n ${body}`; + }); + + const banner = `/** + * Root Jest config — runs all TypeScript workspace test suites in + * parallel via Jest's native \`projects\` support. + * + * ⚠️ THIS FILE IS AUTO-GENERATED. Do not edit it directly. + * + * Generated by scripts/generate-parallel-jest-config.ts + */ + +/** @type {import('jest').Config} */ +module.exports = { + projects: [ +${projectLines.join(',\n\n')} + ], +}; +`; + + fs.writeFileSync(outputPath, banner, 'utf8'); + console.log(`Written: ${path.relative(repoRoot, outputPath)}`); + console.log(` ${projects.length} project(s) included`); + for (const p of projects) { + console.log(` ${p.workspace}`); + } +} + +main(); diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 63a24fd38..d671e0d18 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -4,21 +4,6 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)" -# This file is for you! Edit it to call your unit test suite. Note that the same -# file will be called if you run it locally as if you run it on CI. - -# Replace the following line with something like: -# -# rails test:unit -# python manage.py test -# npm run test -# -# or whatever is appropriate to your project. You should *only* run your fast -# tests from here. If you want to run other test suites, see the predefined -# tasks in scripts/test.mk. - -# Timing helpers — records wall-clock duration for each labelled step and prints -# a summary table at exit so it's easy to see where the time is going. _timer_labels=() _timer_seconds=() @@ -51,43 +36,26 @@ print_timing_summary() { trap print_timing_summary EXIT -# run tests - -# TypeScript/JavaScript projects (npm workspace) -# Runs all Jest workspaces in parallel via the root jest.config.cjs projects -# config, which is faster than sequential `npm run test:unit --workspaces`. -# Note: src/cloudevents is included in the projects list in jest.config.cjs. -# Use || to capture any Jest failure so that Python tests always run; the exit -# code is propagated at the end of the script. run_timed "npm ci" npm ci run_timed "npm run generate-dependencies" npm run generate-dependencies -run_timed "npm run test:unit:parallel" npm run test:unit:parallel || jest_exit=$? - -# Python projects - run all install-dev steps sequentially (they share the same -# pip environment so cannot be parallelised), then run all coverage (pytest) -# steps in parallel since each writes to its own isolated output directory. +run_timed "Node unit tests (parallel)" npm run test:unit:parallel || jest_exit=$? -# ---- Phase 1: install all Python dev dependencies (sequential, shared pip env) ---- +# ---- Phase 1: install all Python dev dependencies (sequential) ---- +# Discover Python projects dynamically: any directory under src/, utils/, or +# lambdas/ whose Makefile defines both an `install-dev` target (Python deps) +# and a `coverage` target (pytest). This avoids maintaining a hardcoded list. echo "Installing Python dev dependencies..." -_python_projects=( - ./src/asyncapigenerator - ./src/cloudeventjekylldocs - ./src/eventcatalogasyncapiimporter - ./utils/py-utils - ./src/python-schema-generator - ./lambdas/mesh-acknowledge - ./lambdas/mesh-poll - ./lambdas/mesh-download - ./lambdas/report-sender +mapfile -t _python_projects < <( + grep -rl "^install-dev:" src/ utils/ lambdas/ --include="Makefile" 2>/dev/null \ + | xargs grep -l "^coverage:" \ + | xargs -I{} dirname {} \ + | sort ) for proj in "${_python_projects[@]}"; do run_timed "${proj}: install-dev" make -C "$proj" install-dev done # ---- Phase 2: run all coverage steps in parallel ---- -# Each job writes output to a temp file; we print them sequentially on -# completion so the log is readable. Non-zero exit codes are all collected and -# the script fails at the end if any job failed. echo "Running Python coverage in parallel..." _py_pids=() @@ -115,7 +83,7 @@ for i in "${!_py_pids[@]}"; do rm -f "${_py_logs[$i]}" done _py_end=$(date +%s) -_timer_labels+=("Python coverage (parallel)") +_timer_labels+=("Python unit tests (parallel)") _timer_seconds+=("$((_py_end - _py_start))") # merge coverage reports diff --git a/src/cloudevents/jest.config.cjs b/src/cloudevents/jest.config.cjs deleted file mode 100644 index f40450410..000000000 --- a/src/cloudevents/jest.config.cjs +++ /dev/null @@ -1,57 +0,0 @@ -/** @type {import('jest').Config} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: [''], - testMatch: [ - '**/__tests__/**/*.ts', - '**/?(*.)+(spec|test).ts' - ], - transform: { - '^.+\\.ts$': ['ts-jest', { - tsconfig: { - esModuleInterop: true, - allowSyntheticDefaultImports: true, - allowImportingTsExtensions: true, - module: 'commonjs', - target: 'ES2020', - moduleResolution: 'node', - noEmit: true - }, - diagnostics: { - ignoreCodes: [1343] // Ignore TS1343: import.meta errors - } - }] - }, - collectCoverageFrom: [ - 'tools/**/*.{ts,js,cjs}', - '!tools/**/*.d.ts', - '!tools/**/__tests__/**', - '!tools/**/*.test.ts', - '!tools/**/*.spec.ts', - '!tools/builder/build-schema.ts', - '!tools/generator/generate-example.ts', - '!tools/generator/manual-bundle-schema.ts', - '!tools/validator/validate.ts' - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html', 'cobertura'], - coveragePathIgnorePatterns: [ - '/node_modules/', - '/__tests__/' - ], - coverageThreshold: { - global: { - branches: 60, - functions: 60, - lines: 60, - statements: 60 - } - }, - moduleFileExtensions: ['ts', 'js', 'json'], - moduleNameMapper: { - '^(.*)\\.ts$': '$1', - }, - verbose: true, - testTimeout: 10000 -}; diff --git a/src/cloudevents/jest.config.ts b/src/cloudevents/jest.config.ts new file mode 100644 index 000000000..d03dd4d4d --- /dev/null +++ b/src/cloudevents/jest.config.ts @@ -0,0 +1,57 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + allowImportingTsExtensions: true, + module: 'commonjs', + target: 'ES2020', + moduleResolution: 'node', + noEmit: true, + }, + diagnostics: { + ignoreCodes: [1343], // Ignore TS1343: import.meta errors + }, + }, + ], + }, + collectCoverageFrom: [ + 'tools/**/*.{ts,js,cjs}', + '!tools/**/*.d.ts', + '!tools/**/__tests__/**', + '!tools/**/*.test.ts', + '!tools/**/*.spec.ts', + '!tools/builder/build-schema.ts', + '!tools/generator/generate-example.ts', + '!tools/generator/manual-bundle-schema.ts', + '!tools/validator/validate.ts', + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html', 'cobertura'], + coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], + coverageThreshold: { + global: { + branches: 60, + functions: 60, + lines: 60, + statements: 60, + }, + }, + moduleFileExtensions: ['ts', 'js', 'json'], + moduleNameMapper: { + '^(.*)\\.ts$': '$1', + }, + verbose: true, + testTimeout: 10000, +}; + +export default config; From 2a54862b18d7da86b071210abbde85b8b958b699 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Mon, 23 Mar 2026 13:33:54 +0000 Subject: [PATCH 07/17] CCM-14615: clean ups --- jest.config.cjs | 1233 ----------------------------------------------- 1 file changed, 1233 deletions(-) delete mode 100644 jest.config.cjs diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index 81b984e24..000000000 --- a/jest.config.cjs +++ /dev/null @@ -1,1233 +0,0 @@ -/** - * Root Jest config — runs all TypeScript workspace test suites in - * parallel via Jest's native `projects` support. - * - * ⚠️ THIS FILE IS AUTO-GENERATED. Do not edit it directly. - * - * Generated by scripts/generate-parallel-jest-config.ts - */ - -/** @type {import('jest').Config} */ -module.exports = { - projects: [ - // lambdas/file-scanner-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/file-scanner-lambda", - "displayName": "lambdas/file-scanner-lambda", - }, - - // lambdas/key-generation - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - "lambda.ts", - "/config.ts", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/key-generation", - "displayName": "lambdas/key-generation", - }, - - // lambdas/refresh-apim-access-token - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 90, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - "cli.ts", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/refresh-apim-access-token", - "displayName": "lambdas/refresh-apim-access-token", - }, - - // lambdas/pdm-mock-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 90, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/pdm-mock-lambda", - "displayName": "lambdas/pdm-mock-lambda", - }, - - // lambdas/pdm-poll-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/pdm-poll-lambda", - "displayName": "lambdas/pdm-poll-lambda", - }, - - // lambdas/ttl-create-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/ttl-create-lambda", - "displayName": "lambdas/ttl-create-lambda", - }, - - // lambdas/ttl-handle-expiry-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/ttl-handle-expiry-lambda", - "displayName": "lambdas/ttl-handle-expiry-lambda", - }, - - // lambdas/ttl-poll-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/ttl-poll-lambda", - "displayName": "lambdas/ttl-poll-lambda", - }, - - // lambdas/pdm-uploader-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/pdm-uploader-lambda", - "displayName": "lambdas/pdm-uploader-lambda", - }, - - // lambdas/print-sender-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/print-sender-lambda", - "displayName": "lambdas/print-sender-lambda", - }, - - // lambdas/core-notifier-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/core-notifier-lambda", - "displayName": "lambdas/core-notifier-lambda", - }, - - // lambdas/print-status-handler - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "transformIgnorePatterns": [ - "node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)", - ], - "rootDir": "/lambdas/print-status-handler", - "displayName": "lambdas/print-status-handler", - }, - - // lambdas/print-analyser - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/print-analyser", - "displayName": "lambdas/print-analyser", - }, - - // lambdas/report-scheduler - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/report-scheduler", - "displayName": "lambdas/report-scheduler", - }, - - // lambdas/report-event-transformer - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/report-event-transformer", - "displayName": "lambdas/report-event-transformer", - }, - - // lambdas/move-scanned-files-lambda - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/move-scanned-files-lambda", - "displayName": "lambdas/move-scanned-files-lambda", - }, - - // lambdas/report-generator - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/lambdas/report-generator", - "displayName": "lambdas/report-generator", - }, - - // utils/utils - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 85, - "functions": 85, - "lines": 85, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - "index.ts", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/utils/utils", - "displayName": "utils/utils", - }, - - // utils/sender-management - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 84, - "functions": 91, - "lines": 90, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/utils/sender-management", - "displayName": "utils/sender-management", - }, - - // src/cloudevents - { - "preset": "ts-jest", - "testEnvironment": "node", - "roots": [ - "", - ], - "testMatch": [ - "**/__tests__/**/*.ts", - "**/?(*.)+(spec|test).ts", - ], - "transform": { - "^.+\\.ts$": [ - "ts-jest", - { - "tsconfig": { - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "allowImportingTsExtensions": true, - "module": "commonjs", - "target": "ES2020", - "moduleResolution": "node", - "noEmit": true, - }, - "diagnostics": { - "ignoreCodes": [ - 1343, - ], - }, - }, - ], - }, - "collectCoverageFrom": [ - "tools/**/*.{ts,js,cjs}", - "!tools/**/*.d.ts", - "!tools/**/__tests__/**", - "!tools/**/*.test.ts", - "!tools/**/*.spec.ts", - "!tools/builder/build-schema.ts", - "!tools/generator/generate-example.ts", - "!tools/generator/manual-bundle-schema.ts", - "!tools/validator/validate.ts", - ], - "coverageDirectory": "coverage", - "coverageReporters": [ - "text", - "lcov", - "html", - "cobertura", - ], - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/__tests__/", - ], - "coverageThreshold": { - "global": { - "branches": 60, - "functions": 60, - "lines": 60, - "statements": 60, - }, - }, - "moduleFileExtensions": [ - "ts", - "js", - "json", - ], - "moduleNameMapper": { - "^(.*)\\.ts$": "$1", - }, - "verbose": true, - "testTimeout": 10000, - "rootDir": "/src/cloudevents", - "displayName": "src/cloudevents", - }, - - // src/python-schema-generator - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - "src/merge-allof-cli.ts", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/src/python-schema-generator", - "displayName": "src/python-schema-generator", - }, - - // src/typescript-schema-generator - { - "preset": "ts-jest", - "clearMocks": true, - "collectCoverage": true, - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!src/**/__tests__/**", - "!src/**/*.test.{ts,tsx}", - "!src/**/*.spec.{ts,tsx}", - ], - "coverageDirectory": "./.reports/unit/coverage", - "coverageProvider": "babel", - "coverageThreshold": { - "global": { - "branches": 100, - "functions": 100, - "lines": 100, - "statements": -10, - }, - }, - "coveragePathIgnorePatterns": [ - "/__tests__/", - "src/generate-types-cli.ts", - "src/generate-validators-cli.ts", - ], - "transform": { - "^.+\\.ts$": "ts-jest", - }, - "testPathIgnorePatterns": [ - ".build", - ], - "testMatch": [ - "**/?(*.)+(spec|test).[jt]s?(x)", - ], - "reporters": [ - "default", - [ - "jest-html-reporter", - { - "pageTitle": "Test Report", - "outputPath": "./.reports/unit/test-report.html", - "includeFailureMsg": true, - }, - ], - ], - "testEnvironment": "node", - "moduleDirectories": [ - "node_modules", - "src", - ], - "rootDir": "/src/typescript-schema-generator", - "displayName": "src/typescript-schema-generator", - } - ], -}; From 573d3d9347bc18e1394c2cd3db643129cae5cd7e Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Mon, 23 Mar 2026 15:56:03 +0000 Subject: [PATCH 08/17] CCM-14615: a look at caching generated dependencies --- .github/workflows/stage-2-test.yaml | 17 +++++++++++++++++ scripts/tests/unit.sh | 2 -- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index 5adb30c7b..af23ec7ae 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -100,12 +100,29 @@ jobs: with: node-version: ${{ inputs.nodejs_version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Install dependencies" + run: npm ci - name: "Setup Python" uses: actions/setup-python@v6 with: python-version: ${{ inputs.python_version }} cache: 'pip' cache-dependency-path: '**/requirements*.txt' + - name: "Cache generated dependencies" + id: schema-cache + uses: actions/cache@v4 + with: + path: | + schemas/digital-letters/ + output/digital-letters/ + src/digital-letters-events/types/ + src/digital-letters-events/validators/ + src/digital-letters-events/models/ + key: generated-deps-${{ runner.os }}-${{ hashFiles('src/cloudevents/**', 'src/typescript-schema-generator/**', 'src/python-schema-generator/**') }} + - name: "Generate dependencies" + if: steps.schema-cache.outputs.cache-hit != 'true' + run: | + npm run generate-dependencies - name: "Run unit test suite" run: | make test-unit diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index d671e0d18..1ea43322c 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -36,8 +36,6 @@ print_timing_summary() { trap print_timing_summary EXIT -run_timed "npm ci" npm ci -run_timed "npm run generate-dependencies" npm run generate-dependencies run_timed "Node unit tests (parallel)" npm run test:unit:parallel || jest_exit=$? # ---- Phase 1: install all Python dev dependencies (sequential) ---- From 2a69f10fc2d1f9e2227deb405a5f42d30992738c Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Tue, 24 Mar 2026 08:02:55 +0000 Subject: [PATCH 09/17] CCM-14615: a look at caching generated dependencies --- .github/workflows/stage-2-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index af23ec7ae..42b5c2734 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -117,7 +117,7 @@ jobs: output/digital-letters/ src/digital-letters-events/types/ src/digital-letters-events/validators/ - src/digital-letters-events/models/ + src/digital-letters-events/digital_letters_events/models/ key: generated-deps-${{ runner.os }}-${{ hashFiles('src/cloudevents/**', 'src/typescript-schema-generator/**', 'src/python-schema-generator/**') }} - name: "Generate dependencies" if: steps.schema-cache.outputs.cache-hit != 'true' From b34140fa4f0302e5474eb0c5fd73874a60990617 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:35:22 +0100 Subject: [PATCH 10/17] CCM-14615: enable coverage --- .../src/__tests__/refresh-keystores.test.ts | 29 +++++++ scripts/generate-parallel-jest-config.ts | 79 ++++++++++++++++++- src/cloudevents/jest.config.ts | 2 - 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts b/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts index e76bc264c..2ce938e9f 100644 --- a/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts +++ b/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts @@ -348,6 +348,35 @@ describe('cleanAndRefreshKeystores', () => { }); }); + it('Does not update youngestKeyDate if first key is already newer than second key', async () => { + const { + mockGenerateNewKey, + mockUploadPublicKeystoreToS3, + mockValidatePrivateKey, + } = setupMocks(['2024-07-15', '2024-06-01']); + + const now = new Date('2024-09-01'); + + // simulate multiple keys where the first key is newer — youngestKeyDate should NOT be updated + mockValidatePrivateKey + .mockResolvedValueOnce({ + valid: true, + keyJwk: { kid: 'key1' } as JWK.Key, + keyDate: new Date('2024-07-15'), // newer key first + }) + .mockResolvedValueOnce({ + valid: true, + keyJwk: { kid: 'key2' } as JWK.Key, + keyDate: new Date('2024-06-01'), // older key second — should not update youngestKeyDate + }); + + await cleanAndRefreshKeystores({ now }); + + expect(mockValidatePrivateKey).toHaveBeenCalledTimes(2); + expect(mockGenerateNewKey).toHaveBeenCalled(); + expect(mockUploadPublicKeystoreToS3).toHaveBeenCalled(); + }); + it('Runs successfully when updating youngestKeyDate if second key is newer', async () => { const { mockGenerateNewKey, diff --git a/scripts/generate-parallel-jest-config.ts b/scripts/generate-parallel-jest-config.ts index ea7fe7714..da6eada82 100644 --- a/scripts/generate-parallel-jest-config.ts +++ b/scripts/generate-parallel-jest-config.ts @@ -63,9 +63,22 @@ function serialise(value: unknown, indent = ' '): string { return String(value); } +// Coverage-related keys that are invalid inside a `projects` entry — they must +// live at the root level only. We strip them from project configs and hoist +// them into the root config instead. +const COVERAGE_PROJECT_KEYS = [ + 'collectCoverage', + 'coverageDirectory', + 'coverageProvider', + 'coverageReporters', + 'coverageThreshold', +] as const; + interface ProjectEntry { workspace: string; config: Record; + coverageThreshold: Record | undefined; + coverageReporters: string[] | undefined; } function main(): void { @@ -108,15 +121,30 @@ function main(): void { } const wsConfig = JSON.parse(json) as Record; + // Extract coverage config before building the project entry — these keys + // are invalid inside a `projects` entry and must live at root level. + const rawThreshold = wsConfig.coverageThreshold as Record | undefined; + const rawReporters = wsConfig.coverageReporters as string[] | undefined; + + // Strip all coverage keys from the project entry. + const strippedConfig: Record = Object.fromEntries( + Object.entries(wsConfig).filter(([k]) => !(COVERAGE_PROJECT_KEYS as readonly string[]).includes(k)), + ); + // Inject rootDir and displayName. Jest resolves all relative paths inside a // project entry relative to that project's rootDir. const entry: Record = { - ...wsConfig, + ...strippedConfig, rootDir: `/${ws}`, displayName: ws, }; - projects.push({ workspace: ws, config: entry }); + projects.push({ + workspace: ws, + config: entry, + coverageThreshold: rawThreshold, + coverageReporters: rawReporters, + }); } // Build the projects array source @@ -125,6 +153,48 @@ function main(): void { return ` // ${p.workspace}\n ${body}`; }); + // Build root-level coverageThreshold using per-workspace directory globs. + // Each workspace's threshold is applied to its own source files only, + // matching the per-workspace enforcement of the serial `npm run test:unit`. + const rootCoverageThreshold: Record = {}; + for (const { workspace, coverageThreshold } of projects) { + if (coverageThreshold) { + // Workspace configs use `{ global: { branches, ... } }` which is correct + // for a standalone per-project run. When hoisted to a root path-based key, + // the `global` wrapper must be unwrapped — path/glob keys take the metric + // values directly (branches/functions/lines/statements). + const thresholdValues = + 'global' in coverageThreshold + ? (coverageThreshold.global as Record) + : coverageThreshold; + + // Jest matches threshold keys against absolute file paths. + // Using an absolute path to the workspace directory means Jest aggregates + // coverage across all files under that directory — matching the `global` + // behaviour of per-workspace serial runs (where one file's low coverage + // can be offset by another's high coverage). + rootCoverageThreshold[`${repoRoot}/${workspace}/`] = thresholdValues; + } + } + + // Collect coverageReporters from all workspaces — take the union so no + // reporter defined in any workspace is lost. Falls back to Jest's default + // if no workspace defines a custom set. + const reporterSet = new Set(); + for (const { coverageReporters } of projects) { + if (coverageReporters) { + for (const r of coverageReporters) reporterSet.add(r); + } + } + // Canonical reporters for all runs: text (console), lcov (SonarCloud), + // html (local browsing), cobertura (CI XML). These are always present + // regardless of what individual workspaces declare. + reporterSet.add('text'); + reporterSet.add('lcov'); + reporterSet.add('html'); + reporterSet.add('cobertura'); + const rootCoverageReporters = [...reporterSet]; + const banner = `/** * Root Jest config — runs all TypeScript workspace test suites in * parallel via Jest's native \`projects\` support. @@ -136,6 +206,11 @@ function main(): void { /** @type {import('jest').Config} */ module.exports = { + collectCoverage: true, + coverageProvider: "babel", + coverageDirectory: ".reports/unit/coverage", + coverageReporters: ${serialise(rootCoverageReporters, ' ')}, + coverageThreshold: ${serialise(rootCoverageThreshold, ' ')}, projects: [ ${projectLines.join(',\n\n')} ], diff --git a/src/cloudevents/jest.config.ts b/src/cloudevents/jest.config.ts index d03dd4d4d..bf12719de 100644 --- a/src/cloudevents/jest.config.ts +++ b/src/cloudevents/jest.config.ts @@ -36,7 +36,6 @@ const config: Config = { '!tools/validator/validate.ts', ], coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html', 'cobertura'], coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], coverageThreshold: { global: { @@ -50,7 +49,6 @@ const config: Config = { moduleNameMapper: { '^(.*)\\.ts$': '$1', }, - verbose: true, testTimeout: 10000, }; From ff15b32d2b5da24e843e3b6cb48c2b4a0c7927bc Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:36:36 +0100 Subject: [PATCH 11/17] CCM-14615: temp coverage failure change --- lambdas/refresh-apim-access-token/jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambdas/refresh-apim-access-token/jest.config.ts b/lambdas/refresh-apim-access-token/jest.config.ts index 1e06dc9e6..d82054a5e 100644 --- a/lambdas/refresh-apim-access-token/jest.config.ts +++ b/lambdas/refresh-apim-access-token/jest.config.ts @@ -7,7 +7,7 @@ config.coverageThreshold = { global: { branches: 100, functions: 100, - lines: 90, + lines: 99, statements: -10, }, }; From dac44d4964400534586cacc38d2cc84526568c82 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:39:08 +0100 Subject: [PATCH 12/17] CCM-14615: temp coverage failure change 2 --- .../src/__tests__/apis/sqs-handler.test.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts index 74c1ac350..57160c9fe 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -35,44 +35,44 @@ describe('SQS Handler', () => { }); describe('pdm.resource.submitted', () => { - it('should send pdm.resource.available event when the document is ready', async () => { - pdm.poll.mockResolvedValueOnce({ - pdmAvailability: 'available', - nhsNumber: '9999999999', - odsCode: 'AB1234', - }); - - const response = await handler(recordEvent([pdmResourceSubmittedEvent])); - - expect(eventPublisher.sendEvents).toHaveBeenCalledWith( - [ - { - ...pdmResourceSubmittedEvent, - id: '550e8400-e29b-41d4-a716-446655440001', - time: '2023-06-20T12:00:00.250Z', - recordedtime: '2023-06-20T12:00:00.250Z', - dataschema: - 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-available-data.schema.json', - type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', - data: { - messageReference: pdmResourceSubmittedEvent.data.messageReference, - senderId: pdmResourceSubmittedEvent.data.senderId, - resourceId: pdmResourceSubmittedEvent.data.resourceId, - nhsNumber: '9999999999', - odsCode: 'AB1234', - }, - }, - ], - expect.any(Function), - ); - expect(logger.info).toHaveBeenCalledWith( - 'Received SQS Event of 1 record(s)', - ); - expect(logger.info).toHaveBeenCalledWith( - '1 of 1 records processed successfully', - ); - expect(response).toEqual({ batchItemFailures: [] }); - }); + // it('should send pdm.resource.available event when the document is ready', async () => { + // pdm.poll.mockResolvedValueOnce({ + // pdmAvailability: 'available', + // nhsNumber: '9999999999', + // odsCode: 'AB1234', + // }); + + // const response = await handler(recordEvent([pdmResourceSubmittedEvent])); + + // expect(eventPublisher.sendEvents).toHaveBeenCalledWith( + // [ + // { + // ...pdmResourceSubmittedEvent, + // id: '550e8400-e29b-41d4-a716-446655440001', + // time: '2023-06-20T12:00:00.250Z', + // recordedtime: '2023-06-20T12:00:00.250Z', + // dataschema: + // 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-available-data.schema.json', + // type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', + // data: { + // messageReference: pdmResourceSubmittedEvent.data.messageReference, + // senderId: pdmResourceSubmittedEvent.data.senderId, + // resourceId: pdmResourceSubmittedEvent.data.resourceId, + // nhsNumber: '9999999999', + // odsCode: 'AB1234', + // }, + // }, + // ], + // expect.any(Function), + // ); + // expect(logger.info).toHaveBeenCalledWith( + // 'Received SQS Event of 1 record(s)', + // ); + // expect(logger.info).toHaveBeenCalledWith( + // '1 of 1 records processed successfully', + // ); + // expect(response).toEqual({ batchItemFailures: [] }); + // }); it('should send pdm.resource.unavailable event when the document is not ready', async () => { pdm.poll.mockResolvedValueOnce({ From c88e0e7e09cc82b957e792ea4b4c0ab1a1d86720 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:43:07 +0100 Subject: [PATCH 13/17] CCM-14615: temp coverage failure change 2 --- .../src/__tests__/apis/sqs-handler.test.ts | 76 ++++---- .../src/__tests__/app/pdm.test.ts | 178 +++++++++--------- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts index 57160c9fe..74c1ac350 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -35,44 +35,44 @@ describe('SQS Handler', () => { }); describe('pdm.resource.submitted', () => { - // it('should send pdm.resource.available event when the document is ready', async () => { - // pdm.poll.mockResolvedValueOnce({ - // pdmAvailability: 'available', - // nhsNumber: '9999999999', - // odsCode: 'AB1234', - // }); - - // const response = await handler(recordEvent([pdmResourceSubmittedEvent])); - - // expect(eventPublisher.sendEvents).toHaveBeenCalledWith( - // [ - // { - // ...pdmResourceSubmittedEvent, - // id: '550e8400-e29b-41d4-a716-446655440001', - // time: '2023-06-20T12:00:00.250Z', - // recordedtime: '2023-06-20T12:00:00.250Z', - // dataschema: - // 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-available-data.schema.json', - // type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', - // data: { - // messageReference: pdmResourceSubmittedEvent.data.messageReference, - // senderId: pdmResourceSubmittedEvent.data.senderId, - // resourceId: pdmResourceSubmittedEvent.data.resourceId, - // nhsNumber: '9999999999', - // odsCode: 'AB1234', - // }, - // }, - // ], - // expect.any(Function), - // ); - // expect(logger.info).toHaveBeenCalledWith( - // 'Received SQS Event of 1 record(s)', - // ); - // expect(logger.info).toHaveBeenCalledWith( - // '1 of 1 records processed successfully', - // ); - // expect(response).toEqual({ batchItemFailures: [] }); - // }); + it('should send pdm.resource.available event when the document is ready', async () => { + pdm.poll.mockResolvedValueOnce({ + pdmAvailability: 'available', + nhsNumber: '9999999999', + odsCode: 'AB1234', + }); + + const response = await handler(recordEvent([pdmResourceSubmittedEvent])); + + expect(eventPublisher.sendEvents).toHaveBeenCalledWith( + [ + { + ...pdmResourceSubmittedEvent, + id: '550e8400-e29b-41d4-a716-446655440001', + time: '2023-06-20T12:00:00.250Z', + recordedtime: '2023-06-20T12:00:00.250Z', + dataschema: + 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-available-data.schema.json', + type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1', + data: { + messageReference: pdmResourceSubmittedEvent.data.messageReference, + senderId: pdmResourceSubmittedEvent.data.senderId, + resourceId: pdmResourceSubmittedEvent.data.resourceId, + nhsNumber: '9999999999', + odsCode: 'AB1234', + }, + }, + ], + expect.any(Function), + ); + expect(logger.info).toHaveBeenCalledWith( + 'Received SQS Event of 1 record(s)', + ); + expect(logger.info).toHaveBeenCalledWith( + '1 of 1 records processed successfully', + ); + expect(response).toEqual({ batchItemFailures: [] }); + }); it('should send pdm.resource.unavailable event when the document is not ready', async () => { pdm.poll.mockResolvedValueOnce({ diff --git a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts index b4f961092..41b7399c2 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts @@ -67,93 +67,93 @@ describe('Pdm', () => { }); }); - describe('poll', () => { - it('returns available when the document is ready', async () => { - const cfg = validConfig(); - pdmClient.getDocumentReference.mockResolvedValue(availableResponse); - - const pdm = new Pdm(cfg); - - const result = await pdm.poll(pdmResourceSubmittedEvent); - - expect(result).toEqual({ - pdmAvailability: 'available', - nhsNumber: '9912003071', - odsCode: 'Y05868', - }); - }); - - it('returns unavailable when the document is not ready', async () => { - const cfg = validConfig(); - const unavailableResponse = { - ...availableResponse, - content: [ - { - attachment: { - contentType: 'application/pdf', - title: 'Dummy PDF', - }, - }, - ], - }; - pdmClient.getDocumentReference.mockResolvedValue(unavailableResponse); - - const pdm = new Pdm(cfg); - - const result = await pdm.poll(pdmResourceSubmittedEvent); - - expect(result).toEqual({ - pdmAvailability: 'unavailable', - nhsNumber: '9912003071', - odsCode: 'Y05868', - }); - }); - - it('logs and throws error when error from PDM', async () => { - const cfg = validConfig(); - const thrown = new Error('pdm failure'); - pdmClient.getDocumentReference.mockRejectedValueOnce(thrown); - - const pdm = new Pdm(cfg); - - await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); - - expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith( - expect.objectContaining({ - description: 'Error getting document resource from PDM', - err: thrown, - }), - ); - }); - - it('logs and throws error when no ODS Code is found', async () => { - const cfg = validConfig(); - const thrown = new Error('No ODS organization code found'); - const noOdsCodeResponse = { - ...availableResponse, - author: [ - { - identifier: { - system: 'https://fhir.nhs.uk/Id/some-other-code', - value: '1111', - }, - }, - ], - }; - pdmClient.getDocumentReference.mockResolvedValue(noOdsCodeResponse); - - const pdm = new Pdm(cfg); - - await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); - - expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith( - expect.objectContaining({ - description: 'Error getting document resource from PDM', - err: thrown, - }), - ); - }); - }); + // describe('poll', () => { + // it('returns available when the document is ready', async () => { + // const cfg = validConfig(); + // pdmClient.getDocumentReference.mockResolvedValue(availableResponse); + + // const pdm = new Pdm(cfg); + + // const result = await pdm.poll(pdmResourceSubmittedEvent); + + // expect(result).toEqual({ + // pdmAvailability: 'available', + // nhsNumber: '9912003071', + // odsCode: 'Y05868', + // }); + // }); + + // it('returns unavailable when the document is not ready', async () => { + // const cfg = validConfig(); + // const unavailableResponse = { + // ...availableResponse, + // content: [ + // { + // attachment: { + // contentType: 'application/pdf', + // title: 'Dummy PDF', + // }, + // }, + // ], + // }; + // pdmClient.getDocumentReference.mockResolvedValue(unavailableResponse); + + // const pdm = new Pdm(cfg); + + // const result = await pdm.poll(pdmResourceSubmittedEvent); + + // expect(result).toEqual({ + // pdmAvailability: 'unavailable', + // nhsNumber: '9912003071', + // odsCode: 'Y05868', + // }); + // }); + + // it('logs and throws error when error from PDM', async () => { + // const cfg = validConfig(); + // const thrown = new Error('pdm failure'); + // pdmClient.getDocumentReference.mockRejectedValueOnce(thrown); + + // const pdm = new Pdm(cfg); + + // await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); + + // expect(logger.error).toHaveBeenCalledTimes(1); + // expect(logger.error).toHaveBeenCalledWith( + // expect.objectContaining({ + // description: 'Error getting document resource from PDM', + // err: thrown, + // }), + // ); + // }); + + // it('logs and throws error when no ODS Code is found', async () => { + // const cfg = validConfig(); + // const thrown = new Error('No ODS organization code found'); + // const noOdsCodeResponse = { + // ...availableResponse, + // author: [ + // { + // identifier: { + // system: 'https://fhir.nhs.uk/Id/some-other-code', + // value: '1111', + // }, + // }, + // ], + // }; + // pdmClient.getDocumentReference.mockResolvedValue(noOdsCodeResponse); + + // const pdm = new Pdm(cfg); + + // await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); + + // expect(logger.error).toHaveBeenCalledTimes(1); + // expect(logger.error).toHaveBeenCalledWith( + // expect.objectContaining({ + // description: 'Error getting document resource from PDM', + // err: thrown, + // }), + // ); + // }); + // }); }); From 170adf8b664c23050e96f6193d099571808c6ffb Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:50:29 +0100 Subject: [PATCH 14/17] CCM-14615: temp coverage failure change 2 --- .../src/__tests__/app/pdm.test.ts | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts index 41b7399c2..0fc8acafa 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts @@ -66,94 +66,4 @@ describe('Pdm', () => { expect(() => new Pdm(cfg)).toThrow('logger has not been provided'); }); }); - - // describe('poll', () => { - // it('returns available when the document is ready', async () => { - // const cfg = validConfig(); - // pdmClient.getDocumentReference.mockResolvedValue(availableResponse); - - // const pdm = new Pdm(cfg); - - // const result = await pdm.poll(pdmResourceSubmittedEvent); - - // expect(result).toEqual({ - // pdmAvailability: 'available', - // nhsNumber: '9912003071', - // odsCode: 'Y05868', - // }); - // }); - - // it('returns unavailable when the document is not ready', async () => { - // const cfg = validConfig(); - // const unavailableResponse = { - // ...availableResponse, - // content: [ - // { - // attachment: { - // contentType: 'application/pdf', - // title: 'Dummy PDF', - // }, - // }, - // ], - // }; - // pdmClient.getDocumentReference.mockResolvedValue(unavailableResponse); - - // const pdm = new Pdm(cfg); - - // const result = await pdm.poll(pdmResourceSubmittedEvent); - - // expect(result).toEqual({ - // pdmAvailability: 'unavailable', - // nhsNumber: '9912003071', - // odsCode: 'Y05868', - // }); - // }); - - // it('logs and throws error when error from PDM', async () => { - // const cfg = validConfig(); - // const thrown = new Error('pdm failure'); - // pdmClient.getDocumentReference.mockRejectedValueOnce(thrown); - - // const pdm = new Pdm(cfg); - - // await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); - - // expect(logger.error).toHaveBeenCalledTimes(1); - // expect(logger.error).toHaveBeenCalledWith( - // expect.objectContaining({ - // description: 'Error getting document resource from PDM', - // err: thrown, - // }), - // ); - // }); - - // it('logs and throws error when no ODS Code is found', async () => { - // const cfg = validConfig(); - // const thrown = new Error('No ODS organization code found'); - // const noOdsCodeResponse = { - // ...availableResponse, - // author: [ - // { - // identifier: { - // system: 'https://fhir.nhs.uk/Id/some-other-code', - // value: '1111', - // }, - // }, - // ], - // }; - // pdmClient.getDocumentReference.mockResolvedValue(noOdsCodeResponse); - - // const pdm = new Pdm(cfg); - - // await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); - - // expect(logger.error).toHaveBeenCalledTimes(1); - // expect(logger.error).toHaveBeenCalledWith( - // expect.objectContaining({ - // description: 'Error getting document resource from PDM', - // err: thrown, - // }), - // ); - // }); - // }); }); From ace6d474ee3c9171a1127fdbf88c9b9b6ed2a78a Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 14:57:35 +0100 Subject: [PATCH 15/17] CCM-14615: temp coverage failure change 2 --- .../src/__tests__/app/pdm.test.ts | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts index 0fc8acafa..040e0628e 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts @@ -1,7 +1,6 @@ import { mock } from 'jest-mock-extended'; import { IPdmClient, Logger } from 'utils'; import { Pdm, PdmDependencies } from 'app/pdm'; -import { pdmResourceSubmittedEvent } from '__tests__/test-data'; const logger = mock(); const pdmClient = mock(); @@ -10,39 +9,6 @@ const validConfig = (): PdmDependencies => ({ logger, }); -const availableResponse = { - resourceType: 'DocumentReference', - id: '4c5af7c3-ca21-31b8-924b-fa526db5379b', - meta: { - versionId: '1', - lastUpdated: '2025-12-10T09:00:47.068021Z', - }, - status: 'current', - author: [ - { - identifier: { - system: 'https://fhir.nhs.uk/Id/ods-organization-code', - value: 'Y05868', - }, - }, - ], - subject: { - identifier: { - system: 'https://fhir.nhs.uk/Id/nhs-number', - value: '9912003071', - }, - }, - content: [ - { - attachment: { - contentType: 'application/pdf', - data: 'base64-encoded-pdf', - title: 'Dummy PDF', - }, - }, - ], -}; - describe('Pdm', () => { describe('constructor', () => { it('is created when required deps are provided', () => { From 0656402f75aea2824316bddc282b7661b9cc7401 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 15:11:09 +0100 Subject: [PATCH 16/17] CCM-14615: revert temp coverage failure changes --- .../src/__tests__/app/pdm.test.ts | 124 ++++++++++++++++++ .../refresh-apim-access-token/jest.config.ts | 2 +- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts index 040e0628e..b4f961092 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/app/pdm.test.ts @@ -1,6 +1,7 @@ import { mock } from 'jest-mock-extended'; import { IPdmClient, Logger } from 'utils'; import { Pdm, PdmDependencies } from 'app/pdm'; +import { pdmResourceSubmittedEvent } from '__tests__/test-data'; const logger = mock(); const pdmClient = mock(); @@ -9,6 +10,39 @@ const validConfig = (): PdmDependencies => ({ logger, }); +const availableResponse = { + resourceType: 'DocumentReference', + id: '4c5af7c3-ca21-31b8-924b-fa526db5379b', + meta: { + versionId: '1', + lastUpdated: '2025-12-10T09:00:47.068021Z', + }, + status: 'current', + author: [ + { + identifier: { + system: 'https://fhir.nhs.uk/Id/ods-organization-code', + value: 'Y05868', + }, + }, + ], + subject: { + identifier: { + system: 'https://fhir.nhs.uk/Id/nhs-number', + value: '9912003071', + }, + }, + content: [ + { + attachment: { + contentType: 'application/pdf', + data: 'base64-encoded-pdf', + title: 'Dummy PDF', + }, + }, + ], +}; + describe('Pdm', () => { describe('constructor', () => { it('is created when required deps are provided', () => { @@ -32,4 +66,94 @@ describe('Pdm', () => { expect(() => new Pdm(cfg)).toThrow('logger has not been provided'); }); }); + + describe('poll', () => { + it('returns available when the document is ready', async () => { + const cfg = validConfig(); + pdmClient.getDocumentReference.mockResolvedValue(availableResponse); + + const pdm = new Pdm(cfg); + + const result = await pdm.poll(pdmResourceSubmittedEvent); + + expect(result).toEqual({ + pdmAvailability: 'available', + nhsNumber: '9912003071', + odsCode: 'Y05868', + }); + }); + + it('returns unavailable when the document is not ready', async () => { + const cfg = validConfig(); + const unavailableResponse = { + ...availableResponse, + content: [ + { + attachment: { + contentType: 'application/pdf', + title: 'Dummy PDF', + }, + }, + ], + }; + pdmClient.getDocumentReference.mockResolvedValue(unavailableResponse); + + const pdm = new Pdm(cfg); + + const result = await pdm.poll(pdmResourceSubmittedEvent); + + expect(result).toEqual({ + pdmAvailability: 'unavailable', + nhsNumber: '9912003071', + odsCode: 'Y05868', + }); + }); + + it('logs and throws error when error from PDM', async () => { + const cfg = validConfig(); + const thrown = new Error('pdm failure'); + pdmClient.getDocumentReference.mockRejectedValueOnce(thrown); + + const pdm = new Pdm(cfg); + + await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ + description: 'Error getting document resource from PDM', + err: thrown, + }), + ); + }); + + it('logs and throws error when no ODS Code is found', async () => { + const cfg = validConfig(); + const thrown = new Error('No ODS organization code found'); + const noOdsCodeResponse = { + ...availableResponse, + author: [ + { + identifier: { + system: 'https://fhir.nhs.uk/Id/some-other-code', + value: '1111', + }, + }, + ], + }; + pdmClient.getDocumentReference.mockResolvedValue(noOdsCodeResponse); + + const pdm = new Pdm(cfg); + + await expect(pdm.poll(pdmResourceSubmittedEvent)).rejects.toThrow(thrown); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ + description: 'Error getting document resource from PDM', + err: thrown, + }), + ); + }); + }); }); diff --git a/lambdas/refresh-apim-access-token/jest.config.ts b/lambdas/refresh-apim-access-token/jest.config.ts index d82054a5e..1e06dc9e6 100644 --- a/lambdas/refresh-apim-access-token/jest.config.ts +++ b/lambdas/refresh-apim-access-token/jest.config.ts @@ -7,7 +7,7 @@ config.coverageThreshold = { global: { branches: 100, functions: 100, - lines: 99, + lines: 90, statements: -10, }, }; From f0f3fe0b4d919c0b09f603c2564c52ba85348d45 Mon Sep 17 00:00:00 2001 From: Ian Hodges Date: Wed, 8 Apr 2026 15:41:50 +0100 Subject: [PATCH 17/17] CCM-14615: tweaks --- .github/workflows/stage-2-test.yaml | 2 +- scripts/config/sonar-scanner.properties | 4 ++-- scripts/tests/unit.sh | 4 ---- src/TESTING_PLAN.md | 10 +--------- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index 42b5c2734..e0655e773 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -137,7 +137,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: code-coverage-report - path: ".reports/lcov.info" + path: ".reports/unit/coverage/lcov.info" - name: "Save Python coverage reports" uses: actions/upload-artifact@v4 with: diff --git a/scripts/config/sonar-scanner.properties b/scripts/config/sonar-scanner.properties index 1e2194787..01ab61b63 100644 --- a/scripts/config/sonar-scanner.properties +++ b/scripts/config/sonar-scanner.properties @@ -33,5 +33,5 @@ sonar.coverage.exclusions=\ # Coverage reports sonar.python.coverage.reportPaths=.coverage/coverage.xml,src/asyncapigenerator/coverage.xml,src/cloudeventjekylldocs/coverage.xml,src/eventcatalogasyncapiimporter/coverage.xml,utils/py-utils/coverage.xml,lambdas/mesh-acknowledge/coverage.xml,src/python-schema-generator/coverage.xml,lambdas/mesh-poll/coverage.xml,lambdas/mesh-download/coverage.xml,lambdas/report-sender/coverage.xml, -sonar.javascript.lcov.reportPaths=lcov.info,src/cloudevents/coverage/lcov.info -sonar.typescript.lcov.reportPaths=lcov.info,src/cloudevents/coverage/lcov.info +sonar.javascript.lcov.reportPaths=lcov.info +sonar.typescript.lcov.reportPaths=lcov.info diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 1ea43322c..1d7b57236 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -84,10 +84,6 @@ _py_end=$(date +%s) _timer_labels+=("Python unit tests (parallel)") _timer_seconds+=("$((_py_end - _py_start))") -# merge coverage reports -run_timed "lcov-result-merger" \ - bash -c 'mkdir -p .reports && TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger "**/.reports/unit/coverage/lcov.info" ".reports/lcov.info" --ignore "node_modules" --prepend-source-files --prepend-path-fix "../../.."' - # Propagate any Jest failure now that all other test suites have completed if [ "${jest_exit:-0}" -ne 0 ]; then echo "Jest tests failed with exit code ${jest_exit}" diff --git a/src/TESTING_PLAN.md b/src/TESTING_PLAN.md index aa844a470..ea40f5206 100644 --- a/src/TESTING_PLAN.md +++ b/src/TESTING_PLAN.md @@ -2336,7 +2336,7 @@ PASS tools/generator/__tests__/generate-docs.test.ts (10.747 s) - **Author**: GitHub Copilot - **Activity**: Fixed CI/CD workflow to upload and download Python coverage files for SonarCloud analysis -- **Root Cause**: Python coverage.xml files were being generated but not uploaded as artifacts. SonarCloud only had access to JavaScript coverage (.reports/lcov.info) +- **Root Cause**: Python coverage.xml files were being generated but not uploaded as artifacts. SonarCloud only had access to JavaScript coverage (.reports/unit/coverage/lcov.info) - **Changes**: - Updated `.github/workflows/stage-2-test.yaml` in `test-unit` job: - Added new step "Save Python coverage reports" to upload `src/**/coverage.xml` as `python-coverage-reports` artifact @@ -3111,14 +3111,6 @@ make -C ./src/cloudeventjekylldocs coverage # Use coverage to generate coverage npm ci npm run test:unit --workspaces -# Merge coverage reports -mkdir -p .reports -TMPDIR="./.reports" ./node_modules/.bin/lcov-result-merger \ - "**/.reports/unit/coverage/lcov.info" \ - ".reports/lcov.info" \ - --ignore "node_modules" \ - --prepend-source-files \ - --prepend-path-fix "../../.." ``` ### CI/CD Workflow Details