tewip-ai 0.64.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -70,7 +70,9 @@ const flakyShare = (test) => {
|
|
|
70
70
|
* quarantine needs the test to have failed in several runs, and to be unreliable rather than
|
|
71
71
|
* consistently wrong.
|
|
72
72
|
*/
|
|
73
|
-
|
|
73
|
+
/** The verdict for one test, and the sentence that justifies it. Exported so it can be tested
|
|
74
|
+
* against the shapes real projects produce rather than only through a whole report. */
|
|
75
|
+
export function decide(test, { minRuns, alwaysFailsAbove }) {
|
|
74
76
|
const rate = share(test);
|
|
75
77
|
const percent = `${Math.round(rate * 100)}% of the ${test.totalRuns} run(s) it was in`;
|
|
76
78
|
const realBugs = test.categories.find((c) => c.category === 'REAL_BUG')?.count ?? 0;
|
|
@@ -80,6 +82,27 @@ function decide(test, { minRuns, alwaysFailsAbove }) {
|
|
|
80
82
|
if (rate > alwaysFailsAbove && test.totalRuns >= minRuns) {
|
|
81
83
|
return { verdict: 'fix-now', why: `it fails in ${percent}. That is not flakiness, it is a test that does not work - hiding it would hide whatever it is telling you.` };
|
|
82
84
|
}
|
|
85
|
+
// Observed, not inferred. A test that failed and then passed on a retry within the same run did
|
|
86
|
+
// both on the same code, same commit, same machine: that is not evidence of flakiness, it is
|
|
87
|
+
// flakiness, and no diagnosis is needed to see it.
|
|
88
|
+
//
|
|
89
|
+
// This mattered more than it looks. Everything below rests on `flakyShare`, which counts how many
|
|
90
|
+
// failures a *previous* `tewip explain` already called flaky - so on a project's raw history,
|
|
91
|
+
// where nothing has been diagnosed yet, no test could ever qualify and the command recommended
|
|
92
|
+
// quarantining nothing at all. Measured on 1,305 real failures from WordPress/gutenberg: **124 of
|
|
93
|
+
// the 180 failing tests had recovered on retry, and all 124 were filed under "watch"**. One of
|
|
94
|
+
// them had done it 93 separate times.
|
|
95
|
+
//
|
|
96
|
+
// Phase 56 is where this comes from: of the signals available when a test fails, non-determinism
|
|
97
|
+
// is the one that is genuinely decisive, and it costs nothing to read.
|
|
98
|
+
if (test.recoveredOnRetry > 0 && test.failedRuns >= minRuns) {
|
|
99
|
+
const times = test.recoveredOnRetry === 1 ? 'once' : `${test.recoveredOnRetry} times`;
|
|
100
|
+
const spread = test.branches.length > 1 ? ` It failed on ${test.branches.length} different branches, so it is the test rather than one person's change.` : '';
|
|
101
|
+
return {
|
|
102
|
+
verdict: 'quarantine',
|
|
103
|
+
why: `it failed and then passed on a retry in the same run ${times}: the same code did both, which is what a flaky test is. It failed in ${percent}.${spread}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
83
106
|
if (test.failedRuns >= minRuns && flakyShare(test) >= 0.5) {
|
|
84
107
|
const spread = test.branches.length > 1 ? ` It failed on ${test.branches.length} different branches, so it is the test rather than one person's change.` : '';
|
|
85
108
|
return { verdict: 'quarantine', why: `it failed in ${percent}, and Tewip read most of those as the test being unreliable.${spread}` };
|
|
@@ -50,6 +50,28 @@ export const LLM_CONFIDENCE_CEILING = 0.45;
|
|
|
50
50
|
export function contradictsHistory(category, context) {
|
|
51
51
|
if (category !== 'REAL_BUG' && category !== 'LOCATOR_DRIFT')
|
|
52
52
|
return null;
|
|
53
|
+
// A timeout is not evidence of a bug, and that is measured rather than argued.
|
|
54
|
+
//
|
|
55
|
+
// Phase 56 took 471 real labelled CI failures from WordPress/gutenberg and asked, for each signal
|
|
56
|
+
// a classifier can actually see when the test fails, how far it moves the label from its base
|
|
57
|
+
// rate. Three signals move it a long way:
|
|
58
|
+
//
|
|
59
|
+
// error says Timeout/exceeded -> TIMING 91% (37% when absent)
|
|
60
|
+
// error is an assertion -> TIMING 93% (ENV 86% when absent)
|
|
61
|
+
// error mentions a locator -> TIMING 94%
|
|
62
|
+
//
|
|
63
|
+
// and REAL_BUG sits at 3-6% in **every single bucket** - its base rate, undisturbed. Nothing in
|
|
64
|
+
// the evidence available at failure time distinguishes a real bug from flakiness. That is the
|
|
65
|
+
// whole explanation for a model layer scoring 17.8%: it reaches for the one category the evidence
|
|
66
|
+
// cannot support, and is wrong about 94% of the time when it does.
|
|
67
|
+
//
|
|
68
|
+
// So a verdict of REAL_BUG resting on nothing but a timeout is refused. Six per cent of those
|
|
69
|
+
// really are bugs, and they are not lost: the case goes to a person with the model's reasoning
|
|
70
|
+
// attached, which is what this project does with every question it cannot answer.
|
|
71
|
+
const message = context.error?.message ?? '';
|
|
72
|
+
if (category === 'REAL_BUG' && /timeout|exceeded/i.test(message)) {
|
|
73
|
+
return 'refused: the only evidence is a timeout, and across 471 labelled failures a timeout was TIMING 91% of the time and REAL_BUG 6%';
|
|
74
|
+
}
|
|
53
75
|
if (context.thisRun?.anotherAttemptPassed) {
|
|
54
76
|
return `refused: another attempt of the same code passed in this run, so the failure is not deterministic and cannot be ${category}`;
|
|
55
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"mcpName": "io.github.hmamut39/tewip",
|
|
5
5
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
6
6
|
"keywords": [
|