Commit df389a9
authored
Add flaky-test-detection skill (#61)
## Description
Adds a new `flaky-test-detection` skill for MetaMask Mobile that
detects, prevents, and fixes flaky Jest unit tests. It provides a review
mode for scanning changed test files against known flakiness risk
patterns (async, timing, isolation, mock, state), and an audit mode that
queries GitHub Actions run history to rank historically flaky tests by
failure rate.
## Type of Change
- [x] New skill
## Skill Details (if adding a new skill)
**Provider Name:** MetaMask
**Skill Name:** flaky-test-detection
**Brief Description:** Detect, prevent, and fix flaky Jest unit tests in
MetaMask Mobile before they reach CI, via PR/diff review against a
risk-pattern table and a GitHub Actions history audit mode.
## Checklist
- [x] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guidelines
- [x] My skill follows the
[SKILL_TEMPLATE.md](.github/SKILL_TEMPLATE.md) format
- [x] I have tested this skill with an AI agent
- [x] My skill does not contain any secrets, private keys, or sensitive
data
- [x] I have added appropriate documentation
- [x] My changes don't break existing skills
## Testing
Ran the skill against MetaMask Mobile PR diffs and CI run history to
validate both review mode (pattern classification) and audit mode
(ranked flaky test output).
Example result from running on Metamask Mobile repo:
---
### Flaky test audit — Jest unit tests (`ci.yml`, last 150 CI runs)
Of 150 recent `ci.yml` runs, 31 failed overall; only 3 failed due to
actual Jest failures (the rest were infra: yarn install retries, build
issues, etc.). All 3 are confirmed flaky — same commit passed on a
separate re-run.
| Rank | Test file | Failures | Runs sampled | Rate | Category |
|---|---|---:|---:|---:|---|
| 1 |
`app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx`
| 1 | 150 | <1% | J9/J10 (see below) |
| 1 |
`app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.test.tsx`
| 1 | 150 | <1% | J9/J10 |
| 1 |
`app/components/Views/confirmations/components/info/contract-deployment/contract-deployment.test.tsx`
| 1 | 150 | <1% | J9/J10 |
| 2 | `app/selectors/assets/balances.test.ts` | 1 | 150 | <1% | J1/J3 |
| 2 |
`app/components/Views/confirmations/components/confirm/confirm-component.test.tsx`
| 1 | 150 | <1% | J1/J3 |
| 2 |
`app/components/Views/confirmations/hooks/gas/useGasFeeToken.test.ts` |
1 | 150 | <1% | J1/J3 |
| 2 |
`app/components/Views/confirmations/components/gas/gas-fee-token-toast/gas-fee-token-toast.test.tsx`
| 1 | 150 | <1% | J1/J3 |
| 2 |
`app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.test.tsx`
| 1 | 150 | <1% | J1/J3 |
| 3 | `app/core/Engine/Engine.test.ts` | 1 | 150 | <1% | untriaged
(unrelated PR retry) |
Individually every file's rate is below the 5% "monitor" threshold, but
the two ranked incidents matter more than the per-file rate:
**Incident 1** — `feat/stellar/ramp-onramp-support` PR, commit
`9953242`: run
[28582173232](https://github.com/MetaMask/metamask-mobile/actions/runs/28582173232)
failed, run
[28582184123](https://github.com/MetaMask/metamask-mobile/actions/runs/28582184123)
on the **exact same commit** passed. All 3 unrelated files threw
`TypeError: selectAccountByScope is not a function` in the same shard.
**Incident 2** — same PR, earlier commit `ff7051b`: run
[28577001112](https://github.com/MetaMask/metamask-mobile/actions/runs/28577001112)
— 5 unrelated files (selectors, a confirm hook, gas-fee-token
components) all threw the identical `TypeError: Cannot read properties
of undefined (reading 'some')`. In the same job log there's also
`ReferenceError: You are trying to import a file after the Jest
environment has been torn down` from `DeeplinkProtocolService.test.ts`
right before the cascade starts.
Both incidents share a signature: several **unrelated** test files in
the same worker/shard fail simultaneously with the identical generic
error, and the same commit is green elsewhere. That's not per-file
flakiness — it's cross-test contamination inside a shared Jest worker,
consistent with skill categories J1 (dangling async work bleeding past
test teardown — the "torn down" error is the smoking gun) and J9/J10
(module-level mock state not reset between files in the same worker,
causing `selectAccountByScope`/array-selector mocks to be undefined for
the next test).
Given the low per-file rate, I'd flag this as a worker-isolation issue
to dig into (likely a shared mock or setup file) rather than something
to fix per test file.
Root cause found in
`app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts`:
```65:77:app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts
public constructor() {
if (!this.isInitialized) {
this.init()
.then(() => {
this.isInitialized = true;
DevLogger.log('DeeplinkProtocolService:: initialized');
})
.catch((err) => {
this.isInitialized = false;
Logger.log(err, 'DeeplinkProtocolService:: error initializing');
});
}
}
```
The constructor fires an un-awaited `init()` promise chain as a side
effect. `DeeplinkProtocolService.test.ts`'s `beforeEach` calls `service
= new DeeplinkProtocolService();` 14+ times, and a constructor return
value can't be awaited — so every dangling `.then()/.catch()` callback
(referencing `DevLogger.log`/`Logger.log`, both `jest.mock()`'d)
resolves on a later microtask/macrotask. When the last of these fires
after this test file's suite is done and Jest has already torn down its
module environment (and possibly moved the worker to the next test
file), it throws exactly the observed `ReferenceError: You are trying to
import a file after the Jest environment has been torn down`, and can
corrupt shared worker state for whatever unrelated test runs next in
that worker — matching the cascading `TypeError: Cannot read properties
of undefined (reading 'some')` / `selectAccountByScope is not a
function` failures across totally unrelated files.
This is category J1 (missing await on async side effects) from the
skill.
**Proposed fix**: add an `afterEach` in
`DeeplinkProtocolService.test.ts` that flushes pending promises (using
the existing `flushPromises` helper from `app/util/test/utils.js`) so
the constructor's dangling `init()` chain always settles before the
test/file ends, instead of leaking into the next test file's
environment.
---
> [!NOTE]
> created https://consensyssoftware.atlassian.net/browse/MCWP-699 for
the above issue that the skill found.
## Additional Context
Includes `references/gh-analysis.md` (GitHub Actions audit procedure)
and `repos/metamask-mobile.md` (repo-specific flakiness pattern catalog,
J1–J10).
fixes https://consensyssoftware.atlassian.net/browse/MCWP-4731 parent 066e101 commit df389a9
3 files changed
Lines changed: 448 additions & 0 deletions
File tree
- domains/coding/skills/flaky-test-detection
- references
- repos
Lines changed: 113 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
0 commit comments