tewip-ai 0.17.3 → 0.18.1
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.
|
@@ -193,6 +193,26 @@ export function junitFiles(path) {
|
|
|
193
193
|
* Playwright; every test, passing or failing, is counted in the run summary so its history
|
|
194
194
|
* shows the passes too.
|
|
195
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* A runner's own timestamp, when it is one a machine can read.
|
|
198
|
+
*
|
|
199
|
+
* Maven Surefire writes `timestamp="2025-03-05T11:53:11 IST"` - a timezone abbreviation with no
|
|
200
|
+
* offset, which is not a date any parser accepts. `new Date(...).toISOString()` then threw, and
|
|
201
|
+
* Tewip died on one of the most widely used test runners there is. This was found by reading real
|
|
202
|
+
* Surefire reports from public repositories; every fixture collected for this project had a clean
|
|
203
|
+
* timestamp. A report Tewip cannot date is still a report worth reading, so an unreadable one falls
|
|
204
|
+
* back to the time of import rather than stopping everything.
|
|
205
|
+
*/
|
|
206
|
+
function readTimestamp(raw) {
|
|
207
|
+
if (!raw)
|
|
208
|
+
return null;
|
|
209
|
+
const asIs = new Date(raw);
|
|
210
|
+
if (!Number.isNaN(asIs.getTime()))
|
|
211
|
+
return asIs.toISOString();
|
|
212
|
+
// Drop a trailing zone abbreviation (IST, CEST, PDT...) and read the rest as local time.
|
|
213
|
+
const withoutZone = new Date(raw.replace(/\s+[A-Z]{2,5}$/, ''));
|
|
214
|
+
return Number.isNaN(withoutZone.getTime()) ? null : withoutZone.toISOString();
|
|
215
|
+
}
|
|
196
216
|
export function importJunit(path, meta) {
|
|
197
217
|
const records = [];
|
|
198
218
|
const attempts = {};
|
|
@@ -288,7 +308,7 @@ export function importJunit(path, meta) {
|
|
|
288
308
|
runId: meta.runId,
|
|
289
309
|
gitSha: meta.gitSha,
|
|
290
310
|
...(meta.branch ? { branch: meta.branch } : {}),
|
|
291
|
-
startedAt:
|
|
311
|
+
startedAt: readTimestamp(startedAt) ?? now,
|
|
292
312
|
finishedAt: now,
|
|
293
313
|
status: anyFailed ? 'failed' : 'passed',
|
|
294
314
|
attempts,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { truncate } from './signature.js';
|
|
2
|
+
/** The module name each runner prints when it cannot load something. */
|
|
3
|
+
const PATTERNS = [
|
|
4
|
+
{ language: 'Python', pattern: /ModuleNotFoundError:\s*No module named ['"]([^'"]+)['"]/ },
|
|
5
|
+
{ language: 'Node', pattern: /Cannot find module ['"]([^'"]+)['"]/ },
|
|
6
|
+
{ language: 'Node', pattern: /ERR_MODULE_NOT_FOUND[^'"]*['"]([^'"]+)['"]/ },
|
|
7
|
+
{ language: 'Java', pattern: /(?:NoClassDefFoundError|ClassNotFoundException):\s*([\w.$]+)/ },
|
|
8
|
+
{ language: 'Ruby', pattern: /cannot load such file --\s*(\S+)/ },
|
|
9
|
+
{ language: 'Go', pattern: /cannot find package ['"]([^'"]+)['"]/ },
|
|
10
|
+
{ language: '.NET', pattern: /Could not load file or assembly ['"]([^'",]+)/ },
|
|
11
|
+
{ language: 'PHP', pattern: /Failed opening required ['"]([^'"]+)['"]|Class ['"]([^'"]+)['"] not found/ },
|
|
12
|
+
];
|
|
13
|
+
// `ImportError: cannot import name 'x'` is deliberately not here. It usually means the code asked
|
|
14
|
+
// for something that does not exist - a rename that missed a caller - which is a real bug, and only
|
|
15
|
+
// sometimes a version mismatch. "No module named" says plainly that nothing could be found.
|
|
16
|
+
/** A module that is part of this project rather than something it depends on. */
|
|
17
|
+
const LOOKS_INTERNAL = /^\.|^(?:src|tests?|app|lib|internal|pkg|main)\b|_test$|\.spec$|\.test$/i;
|
|
18
|
+
/** The file could be found but not understood, which is a mistake in the code, not the machine. */
|
|
19
|
+
const A_CODE_ERROR = /\b(?:SyntaxError|IndentationError|TabError|TypeError|NameError|AttributeError|error TS\d+|Unexpected token)\b/;
|
|
20
|
+
/**
|
|
21
|
+
* pytest reports a missing plugin as a missing fixture, which on its own is ambiguous: a typo in a
|
|
22
|
+
* fixture name reads exactly the same. What separates them is how many tests it hits. A typo breaks
|
|
23
|
+
* the one test that made it; an uninstalled plugin breaks every test that asks for its fixture.
|
|
24
|
+
*
|
|
25
|
+
* Found on humanize's real suite, where 15 tests failed with "fixture 'benchmark' not found". The
|
|
26
|
+
* model answered those 15 identical failures with 13 ENV and 2 TEST_DATA - the same input getting
|
|
27
|
+
* different answers, which is worse than either answer, and exactly what a rule is for.
|
|
28
|
+
*/
|
|
29
|
+
const MISSING_FIXTURE = /fixture ['"]([^'"]+)['"] not found/;
|
|
30
|
+
export const FIXTURE_SHARED_BY = 3;
|
|
31
|
+
export const fixtureMissing = (errorMessage) => MISSING_FIXTURE.exec(errorMessage)?.[1] ?? null;
|
|
32
|
+
export function missingDependency(errorMessage) {
|
|
33
|
+
if (A_CODE_ERROR.test(errorMessage))
|
|
34
|
+
return null;
|
|
35
|
+
for (const { language, pattern } of PATTERNS) {
|
|
36
|
+
const found = pattern.exec(errorMessage);
|
|
37
|
+
const module = found?.slice(1).find(Boolean);
|
|
38
|
+
if (!module)
|
|
39
|
+
continue;
|
|
40
|
+
if (LOOKS_INTERNAL.test(module))
|
|
41
|
+
return null;
|
|
42
|
+
return { language, module };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Nothing about the test ran, so nothing about the test or the code has been shown. The suite needs
|
|
48
|
+
* a dependency the machine does not have.
|
|
49
|
+
*/
|
|
50
|
+
export const envMissingDependency = {
|
|
51
|
+
id: 'env-missing-dependency',
|
|
52
|
+
evaluate({ record, run }) {
|
|
53
|
+
const fixture = fixtureMissing(record.errorMessage);
|
|
54
|
+
if (fixture) {
|
|
55
|
+
const sharing = run.failures.filter((f) => fixtureMissing(f.errorMessage) === fixture).length;
|
|
56
|
+
if (sharing < FIXTURE_SHARED_BY)
|
|
57
|
+
return null; // one test only: a typo is as likely, so say nothing
|
|
58
|
+
return {
|
|
59
|
+
category: 'ENV',
|
|
60
|
+
confidence: 0.85,
|
|
61
|
+
evidence: [
|
|
62
|
+
{
|
|
63
|
+
source: 'env-missing-dependency',
|
|
64
|
+
message: `${sharing} tests in this run all need the fixture "${truncate(fixture, 40)}" and none can find it: the plugin that provides it is not installed, so no test code ran`,
|
|
65
|
+
data: { fixture, sharing },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const found = missingDependency(record.errorMessage);
|
|
71
|
+
if (!found)
|
|
72
|
+
return null;
|
|
73
|
+
// When the whole run failed this way, the install step is the obvious suspect and there is
|
|
74
|
+
// nothing test-specific left to doubt.
|
|
75
|
+
const wholeRun = run.failures.length >= 3 && run.failures.every((f) => missingDependency(f.errorMessage) !== null);
|
|
76
|
+
return {
|
|
77
|
+
category: 'ENV',
|
|
78
|
+
confidence: wholeRun ? 0.92 : 0.85,
|
|
79
|
+
evidence: [
|
|
80
|
+
{
|
|
81
|
+
source: 'env-missing-dependency',
|
|
82
|
+
message: wholeRun
|
|
83
|
+
? `every failure in this run is a module that could not be loaded (${found.language}: "${truncate(found.module, 60)}"): the dependencies were not installed, so no test code ran`
|
|
84
|
+
: `the test could not be loaded because ${found.language} could not find "${truncate(found.module, 60)}": a dependency is missing, so nothing about the test itself has been shown`,
|
|
85
|
+
data: { ...found, wholeRun },
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
package/dist/engine/rules.js
CHANGED
|
@@ -2,6 +2,7 @@ import { deviceEnvSignal } from './device-errors.js';
|
|
|
2
2
|
import { INFRA_MEANING, infraSignal } from './infra-errors.js';
|
|
3
3
|
import { isElementNotFound, networkSignal, valueMismatch } from './error-parse.js';
|
|
4
4
|
import { locatorDrift, locatorDriftStrict } from './drift-rules.js';
|
|
5
|
+
import { envMissingDependency } from './missing-dependency.js';
|
|
5
6
|
import { timingFlaky, timingIntercepted, timingIntermittent, timingKnownFlaky, timingRetryPass, timingStaleElement } from './timing-rules.js';
|
|
6
7
|
import { errorScenarioOf } from './llm/context.js';
|
|
7
8
|
import { errorShape, truncate } from './signature.js';
|
|
@@ -191,4 +192,4 @@ const realBugAssertion = {
|
|
|
191
192
|
* Priority order: the first matching rule decides. Signals from outside the test (ENV)
|
|
192
193
|
* come first, so a 5xx that also fails an assertion is ENV, not REAL_BUG.
|
|
193
194
|
*/
|
|
194
|
-
export const RULES = [envMassFailure, envNetwork, envInfra, envDevice, testDataCollision, envServerError, locatorDriftStrict, timingIntercepted, timingStaleElement, timingRetryPass, locatorDrift, timingFlaky, timingIntermittent, timingKnownFlaky, realBugAssertion];
|
|
195
|
+
export const RULES = [envMissingDependency, envMassFailure, envNetwork, envInfra, envDevice, testDataCollision, envServerError, locatorDriftStrict, timingIntercepted, timingStaleElement, timingRetryPass, locatorDrift, timingFlaky, timingIntermittent, timingKnownFlaky, realBugAssertion];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test-failures",
|