diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6dd02807..e6c2e45e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,6 +219,14 @@ jobs: --head=HEAD \ --parallel + - name: Coverage summary + run: | + echo "::group::Unit test coverage summary" + yarn script:coverage:report \ + --base=origin/${{ github.base_ref }} \ + --head=HEAD + echo "::endgroup::" + check-migration-lock: # Verifies migrations.lock.json is in sync with src/migrations for any # affected SQL store package that defines a `migrations:check-lock` diff --git a/core/acs-reader/vitest.config.ts b/core/acs-reader/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/acs-reader/vitest.config.ts +++ b/core/acs-reader/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/amulet-service/vitest.config.ts b/core/amulet-service/vitest.config.ts index 7f33c93c9..6a583a770 100644 --- a/core/amulet-service/vitest.config.ts +++ b/core/amulet-service/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/asyncapi-client/vitest.config.ts b/core/asyncapi-client/vitest.config.ts index 7f33c93c9..6a583a770 100644 --- a/core/asyncapi-client/vitest.config.ts +++ b/core/asyncapi-client/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/daml-codegen-helpers/vitest.config.ts b/core/daml-codegen-helpers/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/daml-codegen-helpers/vitest.config.ts +++ b/core/daml-codegen-helpers/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/ledger-client-types/package.json b/core/ledger-client-types/package.json index cece5498c..bfb53d2ab 100644 --- a/core/ledger-client-types/package.json +++ b/core/ledger-client-types/package.json @@ -21,8 +21,8 @@ "dev": "tsup --watch --onSuccess \"tsc\"", "flatpack": "yarn pack --out \"$FLATPACK_OUTDIR\"", "clean": "tsc -b --clean; rm -rf dist", - "test": "vitest run --project node --project browser --passWithNoTests", - "test:coverage": "vitest run --project node --project browser --coverage --passWithNoTests" + "test": "vitest run --project node --project browser", + "test:coverage": "vitest run --project node --project browser --coverage" }, "devDependencies": { "@types/node": "^25.3.3", diff --git a/core/ledger-client-types/src/utils.test.ts b/core/ledger-client-types/src/utils.test.ts new file mode 100644 index 000000000..28ff25e57 --- /dev/null +++ b/core/ledger-client-types/src/utils.test.ts @@ -0,0 +1,146 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest' +import { EventFilterBySetup, TransactionFilterBySetup } from './utils' + +const PARTY_ID = 'alice::abc123' +const TEMPLATE_ID = 'pkg:Module:Template' +const INTERFACE_ID = 'pkg:Module:Interface' + +describe('TransactionFilterBySetup', () => { + it('builds a party filter for a single template id', () => { + const filter = TransactionFilterBySetup({ + partyId: PARTY_ID, + templateIds: TEMPLATE_ID, + }) + + expect(filter.filtersByParty).toEqual({ + [PARTY_ID]: { + cumulative: [ + { + identifierFilter: { + TemplateFilter: { + value: { + templateId: TEMPLATE_ID, + includeCreatedEventBlob: true, + }, + }, + }, + }, + ], + }, + }) + }) + + it('normalizes template ids and adds a wildcard filter when requested', () => { + const filter = TransactionFilterBySetup({ + partyId: PARTY_ID, + templateIds: [TEMPLATE_ID, 'pkg:Module:Other'], + includeWildcard: true, + }) + + expect(filter.filtersByParty?.[PARTY_ID]?.cumulative).toEqual([ + { + identifierFilter: { + TemplateFilter: { + value: { + templateId: TEMPLATE_ID, + includeCreatedEventBlob: true, + }, + }, + }, + }, + { + identifierFilter: { + TemplateFilter: { + value: { + templateId: 'pkg:Module:Other', + includeCreatedEventBlob: true, + }, + }, + }, + }, + { + identifierFilter: { + WildcardFilter: { + value: { includeCreatedEventBlob: true }, + }, + }, + }, + ]) + }) + + it('builds interface filters when no template ids are provided', () => { + const filter = TransactionFilterBySetup({ + partyId: PARTY_ID, + interfaceIds: INTERFACE_ID, + }) + + expect(filter.filtersByParty?.[PARTY_ID]?.cumulative).toEqual([ + { + identifierFilter: { + InterfaceFilter: { + value: { + interfaceId: INTERFACE_ID, + includeInterfaceView: true, + includeCreatedEventBlob: true, + }, + }, + }, + }, + ]) + }) + + it('uses filtersForAnyParty for master users', () => { + const filter = TransactionFilterBySetup({ + isMasterUser: true, + templateIds: TEMPLATE_ID, + }) + + expect(filter.filtersByParty).toEqual({}) + expect(filter.filtersForAnyParty).toEqual({ + cumulative: [ + { + identifierFilter: { + InterfaceFilter: { + value: { + interfaceId: TEMPLATE_ID, + includeInterfaceView: true, + includeCreatedEventBlob: true, + }, + }, + }, + }, + ], + }) + }) + + it('requires a party id for non-master users', () => { + expect(() => + TransactionFilterBySetup({ templateIds: TEMPLATE_ID }) + ).toThrow('Party must be provided for non-master users') + }) +}) + +describe('EventFilterBySetup', () => { + it('defaults verbose to false', () => { + const filter = EventFilterBySetup({ + partyId: PARTY_ID, + templateIds: TEMPLATE_ID, + }) + + expect(filter.verbose).toBe(false) + }) + + it('includes verbose when requested', () => { + const filter = EventFilterBySetup({ + partyId: PARTY_ID, + templateIds: TEMPLATE_ID, + verbose: true, + }) + + expect(filter.verbose).toBe(true) + expect(filter.filtersByParty?.[PARTY_ID]?.cumulative).toHaveLength(1) + }) +}) diff --git a/core/ledger-client-types/vitest.config.ts b/core/ledger-client-types/vitest.config.ts index 6d6980ae1..6adc37c6f 100644 --- a/core/ledger-client-types/vitest.config.ts +++ b/core/ledger-client-types/vitest.config.ts @@ -8,8 +8,9 @@ export default defineConfig({ test: { coverage: { include: ['src/**/*.ts'], + exclude: ['src/generated-clients/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/ledger-client/vitest.config.ts b/core/ledger-client/vitest.config.ts index 82db17845..5df6bd255 100644 --- a/core/ledger-client/vitest.config.ts +++ b/core/ledger-client/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/test-utils.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/ledger-proto/vitest.config.ts b/core/ledger-proto/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/ledger-proto/vitest.config.ts +++ b/core/ledger-proto/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/provider-dapp/vitest.config.ts b/core/provider-dapp/vitest.config.ts index c5aeac25c..8bb30179a 100644 --- a/core/provider-dapp/vitest.config.ts +++ b/core/provider-dapp/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/provider-ledger/vitest.config.ts b/core/provider-ledger/vitest.config.ts index 29e8cb660..e8485a934 100644 --- a/core/provider-ledger/vitest.config.ts +++ b/core/provider-ledger/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/rpc-errors/vitest.config.ts b/core/rpc-errors/vitest.config.ts index 29e8cb660..e8485a934 100644 --- a/core/rpc-errors/vitest.config.ts +++ b/core/rpc-errors/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/rpc-generator/vitest.config.ts b/core/rpc-generator/vitest.config.ts index 67370d096..63ca1ed60 100644 --- a/core/rpc-generator/vitest.config.ts +++ b/core/rpc-generator/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/rpc-transport/vitest.config.ts b/core/rpc-transport/vitest.config.ts index 1abe7e092..8e7183576 100644 --- a/core/rpc-transport/vitest.config.ts +++ b/core/rpc-transport/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/signing-blockdaemon/vitest.config.ts b/core/signing-blockdaemon/vitest.config.ts index e3463b72d..3be1b7f79 100644 --- a/core/signing-blockdaemon/vitest.config.ts +++ b/core/signing-blockdaemon/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/signing-dfns/vitest.config.ts b/core/signing-dfns/vitest.config.ts index 09e1a4734..993d7aa41 100644 --- a/core/signing-dfns/vitest.config.ts +++ b/core/signing-dfns/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/signing-fireblocks/vitest.config.ts b/core/signing-fireblocks/vitest.config.ts index 09e1a4734..993d7aa41 100644 --- a/core/signing-fireblocks/vitest.config.ts +++ b/core/signing-fireblocks/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/signing-internal/vitest.config.ts b/core/signing-internal/vitest.config.ts index 09e1a4734..993d7aa41 100644 --- a/core/signing-internal/vitest.config.ts +++ b/core/signing-internal/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/signing-lib/package.json b/core/signing-lib/package.json index 66bd10182..ccc7111aa 100644 --- a/core/signing-lib/package.json +++ b/core/signing-lib/package.json @@ -20,8 +20,8 @@ "dev": "tsup --watch --onSuccess \"tsc\"", "flatpack": "yarn pack --out \"$FLATPACK_OUTDIR\"", "clean": "tsc -b --clean && rm -rf ./dist", - "test": "vitest run --project node --project browser --passWithNoTests", - "test:coverage": "vitest run --project node --project browser --coverage --passWithNoTests" + "test": "vitest run --project node", + "test:coverage": "vitest run --project node --coverage" }, "dependencies": { "@canton-network/core-wallet-auth": "workspace:^", @@ -35,9 +35,7 @@ "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.24", "@types/node": "^25.3.3", - "@vitest/browser-playwright": "^4.1.2", "@vitest/coverage-v8": "^4.1.2", - "playwright": "^1.58.2", "tsup": "^8.5.1", "typescript": "^5.9.3", "vitest": "^4.1.2" diff --git a/core/signing-lib/src/index.test.ts b/core/signing-lib/src/index.test.ts new file mode 100644 index 000000000..8f2f2d6d3 --- /dev/null +++ b/core/signing-lib/src/index.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest' +import nacl from 'tweetnacl' +import naclUtil from 'tweetnacl-util' +import { + createKeyPair, + getPublicKeyFromPrivate, + signMessage, + signTransactionHash, + verifySignedTxHash, +} from './index' + +const TX_HASH = + '88beb0783e394f6128699bad42906374ab64197d260db05bb0cfeeb518ba3ac2' + +describe('getPublicKeyFromPrivate', () => { + it('derives the matching public key', () => { + const { publicKey, privateKey } = createKeyPair() + + expect(getPublicKeyFromPrivate(privateKey)).toBe(publicKey) + }) +}) + +describe('signTransactionHash', () => { + it('produces a signature that verifySignedTxHash accepts', () => { + const { publicKey, privateKey } = createKeyPair() + const signature = signTransactionHash(TX_HASH, privateKey) + + expect(verifySignedTxHash(TX_HASH, publicKey, signature)).toBe(true) + }) + + it('is rejected by verifySignedTxHash with a different public key', () => { + const { privateKey } = createKeyPair() + const { publicKey: otherPublicKey } = createKeyPair() + const signature = signTransactionHash(TX_HASH, privateKey) + + expect(verifySignedTxHash(TX_HASH, otherPublicKey, signature)).toBe( + false + ) + }) +}) + +describe('signMessage', () => { + it('signs a UTF-8 message with the private key', () => { + const message = 'message' + const { publicKey, privateKey } = createKeyPair() + const signature = signMessage(message, privateKey) + + expect( + nacl.sign.detached.verify( + new TextEncoder().encode(message), + naclUtil.decodeBase64(signature), + naclUtil.decodeBase64(publicKey) + ) + ).toBe(true) + }) +}) + +describe('verifySignedTxHash', () => { + it('rejects an invalid signature', () => { + const { publicKey } = createKeyPair() + const invalidSignature = naclUtil.encodeBase64( + Uint8Array.from({ length: nacl.sign.signatureLength }, () => 0) + ) + + expect(verifySignedTxHash(TX_HASH, publicKey, invalidSignature)).toBe( + false + ) + }) +}) diff --git a/core/signing-lib/vitest.config.ts b/core/signing-lib/vitest.config.ts index 6d6980ae1..1478ea281 100644 --- a/core/signing-lib/vitest.config.ts +++ b/core/signing-lib/vitest.config.ts @@ -2,14 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { defineConfig, defineProject } from 'vitest/config' -import { playwright } from '@vitest/browser-playwright' export default defineConfig({ test: { coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, @@ -27,22 +26,6 @@ export default defineConfig({ include: ['src/**/*.test.ts'], }, }), - defineProject({ - test: { - name: 'browser', - include: ['src/**/*.test.ts'], - browser: { - enabled: true, - provider: playwright({ - trace: 'off', - screenshot: 'off', - video: 'off', - }), - instances: [{ browser: 'chromium' }], - headless: true, - }, - }, - }), ], }, }) diff --git a/core/signing-participant/vitest.config.ts b/core/signing-participant/vitest.config.ts index d268bc286..5ef573f05 100644 --- a/core/signing-participant/vitest.config.ts +++ b/core/signing-participant/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/signing-store-sql/vitest.config.ts b/core/signing-store-sql/vitest.config.ts index 27a42c551..16ff7e71e 100644 --- a/core/signing-store-sql/vitest.config.ts +++ b/core/signing-store-sql/vitest.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ 'src/migrator.ts', ], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/splice-client/vitest.config.ts b/core/splice-client/vitest.config.ts index 125a7bee1..fd9d37bc9 100644 --- a/core/splice-client/vitest.config.ts +++ b/core/splice-client/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/generated-clients/*', 'src/test-utils.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/splice-provider/vitest.config.ts b/core/splice-provider/vitest.config.ts index 7f33c93c9..6a583a770 100644 --- a/core/splice-provider/vitest.config.ts +++ b/core/splice-provider/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/token-standard-service/vitest.config.ts b/core/token-standard-service/vitest.config.ts index dfea32869..d52fa766e 100644 --- a/core/token-standard-service/vitest.config.ts +++ b/core/token-standard-service/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/tx-parser/vitest.config.ts b/core/tx-parser/vitest.config.ts index dfea32869..d52fa766e 100644 --- a/core/tx-parser/vitest.config.ts +++ b/core/tx-parser/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/tx-visualizer/vitest.config.ts b/core/tx-visualizer/vitest.config.ts index 1bbe3bfa0..5035aa427 100644 --- a/core/tx-visualizer/vitest.config.ts +++ b/core/tx-visualizer/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['**/fixtures/**'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/types/src/index.test.ts b/core/types/src/index.test.ts new file mode 100644 index 000000000..3c210d688 --- /dev/null +++ b/core/types/src/index.test.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest' +import { WalletEvent, isSpliceMessage, isSpliceMessageEvent } from './index' + +describe('isSpliceMessage', () => { + it('accepts each splice message variant', () => { + expect( + isSpliceMessage({ + type: WalletEvent.SPLICE_WALLET_REQUEST, + request: { jsonrpc: '2.0', method: 'connect', id: 1 }, + }) + ).toBe(true) + expect( + isSpliceMessage({ + type: WalletEvent.SPLICE_WALLET_RESPONSE, + response: { jsonrpc: '2.0', id: 1, result: null }, + }) + ).toBe(true) + expect( + isSpliceMessage({ type: WalletEvent.SPLICE_WALLET_EXT_READY }) + ).toBe(true) + expect( + isSpliceMessage({ type: WalletEvent.SPLICE_WALLET_EXT_ACK }) + ).toBe(true) + expect( + isSpliceMessage({ + type: WalletEvent.SPLICE_WALLET_EXT_OPEN, + url: 'https://wallet.example.com', + }) + ).toBe(true) + expect( + isSpliceMessage({ + type: WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS, + token: 'token', + sessionId: 'session', + }) + ).toBe(true) + }) + + it('rejects invalid values', () => { + expect(isSpliceMessage(null)).toBe(false) + expect(isSpliceMessage('message')).toBe(false) + expect( + isSpliceMessage({ type: WalletEvent.SPLICE_WALLET_REQUEST }) + ).toBe(false) + expect( + isSpliceMessage({ + type: WalletEvent.SPLICE_WALLET_EXT_OPEN, + url: 'not-a-url', + }) + ).toBe(false) + }) +}) + +describe('isSpliceMessageEvent', () => { + it('accepts objects with valid splice message data', () => { + const message = { + type: WalletEvent.SPLICE_WALLET_EXT_READY, + } + expect(isSpliceMessageEvent({ data: message })).toBe(true) + }) + + it('rejects objects without valid splice message data', () => { + expect(isSpliceMessageEvent(null)).toBe(false) + expect(isSpliceMessageEvent({})).toBe(false) + expect(isSpliceMessageEvent({ data: { type: 'unknown' } })).toBe(false) + expect( + isSpliceMessageEvent({ + data: { type: WalletEvent.SPLICE_WALLET_REQUEST }, + }) + ).toBe(false) + }) +}) diff --git a/core/types/vitest.config.ts b/core/types/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/types/vitest.config.ts +++ b/core/types/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/wallet-auth/vitest.config.ts b/core/wallet-auth/vitest.config.ts index a4f8089bd..bfec8ce94 100644 --- a/core/wallet-auth/vitest.config.ts +++ b/core/wallet-auth/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/wallet-dapp-remote-rpc-client/vitest.config.ts b/core/wallet-dapp-remote-rpc-client/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/wallet-dapp-remote-rpc-client/vitest.config.ts +++ b/core/wallet-dapp-remote-rpc-client/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/wallet-dapp-rpc-client/vitest.config.ts b/core/wallet-dapp-rpc-client/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/wallet-dapp-rpc-client/vitest.config.ts +++ b/core/wallet-dapp-rpc-client/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/wallet-discovery/vitest.config.ts b/core/wallet-discovery/vitest.config.ts index dfea32869..d52fa766e 100644 --- a/core/wallet-discovery/vitest.config.ts +++ b/core/wallet-discovery/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/wallet-store-inmemory/vitest.config.ts b/core/wallet-store-inmemory/vitest.config.ts index ec7d23111..f7695a1d2 100644 --- a/core/wallet-store-inmemory/vitest.config.ts +++ b/core/wallet-store-inmemory/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/index.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/wallet-store-sql/vitest.config.ts b/core/wallet-store-sql/vitest.config.ts index 1f146ed0d..d9c9dde06 100644 --- a/core/wallet-store-sql/vitest.config.ts +++ b/core/wallet-store-sql/vitest.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ 'src/cli.ts', ], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/wallet-store/vitest.config.ts b/core/wallet-store/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/core/wallet-store/vitest.config.ts +++ b/core/wallet-store/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/wallet-test-utils/vitest.config.ts b/core/wallet-test-utils/vitest.config.ts index 67370d096..63ca1ed60 100644 --- a/core/wallet-test-utils/vitest.config.ts +++ b/core/wallet-test-utils/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/core/wallet-ui-components/vitest.config.ts b/core/wallet-ui-components/vitest.config.ts index ec8736932..e9a17b74a 100644 --- a/core/wallet-ui-components/vitest.config.ts +++ b/core/wallet-ui-components/vitest.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ 'src/components/fixtures.ts', ], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 80, functions: 80, diff --git a/core/wallet-user-rpc-client/vitest.config.ts b/core/wallet-user-rpc-client/vitest.config.ts index 67370d096..63ca1ed60 100644 --- a/core/wallet-user-rpc-client/vitest.config.ts +++ b/core/wallet-user-rpc-client/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/package.json b/package.json index 432acf6c8..b5e5a7085 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "clean:all": "nx run-many -t clean --parallel && yarn nx reset", "test:all": "nx run-many -t test --parallel", "test-coverage:all": "nx run-many -t test:coverage --parallel", + "script:coverage:report": "tsx ./scripts/src/coverage-report.ts", "postinstall": "husky && yarn generate:tokenstandard", "docs:update-wg-config": "tsx ./scripts/src/docs-update-wg-config.ts", "script:cleancoding": "tsx ./scripts/src/clean-coding.ts", diff --git a/scripts/src/coverage-report.config.json b/scripts/src/coverage-report.config.json new file mode 100644 index 000000000..a16781e8d --- /dev/null +++ b/scripts/src/coverage-report.config.json @@ -0,0 +1,9 @@ +{ + "excludedProjects": [ + "@canton-network/core-rpc-generator", + "@canton-network/core-daml-codegen-helpers", + "@canton-network/core-ledger-proto", + "@canton-network/core-wallet-test-utils", + "@canton-network/core-wallet-store" + ] +} diff --git a/scripts/src/coverage-report.ts b/scripts/src/coverage-report.ts new file mode 100644 index 000000000..611a14339 --- /dev/null +++ b/scripts/src/coverage-report.ts @@ -0,0 +1,198 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { getArgValue, getRepoRoot } from './lib/utils.js' + +interface CoverageMetric { + total: number + covered: number + pct: number +} + +interface CoverageSummary { + total: { + lines: CoverageMetric + } +} + +interface NxProject { + root: string +} + +interface CoverageReportConfig { + excludedProjects?: string[] +} + +type CoverageResult = + | { + status: 'measured' + linesPct: number + linesTotal: number + linesCovered: number + } + | { status: 'excluded' } + | { status: 'missing' } + +interface PackageCoverage { + name: string + result: CoverageResult +} + +const repoRoot = getRepoRoot() +const base = getArgValue('base') +const head = getArgValue('head') + +const coverageReportConfigPath = join( + repoRoot, + 'scripts/src/coverage-report.config.json' +) +const centrallyExcludedProjects = new Set( + existsSync(coverageReportConfigPath) + ? (( + JSON.parse( + readFileSync(coverageReportConfigPath, 'utf8') + ) as CoverageReportConfig + ).excludedProjects ?? []) + : [] +) + +function nxJson(command: string): unknown { + const stdout = execSync(`yarn ${command}`, { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }) + return JSON.parse(stdout) +} + +function getProjects(): string[] { + let command = 'nx show projects --with-target=test:coverage --json' + if (base && head) { + command += ` --affected --base=${base} --head=${head}` + } + return nxJson(command) as string[] +} + +function getProjectRoot(projectName: string): string { + const project = nxJson(`nx show project ${projectName} --json`) as NxProject + return project.root +} + +function isCoverageExcluded(projectName: string): boolean { + return centrallyExcludedProjects.has(projectName) +} + +function readLineCoverage( + projectName: string, + projectRoot: string +): CoverageResult { + if (isCoverageExcluded(projectName)) { + return { status: 'excluded' } + } + + const summaryPath = join( + repoRoot, + projectRoot, + 'coverage', + 'coverage-summary.json' + ) + if (!existsSync(summaryPath)) { + return { status: 'missing' } + } + + const summary = JSON.parse( + readFileSync(summaryPath, 'utf8') + ) as CoverageSummary + const lines = summary.total.lines + return { + status: 'measured', + linesPct: lines.pct, + linesTotal: lines.total, + linesCovered: lines.covered, + } +} + +function formatCoverage(result: CoverageResult): string { + switch (result.status) { + case 'measured': + return `${result.linesPct.toFixed(2)}%` + case 'excluded': + return 'N/A' + case 'missing': + return 'No coverage info' + } +} + +function printReport(entries: PackageCoverage[]): void { + const nameWidth = Math.max( + 7, + ...entries.map((entry) => entry.name.length), + 'Package'.length + ) + const coverageWidth = Math.max('Coverage'.length, 8) + const divider = '-'.repeat(nameWidth + coverageWidth + 3) + + console.log('') + console.log('Unit test coverage summary') + console.log(divider) + console.log( + `${'Package'.padEnd(nameWidth)} | ${'Coverage'.padStart(coverageWidth)}` + ) + console.log(divider) + + for (const entry of entries) { + console.log( + `${entry.name.padEnd(nameWidth)} | ${formatCoverage(entry.result).padStart(coverageWidth)}` + ) + } + + const measured = entries.flatMap((entry) => + entry.result.status === 'measured' ? [entry.result] : [] + ) + if (measured.length > 0) { + const totalLines = measured.reduce( + (sum, result) => sum + result.linesTotal, + 0 + ) + const coveredLines = measured.reduce( + (sum, result) => sum + result.linesCovered, + 0 + ) + const totalPct = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0 + console.log(divider) + console.log( + `${'Total'.padEnd(nameWidth)} | ${`${totalPct.toFixed(2)}%`.padStart(coverageWidth)}` + ) + } + + console.log(divider) + console.log('') +} + +const projects = getProjects() +if (projects.length === 0) { + console.log('No packages with test:coverage target to report.') + process.exit(0) +} + +const entries: PackageCoverage[] = projects + .map((name) => { + const projectRoot = getProjectRoot(name) + return { + name, + result: readLineCoverage(name, projectRoot), + } + }) + .sort((a, b) => a.name.localeCompare(b.name)) + +const missing = entries.filter((entry) => entry.result.status === 'missing') +if (missing.length > 0) { + console.warn( + `Coverage summary missing for ${missing.length} package(s): ${missing.map((entry) => entry.name).join(', ')}` + ) +} + +printReport(entries) diff --git a/sdk/dapp-sdk/vitest.config.ts b/sdk/dapp-sdk/vitest.config.ts index 6e2d81cef..7d12496d7 100644 --- a/sdk/dapp-sdk/vitest.config.ts +++ b/sdk/dapp-sdk/vitest.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ include: ['src/**/*.ts'], exclude: ['src/integration-test'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/sdk/wallet-sdk/vitest.config.ts b/sdk/wallet-sdk/vitest.config.ts index 6d6980ae1..75c772c1b 100644 --- a/sdk/wallet-sdk/vitest.config.ts +++ b/sdk/wallet-sdk/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { include: ['src/**/*.ts'], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/wallet-gateway/remote/vitest.config.ts b/wallet-gateway/remote/vitest.config.ts index 83a496dd9..f838b4d0d 100644 --- a/wallet-gateway/remote/vitest.config.ts +++ b/wallet-gateway/remote/vitest.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ 'src/**/rpc-gen/**', ], provider: 'v8', - reporter: ['text', 'html', 'lcov'], + reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { lines: 0, functions: 0, diff --git a/yarn.lock b/yarn.lock index cf4ce1cc3..dd8dfd4f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,11 +1751,9 @@ __metadata: "@types/fs-extra": "npm:^11.0.4" "@types/lodash": "npm:^4.17.24" "@types/node": "npm:^25.3.3" - "@vitest/browser-playwright": "npm:^4.1.2" "@vitest/coverage-v8": "npm:^4.1.2" fs-extra: "npm:^11.3.3" lodash: "npm:^4.18.1" - playwright: "npm:^1.58.2" tsup: "npm:^8.5.1" tweetnacl: "npm:^1.0.3" tweetnacl-util: "npm:^0.15.1"