From b5c2154427cfd3dd148e94d8cb1652db701eb0d2 Mon Sep 17 00:00:00 2001 From: boalambo Date: Sat, 17 Oct 2026 12:31:32 +0100 Subject: [PATCH 1/3] test(stellar): stabilize fuzz harness and add PR sample - Increase testTimeout to 60 minutes to accommodate 100k-run fuzz campaign - Add test:fuzz:sample script with fixed seed for quick PR checks - Add onFailure callback to print seed and replay instructions on failure - Update CI to run sample on PRs, keep full campaign scheduled nightly --- .github/workflows/ci.yml | 4 +++ package.json | 1 + test/chains/stellar/properties.test.ts | 44 +++++++++++++++++++------- vitest.config.ts | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb0130b..5b42a66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,10 @@ jobs: # test() bodies in this repo, so it cannot be trusted as a gate. - run: pnpm test:types - run: pnpm test + # Run a small fixed-seed sample of Stellar property tests on every PR + - name: Stellar property test sample + if: github.event_name == 'pull_request' + run: pnpm test:fuzz:sample entrypoints: name: Package entry point smoke tests diff --git a/package.json b/package.json index 86308ea..512ef2c 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "format:check": "prettier --check .", "prepare": "husky", "test:fuzz": "FC_RUNS=100000 vitest run test/chains/stellar/properties.test.ts", + "test:fuzz:sample": "FC_RUNS=100 FC_SEED=12345 vitest run test/chains/stellar/properties.test.ts", "test:leak": "vitest run test/leaks/scan-leak.test.ts", "test:heap-leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leaks/heap-snapshot.test.ts --pool=threads", "size": "size-limit", diff --git a/test/chains/stellar/properties.test.ts b/test/chains/stellar/properties.test.ts index 8299e8d..30a0069 100644 --- a/test/chains/stellar/properties.test.ts +++ b/test/chains/stellar/properties.test.ts @@ -14,6 +14,26 @@ import { import { computeViewTag } from '../../../src/chains/stellar/stealth'; const FC_RUNS = process.env.FC_RUNS ? parseInt(process.env.FC_RUNS, 10) : 1000; +const FC_SEED = process.env.FC_SEED ? parseInt(process.env.FC_SEED, 10) : undefined; + +// Configure fast-check with seed if provided +const configure = () => { + if (FC_SEED !== undefined) { + console.log(`\nšŸ”¬ Using fixed seed: ${FC_SEED}`); + console.log(` Replay with: FC_SEED=${FC_SEED} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`); + return { seed: FC_SEED }; + } + return {}; +}; + +// Custom error handler to capture seed on failure +const onFailure = (error: Error, seed: number) => { + console.error(`\nāŒ Property test failed with seed: ${seed}`); + console.error(` Replay with: FC_SEED=${seed} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`); + throw error; +}; + + // ───────────────────────────────────────────────────────────── // Helpers @@ -48,7 +68,7 @@ describe('scalar algebra sanity', () => { const r2 = (b + a) % L; expect(r1).toBe(r2); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -57,7 +77,7 @@ describe('scalar algebra sanity', () => { fc.property(scalarAnyArb, (a) => { expect((a + 0n) % L).toBe(a % L); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); @@ -73,7 +93,7 @@ describe('scalar encoding roundtrip', () => { const normalized = a % L; expect(bytesToScalar(scalarToBytes(normalized)) % L).toBe(normalized); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -82,7 +102,7 @@ describe('scalar encoding roundtrip', () => { fc.property(scalarAnyArb, (a) => { expect(scalarToBytes(a)).toHaveLength(32); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); @@ -99,7 +119,7 @@ describe('seedToScalar stability', () => { const b = seedToScalar(seed); expect(a).toBe(b); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -116,7 +136,7 @@ describe('seedToScalar stability', () => { // sanity: still a valid scalar space expect(s < 2n ** 256n).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); @@ -140,7 +160,7 @@ describe('stealth pubkey correctness', () => { expect(lhs.equals(rhs)).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -159,7 +179,7 @@ describe('stealth pubkey correctness', () => { expect(derived).toEqual(expected); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); @@ -175,7 +195,7 @@ describe('view tag distribution sanity', () => { const tag = computeViewTag(secret); expect(tag >= 0 && tag <= 255).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); @@ -195,7 +215,7 @@ describe('signWithScalar correctness', () => { expect(sig).toHaveLength(64); expect(verifySignature(sig, msg, pub)).toBe(true); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -210,7 +230,7 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, m2, pub)).toBe(false); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); @@ -227,7 +247,7 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, msg, pub2)).toBe(false); }), - { numRuns: FC_RUNS }, + { numRuns: FC_RUNS, ...configure(), onFailure }, ); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index fc32b91..1e33562 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ 'test/chains/evm/vectors.test.ts', 'test/chains/solana/vectors.test.ts', ], - testTimeout: 1200000, // 20 minutes for high-run nightly fuzz tests + testTimeout: 3600000, // 60 minutes for high-run nightly fuzz tests (100k runs takes ~6.5min + reporting overhead) }, benchmark: { include: ['test/chains/**/bench/**/*.bench.ts', 'test/bench/**/*.bench.ts'], From ee026faaa39e7ae87099bfc0cf0eb72adb018c27 Mon Sep 17 00:00:00 2001 From: boalambo Date: Sat, 17 Oct 2026 12:49:22 +0100 Subject: [PATCH 2/3] style(stellar): format properties test with prettier --- test/chains/stellar/properties.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/chains/stellar/properties.test.ts b/test/chains/stellar/properties.test.ts index 30a0069..fbbf0de 100644 --- a/test/chains/stellar/properties.test.ts +++ b/test/chains/stellar/properties.test.ts @@ -20,7 +20,9 @@ const FC_SEED = process.env.FC_SEED ? parseInt(process.env.FC_SEED, 10) : undefi const configure = () => { if (FC_SEED !== undefined) { console.log(`\nšŸ”¬ Using fixed seed: ${FC_SEED}`); - console.log(` Replay with: FC_SEED=${FC_SEED} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`); + console.log( + ` Replay with: FC_SEED=${FC_SEED} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`, + ); return { seed: FC_SEED }; } return {}; @@ -29,12 +31,12 @@ const configure = () => { // Custom error handler to capture seed on failure const onFailure = (error: Error, seed: number) => { console.error(`\nāŒ Property test failed with seed: ${seed}`); - console.error(` Replay with: FC_SEED=${seed} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`); + console.error( + ` Replay with: FC_SEED=${seed} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`, + ); throw error; }; - - // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── From 2c5ecfec8ffb51fbd3b82297ef9b3950e7ecb4d7 Mon Sep 17 00:00:00 2001 From: boalambo Date: Sun, 18 Oct 2026 09:43:45 +0100 Subject: [PATCH 3/3] fix(stellar): resolve merge conflict regressions - Remove duplicate test:leak script in package.json - Replace invalid onFailure callback with valid reporter parameter - Add FC_PATH support for replay with counterexample path - Add proof test (gated by FC_PROOF_TEST env var) to demonstrate replay output - Add explicit type annotation to proof test parameter --- package.json | 2 +- test/chains/stellar/properties.test.ts | 68 ++++++++++++++++++-------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 60405c2..1ee548e 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "prepare": "husky", "test:fuzz": "FC_RUNS=100000 vitest run test/chains/stellar/properties.test.ts", "test:fuzz:sample": "FC_RUNS=100 FC_SEED=12345 vitest run test/chains/stellar/properties.test.ts", - "test:leak": "vitest run test/leaks/scan-leak.test.ts", + "test:fuzz:proof": "FC_PROOF_TEST=1 vitest run test/chains/stellar/properties.test.ts", "test:leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leaks/scan-leak.test.ts --pool=threads", "test:heap-leak": "node --expose-gc ./node_modules/vitest/vitest.mjs run test/leaks/heap-snapshot.test.ts --pool=threads", "size": "size-limit", diff --git a/test/chains/stellar/properties.test.ts b/test/chains/stellar/properties.test.ts index fbbf0de..aff25eb 100644 --- a/test/chains/stellar/properties.test.ts +++ b/test/chains/stellar/properties.test.ts @@ -15,6 +15,7 @@ import { computeViewTag } from '../../../src/chains/stellar/stealth'; const FC_RUNS = process.env.FC_RUNS ? parseInt(process.env.FC_RUNS, 10) : 1000; const FC_SEED = process.env.FC_SEED ? parseInt(process.env.FC_SEED, 10) : undefined; +const FC_PATH = process.env.FC_PATH; // Configure fast-check with seed if provided const configure = () => { @@ -23,18 +24,27 @@ const configure = () => { console.log( ` Replay with: FC_SEED=${FC_SEED} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`, ); - return { seed: FC_SEED }; + const config: Record = { seed: FC_SEED }; + if (FC_PATH !== undefined) { + config.path = FC_PATH; + } + return config; } return {}; }; -// Custom error handler to capture seed on failure -const onFailure = (error: Error, seed: number) => { - console.error(`\nāŒ Property test failed with seed: ${seed}`); - console.error( - ` Replay with: FC_SEED=${seed} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts\n`, - ); - throw error; +// Custom reporter to print seed and path on failure +const reporter = (runDetails: fc.RunDetails) => { + if (runDetails.failed) { + const seed = runDetails.seed; + const path = runDetails.counterexamplePath; + console.error(`\nāŒ Property test failed`); + console.error(` Seed: ${seed}`); + console.error(` Path: ${path}`); + const replayCmd = `FC_SEED=${seed} FC_PATH=${path} FC_RUNS=${FC_RUNS} pnpm test test/chains/stellar/properties.test.ts`; + console.error(` Replay with: ${replayCmd}\n`); + throw new Error(fc.defaultReportMessage(runDetails)); + } }; // ───────────────────────────────────────────────────────────── @@ -70,7 +80,7 @@ describe('scalar algebra sanity', () => { const r2 = (b + a) % L; expect(r1).toBe(r2); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -79,7 +89,7 @@ describe('scalar algebra sanity', () => { fc.property(scalarAnyArb, (a) => { expect((a + 0n) % L).toBe(a % L); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -95,7 +105,7 @@ describe('scalar encoding roundtrip', () => { const normalized = a % L; expect(bytesToScalar(scalarToBytes(normalized)) % L).toBe(normalized); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -104,7 +114,7 @@ describe('scalar encoding roundtrip', () => { fc.property(scalarAnyArb, (a) => { expect(scalarToBytes(a)).toHaveLength(32); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -121,7 +131,7 @@ describe('seedToScalar stability', () => { const b = seedToScalar(seed); expect(a).toBe(b); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -138,7 +148,7 @@ describe('seedToScalar stability', () => { // sanity: still a valid scalar space expect(s < 2n ** 256n).toBe(true); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -162,7 +172,7 @@ describe('stealth pubkey correctness', () => { expect(lhs.equals(rhs)).toBe(true); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -181,7 +191,7 @@ describe('stealth pubkey correctness', () => { expect(derived).toEqual(expected); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -197,7 +207,7 @@ describe('view tag distribution sanity', () => { const tag = computeViewTag(secret); expect(tag >= 0 && tag <= 255).toBe(true); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); @@ -217,7 +227,7 @@ describe('signWithScalar correctness', () => { expect(sig).toHaveLength(64); expect(verifySignature(sig, msg, pub)).toBe(true); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -232,7 +242,7 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, m2, pub)).toBe(false); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); @@ -249,7 +259,25 @@ describe('signWithScalar correctness', () => { expect(verifySignature(sig, msg, pub2)).toBe(false); }), - { numRuns: FC_RUNS, ...configure(), onFailure }, + { numRuns: FC_RUNS, ...configure(), reporter }, ); }); }); + +// ───────────────────────────────────────────────────────────── +// 7. proof test: demonstrates replay output on failure +// ───────────────────────────────────────────────────────────── + +if (process.env.FC_PROOF_TEST === '1') { + describe('replay output proof test', () => { + test('deliberately fails to prove replay output is emitted', () => { + fc.assert( + fc.property(scalarArb, (a: bigint) => { + // This will always fail + expect(a).toBe(0n); + }), + { numRuns: 10, seed: 42, reporter }, + ); + }); + }); +}