Skip to content

ci(mutation): test only the changed mutants on pull requests with Stryker's incremental mode - #613

Merged
hyanmandian merged 3 commits into
mainfrom
claude/stryker-incremental
Sep 29, 2026
Merged

hyanmandian merged 3 commits into
mainfrom
claude/stryker-incremental

Conversation

@hyanmandian

@hyanmandian hyanmandian commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

The Stryker job took about 17 minutes on every pull request, because it tested all ~5,300 mutants every time. This PR turns on Stryker's incremental mode in CI.

  • Pull requests run stryker run --incremental. The job restores reports/stryker-incremental.json from the cache, trying the PR's own last run first and then main's. Only mutants whose results may have changed are tested again; every other result is reused.
  • main keeps testing every mutant (--incremental --force) before publishing the score to the dashboard. That run also rebuilds the baseline the pull requests start from, so neither the score nor the baseline carries an old result.
  • The results are saved even when a mutant survives, since the results of the mutants that were tested stay valid.

Safety: what Stryker can't see

Stryker reuses a mutant's result when the mutant's own code and the source of its covering tests did not change. It looks at nothing else, so three gaps are covered here:

  1. Files that are not mutated. The cache key includes a hash of the files that can change behaviour without Stryker noticing. When any of them changes, no earlier result matches the key, and every mutant runs again:
    • src/**/constants.ts and src/_internals/constants/**
    • src/_internals/test/**
    • src/index.ts
    • package-lock.json
    • stryker.config.json
    • vite.config.ts
  2. Imported helpers. Before each PR run, scripts/prune-stryker-incremental.ts compares every mutated file with the source saved in the report. Every mutant of a file that changed, or that imports a changed file directly or through other files, is removed from the report and runs again. For example, a change to _internals/format reruns every format*.
  3. Tests that reach a changed file. A helper's mutant killed by the format-cpf tests would stay "killed" after format-cpf stops calling that helper, because the test's source did not change. The script also removes every mutant killed by a test in a test file that reaches a changed file. For a mutant with another status (timeout, survived), it removes the mutant if any such test covers it. For a killed mutant, only the killing test counts. index.test.ts imports every module and covers most mutants, so it sends back only the few it killed.

The previous sources are already in the report, so no git history is needed.

Checklist

  • My commit/PR title follows Conventional Commits.
  • I added or updated tests covering this change: CI-only change, no library code. The prune script was verified against real Stryker runs (below).
  • I updated the documentation in both docs/utilities.md and docs/pt-br/utilities.md: not applicable. CONTRIBUTING.md describes the new behaviour and how to run it locally.
  • npm run check passes locally (format, lint, types), and knip is clean.
  • I ran npm run build:llms: docs/ untouched.
  • This change does not introduce a breaking change.
  • This change does not add any runtime dependency.

Additional context

On CI:

Run Stryker
This PR's first run, no cache 17m36s, score 100
Second push, cache restored 1m05s, score 100 (5,276 killed, 43 timeout)

The time left is mostly the initial run of the whole test suite (perTest coverage), which Stryker always does.

The prune, checked locally with real Stryker runs on _internals/format + format-pis + format-cnae (53 mutants), after a change to _internals/format only:

Run Result
--incremental alone 53 of 53 mutant result(s) are reused: the helper change goes unseen
prune, then --incremental 0 of 53 mutant result(s) are reused: all rerun, score 100

Against a report covering every mutated file:

  • An unchanged tree drops nothing.
  • A change to _internals/format drops 27 whole files: the helper, every format* that uses it, and what imports those in turn (generate-pix-payload, get-pix-key-info, is-valid-pix-key, parse-cnpj). It also drops the other mutants killed by a test that reaches it.

The first push to main after merging seeds the baseline. The cache uses actions/cache/restore and actions/cache/save v6.1.0, pinned by SHA like the other actions.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH


Generated by Claude Code

…yker's incremental mode

The whole Stryker run took about 17 minutes on every pull request. Pull requests now run
`stryker run --incremental`, restoring reports/stryker-incremental.json from their own last
run or from main's, so only the mutants whose code or covering tests changed are tested.
Files Stryker cannot track (constants, test helpers, the lockfile, the configs) are hashed
into the cache key, so a change to one of them reruns every mutant. main keeps testing every
mutant (--force) before publishing the score, and rebuilds the baseline the pull requests
start from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
@vercel

vercel Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
brazilian-utils Ready Ready Preview Sep 29, 2026 4:15am UTC

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f28ccc8b-f124-42c4-8520-b762ca3df043

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@brazilian-utils/brazilian-utils@613

commit: 8accbb9

@github-actions

Copy link
Copy Markdown
Contributor

Tree-shaking report

✅ No bundle size impact. All 185 exports are the same size as on the base branch (full import 1985.9 KB, gzip 378.2 KB).

All exports (185)
Export Base Head Δ gzip
⚪ GetAddressInfoByCepError 966 B 966 B 0 B 600 B
⚪ GetAddressInfoByCepNotFoundError 1.0 KB 1.0 KB 0 B 619 B
⚪ GetAddressInfoByCepServiceError 1.0 KB 1.0 KB 0 B 617 B
⚪ GetAddressInfoByCepValidationError 1.0 KB 1.0 KB 0 B 620 B
⚪ GetCepInfoByAddressError 966 B 966 B 0 B 600 B
⚪ GetCepInfoByAddressNotFoundError 1.0 KB 1.0 KB 0 B 619 B
⚪ GetCepInfoByAddressValidationError 1.0 KB 1.0 KB 0 B 620 B
⚪ addBusinessDays 7.5 KB 7.5 KB 0 B 3.1 KB
⚪ capitalize 2.5 KB 2.5 KB 0 B 1.3 KB
⚪ convertCurrencyToWords 2.8 KB 2.8 KB 0 B 1.5 KB
⚪ convertDateToWords 3.2 KB 3.2 KB 0 B 1.7 KB
⚪ convertLicensePlateToMercosul 1.3 KB 1.3 KB 0 B 809 B
⚪ convertNumberToWords 2.4 KB 2.4 KB 0 B 1.3 KB
⚪ differenceInBusinessDays 7.3 KB 7.3 KB 0 B 3.0 KB
⚪ formatBoleto 1.4 KB 1.4 KB 0 B 837 B
⚪ formatCEP 1.2 KB 1.2 KB 0 B 776 B
⚪ formatCNPJ 1.4 KB 1.4 KB 0 B 856 B
⚪ formatCPF 1.3 KB 1.3 KB 0 B 806 B
⚪ formatCaepf 1.3 KB 1.3 KB 0 B 787 B
⚪ formatCei 1.3 KB 1.3 KB 0 B 786 B
⚪ formatCep 1.2 KB 1.2 KB 0 B 776 B
⚪ formatCertidao 1.3 KB 1.3 KB 0 B 789 B
⚪ formatCest 1.3 KB 1.3 KB 0 B 815 B
⚪ formatCid10 1.3 KB 1.3 KB 0 B 803 B
⚪ formatCnae 1.2 KB 1.2 KB 0 B 782 B
⚪ formatCnh 1.3 KB 1.3 KB 0 B 804 B
⚪ formatCno 1.3 KB 1.3 KB 0 B 787 B
⚪ formatCnpj 1.4 KB 1.4 KB 0 B 856 B
⚪ formatCns 1.3 KB 1.3 KB 0 B 780 B
⚪ formatCpf 1.3 KB 1.3 KB 0 B 806 B
⚪ formatCurrency 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ formatIban 1.1 KB 1.1 KB 0 B 697 B
⚪ formatLegalNature 1.2 KB 1.2 KB 0 B 777 B
⚪ formatLicensePlate 1.2 KB 1.2 KB 0 B 738 B
⚪ formatNbs 1.3 KB 1.3 KB 0 B 812 B
⚪ formatNcm 1.2 KB 1.2 KB 0 B 780 B
⚪ formatNfeKey 1.3 KB 1.3 KB 0 B 784 B
⚪ formatPassport 1.0 KB 1.0 KB 0 B 644 B
⚪ formatPhone 3.4 KB 3.4 KB 0 B 1.6 KB
⚪ formatPis 1.3 KB 1.3 KB 0 B 806 B
⚪ formatProcessoJuridico 1.3 KB 1.3 KB 0 B 785 B
⚪ formatSuframa 1.3 KB 1.3 KB 0 B 816 B
⚪ formatVoterId 1.5 KB 1.5 KB 0 B 875 B
⚪ generateBoleto 2.1 KB 2.1 KB 0 B 1.2 KB
⚪ generateCNPJ 1.6 KB 1.6 KB 0 B 970 B
⚪ generateCPF 1.4 KB 1.4 KB 0 B 879 B
⚪ generateCep 984 B 984 B 0 B 611 B
⚪ generateCnh 1.4 KB 1.4 KB 0 B 830 B
⚪ generateCnpj 1.6 KB 1.6 KB 0 B 970 B
⚪ generateCpf 1.4 KB 1.4 KB 0 B 879 B
⚪ generateLegalNature 5.9 KB 5.9 KB 0 B 2.1 KB
⚪ generateLicensePlate 1.1 KB 1.1 KB 0 B 693 B
⚪ generatePassport 1.1 KB 1.1 KB 0 B 656 B
⚪ generatePhone 1.5 KB 1.5 KB 0 B 900 B
⚪ generatePis 1.2 KB 1.2 KB 0 B 744 B
⚪ generatePixPayload 6.3 KB 6.3 KB 0 B 2.8 KB
⚪ generateProcessoJuridico 1.4 KB 1.4 KB 0 B 871 B
⚪ generateRenavam 1.2 KB 1.2 KB 0 B 761 B
⚪ generateSuframa 1.3 KB 1.3 KB 0 B 808 B
⚪ generateVoterId 1.7 KB 1.7 KB 0 B 1023 B
⚪ getAddressInfoByCep 4.1 KB 4.1 KB 0 B 1.9 KB
⚪ getAreaCodeInfo 3.9 KB 3.9 KB 0 B 1.4 KB
⚪ getAreaCodesByState 1.6 KB 1.6 KB 0 B 919 B
⚪ getBankByCode 38.6 KB 38.6 KB 0 B 9.8 KB
⚪ getBankByIspb 38.6 KB 38.6 KB 0 B 9.8 KB
⚪ getBanks 38.4 KB 38.4 KB 0 B 9.6 KB
⚪ getBoletoInfo 3.1 KB 3.1 KB 0 B 1.6 KB
⚪ getCbo 119.1 KB 119.1 KB 0 B 30.7 KB
⚪ getCepInfoByAddress 2.7 KB 2.7 KB 0 B 1.4 KB
⚪ getCertidaoInfo 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ getCest 117.8 KB 117.8 KB 0 B 26.8 KB
⚪ getCfop 68.9 KB 68.9 KB 0 B 6.9 KB
⚪ getCid10 1030.4 KB 1030.4 KB 0 B 146.9 KB
⚪ getCities 154.3 KB 154.3 KB 0 B 49.9 KB
⚪ getClassTrib 50.8 KB 50.8 KB 0 B 9.6 KB
⚪ getCnae 93.9 KB 93.9 KB 0 B 21.2 KB
⚪ getCnpjInfo 1.8 KB 1.8 KB 0 B 1011 B
⚪ getCpfInfo 1.7 KB 1.7 KB 0 B 1000 B
⚪ getCstIbsCbs 1.8 KB 1.8 KB 0 B 1009 B
⚪ getFormatLicensePlate 1.1 KB 1.1 KB 0 B 693 B
⚪ getGtinInfo 1.7 KB 1.7 KB 0 B 1.0 KB
⚪ getHolidays 6.3 KB 6.3 KB 0 B 2.6 KB
⚪ getIbanInfo 1.6 KB 1.6 KB 0 B 954 B
⚪ getLegalNature 6.3 KB 6.3 KB 0 B 2.3 KB
⚪ getLegalNatures 5.9 KB 5.9 KB 0 B 2.1 KB
⚪ getLegalNaturesByCategory 6.5 KB 6.5 KB 0 B 2.4 KB
⚪ getMunicipalities 156.4 KB 156.4 KB 0 B 50.3 KB
⚪ getMunicipality 154.9 KB 154.9 KB 0 B 50.3 KB
⚪ getMunicipalityByCode 156.5 KB 156.5 KB 0 B 50.4 KB
⚪ getNbs 81.8 KB 81.8 KB 0 B 13.8 KB
⚪ getNfeKeyInfo 2.7 KB 2.7 KB 0 B 1.5 KB
⚪ getNfseKeyInfo 3.2 KB 3.2 KB 0 B 1.7 KB
⚪ getPixKeyInfo 4.5 KB 4.5 KB 0 B 2.0 KB
⚪ getPixPayloadInfo 2.9 KB 2.9 KB 0 B 1.5 KB
⚪ getServiceItem 27.2 KB 27.2 KB 0 B 8.9 KB
⚪ getStateByCep 4.5 KB 4.5 KB 0 B 1.5 KB
⚪ getStateByIbgeCode 3.2 KB 3.2 KB 0 B 1.1 KB
⚪ getStateCodeByName 3.2 KB 3.2 KB 0 B 1.1 KB
⚪ getStateNameByCode 3.1 KB 3.1 KB 0 B 1.0 KB
⚪ getStates 3.0 KB 3.0 KB 0 B 1019 B
⚪ getTimezoneByState 1.6 KB 1.6 KB 0 B 810 B
⚪ isBusinessDay 6.7 KB 6.7 KB 0 B 2.8 KB
⚪ isHoliday 6.6 KB 6.6 KB 0 B 2.7 KB
⚪ isValidBankAccount 7.4 KB 7.4 KB 0 B 2.9 KB
⚪ isValidBoleto 2.4 KB 2.4 KB 0 B 1.3 KB
⚪ isValidCEP 984 B 984 B 0 B 611 B
⚪ isValidCNPJ 1.6 KB 1.6 KB 0 B 915 B
⚪ isValidCPF 1.3 KB 1.3 KB 0 B 806 B
⚪ isValidCaepf 1.5 KB 1.5 KB 0 B 913 B
⚪ isValidCbo 119.2 KB 119.2 KB 0 B 30.7 KB
⚪ isValidCei 1.5 KB 1.5 KB 0 B 900 B
⚪ isValidCep 984 B 984 B 0 B 611 B
⚪ isValidCertidao 1.6 KB 1.6 KB 0 B 938 B
⚪ isValidCest 116.6 KB 116.6 KB 0 B 26.4 KB
⚪ isValidCfop 68.9 KB 68.9 KB 0 B 6.9 KB
⚪ isValidCid10 27.0 KB 27.0 KB 0 B 7.4 KB
⚪ isValidClassTrib 2.6 KB 2.6 KB 0 B 1.1 KB
⚪ isValidCnae 94.0 KB 94.0 KB 0 B 21.2 KB
⚪ isValidCnh 1.4 KB 1.4 KB 0 B 856 B
⚪ isValidCno 1.5 KB 1.5 KB 0 B 902 B
⚪ isValidCnpj 1.6 KB 1.6 KB 0 B 915 B
⚪ isValidCns 1.5 KB 1.5 KB 0 B 924 B
⚪ isValidCpf 1.3 KB 1.3 KB 0 B 806 B
⚪ isValidCreditCard 1.4 KB 1.4 KB 0 B 897 B
⚪ isValidCsosn 1.2 KB 1.2 KB 0 B 737 B
⚪ isValidCst 1.8 KB 1.8 KB 0 B 1.0 KB
⚪ isValidCstIbsCbs 1.7 KB 1.7 KB 0 B 978 B
⚪ isValidEmail 1.0 KB 1.0 KB 0 B 623 B
⚪ isValidGtin 1.3 KB 1.3 KB 0 B 836 B
⚪ isValidIE 5.7 KB 5.7 KB 0 B 2.2 KB
⚪ isValidIban 1.3 KB 1.3 KB 0 B 837 B
⚪ isValidIe 5.7 KB 5.7 KB 0 B 2.2 KB
⚪ isValidLandlinePhone 1.5 KB 1.5 KB 0 B 933 B
⚪ isValidLegalNature 5.8 KB 5.8 KB 0 B 2.1 KB
⚪ isValidLicensePlate 1.1 KB 1.1 KB 0 B 704 B
⚪ isValidMobilePhone 1.6 KB 1.6 KB 0 B 972 B
⚪ isValidNbs 81.8 KB 81.8 KB 0 B 13.8 KB
⚪ isValidNcm 114.2 KB 114.2 KB 0 B 24.6 KB
⚪ isValidNfeKey 2.7 KB 2.7 KB 0 B 1.5 KB
⚪ isValidNfseKey 2.9 KB 2.9 KB 0 B 1.5 KB
⚪ isValidPIS 1.2 KB 1.2 KB 0 B 785 B
⚪ isValidPassport 1.0 KB 1.0 KB 0 B 655 B
⚪ isValidPhone 2.6 KB 2.6 KB 0 B 1.3 KB
⚪ isValidPis 1.2 KB 1.2 KB 0 B 785 B
⚪ isValidPixKey 4.6 KB 4.6 KB 0 B 2.1 KB
⚪ isValidPixPayload 2.9 KB 2.9 KB 0 B 1.5 KB
⚪ isValidProcessoJuridico 1.3 KB 1.3 KB 0 B 788 B
⚪ isValidRegistroProfissional 1.6 KB 1.6 KB 0 B 964 B
⚪ isValidRenavam 1.3 KB 1.3 KB 0 B 836 B
⚪ isValidServiceItem 27.1 KB 27.1 KB 0 B 8.8 KB
⚪ isValidServicePhone 1.5 KB 1.5 KB 0 B 846 B
⚪ isValidSuframa 1.4 KB 1.4 KB 0 B 884 B
⚪ isValidVin 1.6 KB 1.6 KB 0 B 996 B
⚪ isValidVoterId 1.6 KB 1.6 KB 0 B 900 B
⚪ parseBoleto 1020 B 1020 B 0 B 635 B
⚪ parseCaepf 1003 B 1003 B 0 B 622 B
⚪ parseCbo 1002 B 1002 B 0 B 621 B
⚪ parseCei 1003 B 1003 B 0 B 620 B
⚪ parseCep 1002 B 1002 B 0 B 621 B
⚪ parseCertidao 1003 B 1003 B 0 B 622 B
⚪ parseCest 1.0 KB 1.0 KB 0 B 657 B
⚪ parseCfop 1002 B 1002 B 0 B 621 B
⚪ parseCid10 1.0 KB 1.0 KB 0 B 637 B
⚪ parseCnae 1002 B 1002 B 0 B 621 B
⚪ parseCnh 1003 B 1003 B 0 B 622 B
⚪ parseCno 1003 B 1003 B 0 B 620 B
⚪ parseCnpj 1.1 KB 1.1 KB 0 B 669 B
⚪ parseCns 1003 B 1003 B 0 B 622 B
⚪ parseCpf 1003 B 1003 B 0 B 622 B
⚪ parseCurrency 1.4 KB 1.4 KB 0 B 882 B
⚪ parseIban 1.0 KB 1.0 KB 0 B 639 B
⚪ parseLegalNature 1002 B 1002 B 0 B 621 B
⚪ parseLicensePlate 1.0 KB 1.0 KB 0 B 639 B
⚪ parseNcm 1002 B 1002 B 0 B 621 B
⚪ parseNfeKey 1.0 KB 1.0 KB 0 B 660 B
⚪ parseNfseKey 1.1 KB 1.1 KB 0 B 689 B
⚪ parsePassport 1.0 KB 1.0 KB 0 B 637 B
⚪ parsePhone 1.1 KB 1.1 KB 0 B 708 B
⚪ parsePis 1003 B 1003 B 0 B 622 B
⚪ parseProcessoJuridico 1003 B 1003 B 0 B 622 B
⚪ parseSuframa 1.0 KB 1.0 KB 0 B 657 B
⚪ parseVoterId 1.0 KB 1.0 KB 0 B 650 B
⚪ removeAccents 953 B 953 B 0 B 594 B
⚪ subBusinessDays 7.5 KB 7.5 KB 0 B 3.1 KB
⚪ toStandardSchema 1.1 KB 1.1 KB 0 B 714 B
How this is measured

Every export is imported alone into an esbuild consumer bundle (minified, tree-shaken) built from the head and from the base of this pull request; the sizes are the resulting bundles, gzip is their gzipped size. 🔴 marks a regression: a pre-existing export that grew more than 20% and more than 256 B, or the bundle importing every pre-existing export growing more than 5%. 🟡 is growth under the threshold, 🟢 a decrease, ⚪ no change, 🆕 an export that does not exist on the base (never a regression), 🗑️ an export that was removed. An intentional increase is accepted with the tree-shaking: accepted label.

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9fd74a6) to head (8accbb9).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #613   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          218       218           
  Lines         2252      2252           
  Branches       673       673           
=========================================
  Hits          2252      2252           
Flag Coverage Δ
node 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…file

Stryker's incremental mode reuses a mutant's result when the mutant's own code and its covering
tests did not change, without looking at the files that code imports: a change to
`_internals/format` kept every `format*` mutant's old result (53 of 53 reused on a local run).
`scripts/prune-stryker-incremental.ts` compares every mutated file with the source saved in the
report, follows the relative imports back to every file that depends on a changed one, and drops
those files from the report, so Stryker tests all of their mutants again (0 of 53 reused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…at reaches a changed file

A mutant in an unchanged helper, killed by the tests of a function that changed, kept its
"killed" result even when the function no longer calls the helper: the killing test's source did
not change. The prune script now also drops every mutant killed by a test in a test file that
imports a changed file (or, for a mutant with another status, covered by one). Only the killing
test counts for a killed mutant, so `index.test.ts`, which imports every module and covers most
mutants, sends back only the few it killed. Whole files are still dropped when they changed or
import a changed file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
@hyanmandian
hyanmandian merged commit 56501e4 into main Sep 29, 2026
30 checks passed

This branch was successfully deployed

1 active deployment
Preview — 8accbb9f Deployed Sep 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants