@fluxomize/hunch
v0.2.0
Published
Playwright's smart hunch about which tests to run. Predictive test selection with ML to cut CI time and carbon footprint.
Downloads
668
Maintainers
Readme
Status: beta. All three pieces work: the reporter records history,
hunch trainbuilds a model from it, andhunch runuses the model to run a subset of your suite. It has not yet been used in anger on a large real-world suite, which is the next thing that has to happen.
The problem
Your Playwright suite takes twenty minutes. You changed one CSS file. You still pay for all twenty minutes, on every push, on every pull request, and so does the planet: every minute of runner time is energy burned and CO₂ emitted.
Playwright already ships two good answers to part of this. --last-failed reruns what broke
last time. --only-changed runs what statically depends on the files you touched. Both are
useful, and Hunch is built to sit next to them rather than replace them.
But static dependency analysis only knows what could break. It does not know what usually
breaks. A change to styles/main.css might statically touch 40 UI tests while, historically,
only 3 of those 40 ever fail because of it.
The idea
Hunch learns that difference from your own history.
- Collect. A Playwright reporter records every test run, with its result, its duration and
the files that changed in that commit, into a plain
.hunch/history.jsonlin your repo. - Train.
hunch trainbuilds a baseline logistic regression model that predicts which tests are likely to fail for a given set of changed files. - Run.
hunch runscores your suite against the current diff and runs only the tests worth running. - Report. After each run you get the time, energy (kWh) and carbon (gCO₂eq) you saved.
No Python. No server. No dashboard. No network calls. Everything runs locally, on your machine or on your runner, and the data stays in your repository where you can read it.
Quickstart
pnpm add -D @fluxomize/hunchAdd the reporter to your Playwright config so Hunch starts learning from your normal runs:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['list'], ['@fluxomize/hunch/reporter']],
});That is the whole setup. Run your suite the way you always do, and Hunch writes one line per
test per run into .hunch/history.jsonl at the root of your repository:
{
"testId": "tests/login.spec.ts > login > signs in",
"file": "tests/login.spec.ts",
"project": "chromium",
"duration": 1243,
"status": "passed",
"timestamp": "2026-09-17T05:11:47.998Z",
"commitSha": "390f1c2e66160f62d0a8d75d56da77be15d38dde",
"changedFiles": ["src/login.ts"],
"branch": "main",
"retry": 0
}Commit that file. It is the training data, it is meant to be shared with your team, and it is plain text you can read, grep and diff.
Once you have around 20 to 30 runs of history, train a model from it:
npx hunch trainIt tells you what it learned and how well, measured on runs it never saw:
Trained on 40 runs, 195 examples.
Quality at the usual 50% cutoff, measured on the most recent runs the model never saw.
precision 100.0% of the tests it flags, this many really fail
recall 53.3% of the tests that fail, this many get flagged
F1 69.6%
Weights, largest influence first:
-5.751 recentFailureRate
+5.032 coChangeFailureRate
+2.739 changesetSize
...The model lands in .hunch/model.json as a readable list of feature names and weights. If it
ever makes a choice you disagree with, that file is where you go to find out why.
Then run only what the model thinks is worth running:
npx hunch run -- --project=chromiumSelected 47 of 210 tests, from 3 changed file(s).
2 of them have no history yet and are run regardless, because nothing is known about them.
... playwright output ...
Skipped 163 test(s), historically 4m 12s of machine time.
Estimated saving: 4.55 Wh, 2.16 gCO2eq.
Estimates, from your own history and the documented default assumptions about runner
power draw and grid carbon intensity. Not measurements.Anything after -- goes straight to playwright test. Use --dry-run to see the selection
without running anything.
While you wait
Training needs weeks of history, and a tool that says nothing for weeks is indistinguishable
from a broken one. hunch stats answers the two questions you actually have during that time:
npx hunch statsHistory: 42 runs, 210 tests, 8820 records.
Collected from 2026-08-04 to 2026-09-17.
Enough history to train. Run `hunch train`.
Fails most often:
31% tests/checkout.spec.ts > checkout > applies a discount code
12% tests/auth.spec.ts > auth > signs in with SSO
Flakiest:
18% tests/search.spec.ts > search > shows suggestions
Slowest:
14.2s tests/checkout.spec.ts > checkout > completes an order
Model: trained 2026-09-17 on 6300 examples.
precision 81.0%, recall 74.2%, F1 77.4%
Strongest signals: coChangeFailureRate +4.21, failureRate +2.05, flakyRate -1.88That ranking is useful on its own, before any prediction happens. The flakiest list in particular is usually a to-do list somebody has been meaning to write down.
| Option | Default | What it does |
| --- | --- | --- |
| --ratio <n> | 0.3 | Fraction of the suite to run |
| --min-tests <n> | 5 | Never run fewer than this many |
| --diff <range> | HEAD~1..HEAD | Git range used to work out what changed |
| --dry-run | off | Print the selection and stop |
Two rules that keep it from being confidently wrong
A test with no history is always run. No history means no prediction, and skipping a test because nothing is known about it would silently drop everything you added this week, which is exactly the code most likely to be broken.
When it cannot do its job, it refuses instead of guessing. No model, no git repository, no tests found: it says which, and runs nothing. A selector that quietly falls back to a partial run is worse than one that stops, because you would believe you had been tested when you had not.
Hunch is also complementary to Playwright's own --last-failed. Running that first and Hunch
second gives you better coverage than either alone.
Configuration
Optional. Put hunch.config.json at the top of your repository, next to playwright.config.ts:
{
"selectionRatio": 0.3,
"minTests": 5,
"minRunsToTrain": 20,
"sustainability": {
"runnerWatts": 65,
"gridIntensity": 475
}
}Everything is optional and anything you leave out keeps its default. A command line flag beats the file, and the file beats the defaults.
The sustainability numbers are the ones worth setting. They are assumptions about hardware
Hunch has never seen. 65 W is a rough figure for a GitHub hosted runner, and 475 gCO₂eq/kWh is
a global average, which is the right shape of guess when the runner's location is unknown. A
self hosted runner in Brazil sits closer to 85 gCO₂eq/kWh because the grid is mostly
hydroelectric; the same suite is genuinely cleaner there, and Hunch has no way to know that on
its own. hunch run prints the values it used, so what you see is always what it assumed.
On CI
Two ready to copy workflows live in examples/github-actions:
one that runs the full suite on main and collects history, and one that runs Hunch's
selection on pull requests. The README there covers the two things people get wrong, both of
which silently produce a useless model rather than an error.
What the model looks at
Twelve features, all of them plain numbers you can inspect:
| Feature | What it captures |
| --- | --- |
| failureRate, recentFailureRate | How often this test fails, overall and lately |
| observationConfidence | How much history stands behind those rates |
| failureRecency | How long ago it last failed |
| flakyRate | How often it fails and then passes on retry |
| durationWeight | How slow it is |
| coChangeFailureRate, coChangeSupport | How often it failed when these exact files changed before |
| ownFileChanged | Whether the test's own file is in the diff |
| pathAffinity | How close the test sits to the changed files in the tree |
| changesetSize | How big the change is |
The two in bold are the reason this project exists. They are what a dependency graph cannot tell you: not what could break, but what has broken, for this change, before.
Known limitations
The baseline model has no regularisation. With a dozen features and the twenty to thirty runs this is designed for, features that measure similar things can end up with large opposing weights. The model still ranks tests sensibly, but do not read a single weight as if it were an independent finding. Quality is always reported on held out runs, so the numbers you see are not the model grading its own homework.
Reporter options
| Option | Default | What it does |
| --- | --- | --- |
| outputDir | .hunch | Where to write, relative to the repository root |
| verbose | false | Print what the reporter is doing. Off by default, so normal runs stay quiet |
| diffRange | HEAD~1..HEAD | Git revision range used to work out which files changed |
On CI you usually want diffRange to cover the whole pull request rather than the last commit:
reporter: [['list'], ['@fluxomize/hunch/reporter', { diffRange: 'origin/main...HEAD' }]];The reporter is built so it can never fail your suite. Outside a git repository, or if anything it touches goes wrong, it prints a warning and records nothing.
Why energy matters here
Test selection is usually sold as a speed feature. Speed is the side effect. The thing being saved is compute, and compute is electricity.
Hunch reports its savings in kWh and gCO₂eq using a deliberately simple and fully documented estimate, with configurable runner power draw and grid emission factor. It is an estimate, not a measurement, and the docs will always say so.
Design constraints
Things Hunch will deliberately not do in v1:
- Support test frameworks other than Playwright.
- Ship a dashboard, a web UI, a backend or an API.
- Require Python, at runtime or at build time.
- Use deep learning, code embeddings or anything you cannot inspect in a JSON file.
Contributing
Contributions are welcome, including from people who have never contributed to open source before. Start with CONTRIBUTING.md, and please read our Code of Conduct.
