tewip-ai 0.54.0 → 0.55.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.
- package/README.md +12 -3
- package/dist/engine/verdict/verdict.js +31 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1067,9 +1067,18 @@ that never run, and tests that compare no value at all. No model, no cost.
|
|
|
1067
1067
|
It is also an MCP tool, so a coding agent can ask about the tests it just wrote before claiming a
|
|
1068
1068
|
change is covered.
|
|
1069
1069
|
|
|
1070
|
-
**Measured on
|
|
1071
|
-
|
|
1072
|
-
|
|
1070
|
+
**Measured on 41,523 tests in six public projects** - Keycloak, OpenMRS, bitwarden/server, Saleor,
|
|
1071
|
+
GOV.UK Frontend and Firefly III - in Java, C#, Python, PHP and TypeScript. Every accusation was read
|
|
1072
|
+
against its source by hand, and the counts are kept in [`corpus/expected.json`](corpus/expected.json)
|
|
1073
|
+
against a pinned commit, re-derived by `npm run corpus` on every release.
|
|
1074
|
+
|
|
1075
|
+
**It has been badly wrong once, and that is worth saying plainly.** An earlier version of this
|
|
1076
|
+
paragraph reported 19 of 19 correct on 5,448 tests. Then Tewip was pointed at Keycloak and **called
|
|
1077
|
+
166 correct tests worthless** - every one of them the standard Java way of writing "I expect this to
|
|
1078
|
+
throw", which `Assert.fail()` makes fail properly because `AssertionError` is not an `Exception`.
|
|
1079
|
+
The rule had never read the caught type. 918 unit tests passed before that and after it, which is
|
|
1080
|
+
why the corpus exists: the thing that matters is what Tewip says about other people's code, and
|
|
1081
|
+
nothing was measuring it.
|
|
1073
1082
|
|
|
1074
1083
|
Two things it refuses to do, which is why the list is worth reading:
|
|
1075
1084
|
|
|
@@ -25,11 +25,33 @@ export function decide(evidence) {
|
|
|
25
25
|
if (evidence.changedSource.length === 0) {
|
|
26
26
|
checks.push({ question: 'Is the changed code tested?', standing: 'not-applicable', answer: 'this change touches no source file', decidedBy: 'the diff' });
|
|
27
27
|
}
|
|
28
|
+
else if (evidence.covering.length === 0 && evidence.changedTests.length > 0) {
|
|
29
|
+
// Nothing *imports* the changed file, and the same change ships tests. Coverage here is followed
|
|
30
|
+
// through imports, which is a fact rather than a name match - and which cannot see a test that
|
|
31
|
+
// reaches the code through a framework's entry point. A Saleor test posts a GraphQL query and
|
|
32
|
+
// asserts the error it gets back; the resolver chain in between is never imported by name.
|
|
33
|
+
//
|
|
34
|
+
// Found in Phase 46 by running this verdict over six real merged Saleor pull requests. Four came
|
|
35
|
+
// back **Not ready**, and all four shipped a test for exactly the change they were making. A gate
|
|
36
|
+
// that says "not ready" to a correct pull request is uninstalled within a week, and it is the
|
|
37
|
+
// same overclaim as a false accusation, pointed at the change instead of at a test.
|
|
38
|
+
//
|
|
39
|
+
// So: cannot say. Which is this project's whole point, and this check had no way to say it.
|
|
40
|
+
checks.push({
|
|
41
|
+
question: 'Is the changed code tested?',
|
|
42
|
+
standing: 'not-checked',
|
|
43
|
+
answer: `no test imports or reaches ${evidence.changedSource.slice(0, 3).join(', ')}, but this change also ` +
|
|
44
|
+
`edits ${evidence.changedTests.length} test file(s). Coverage is followed through imports, which cannot see a test ` +
|
|
45
|
+
'that reaches the code through a GraphQL resolver, an HTTP route or any other framework entry point - ' +
|
|
46
|
+
'so whether those tests cover this is a question the imports cannot answer',
|
|
47
|
+
decidedBy: 'imports followed through the source, which is a fact rather than a name match',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
28
50
|
else if (evidence.covering.length === 0) {
|
|
29
51
|
checks.push({
|
|
30
52
|
question: 'Is the changed code tested?',
|
|
31
53
|
standing: 'not-met',
|
|
32
|
-
answer: `no test imports or reaches ${evidence.changedSource.slice(0, 3).join(', ')}, so nothing here could notice a mistake in it`,
|
|
54
|
+
answer: `no test imports or reaches ${evidence.changedSource.slice(0, 3).join(', ')}, and this change adds no test either, so nothing here could notice a mistake in it`,
|
|
33
55
|
decidedBy: 'imports followed through the source, which is a fact rather than a name match',
|
|
34
56
|
});
|
|
35
57
|
}
|
|
@@ -50,7 +72,7 @@ export function decide(evidence) {
|
|
|
50
72
|
question: 'Can the tests in this change fail?',
|
|
51
73
|
standing: 'not-met',
|
|
52
74
|
answer: `${evidence.cannotFail.length} of them cannot: ${evidence.cannotFail.slice(0, 2).map((t) => `${t.name} (${t.file}:${t.line})`).join(', ')}`,
|
|
53
|
-
decidedBy: 'tewip vouch,
|
|
75
|
+
decidedBy: 'tewip vouch, measured on 41,523 real tests in the six public projects in corpus/expected.json',
|
|
54
76
|
});
|
|
55
77
|
}
|
|
56
78
|
else {
|
|
@@ -120,7 +142,13 @@ export function decide(evidence) {
|
|
|
120
142
|
headline: broken.length > 0
|
|
121
143
|
? `Not ready: ${broken[0].answer}`
|
|
122
144
|
: verdict === 'cannot-say'
|
|
123
|
-
?
|
|
145
|
+
? // Quoted, because the questions are questions: lowercasing them and joining with "and"
|
|
146
|
+
// produced "is anything failing and is the changed code tested could not be checked",
|
|
147
|
+
// which is the headline of the verdict this whole tool is named for.
|
|
148
|
+
`Cannot say: ${essential.filter((c) => c.standing === 'not-checked').length === 1 ? 'one question that matters' : 'two questions that matter'} here went unanswered - ${essential
|
|
149
|
+
.filter((c) => c.standing === 'not-checked')
|
|
150
|
+
.map((c) => `"${c.question}"`)
|
|
151
|
+
.join(' and ')} - so this is not an approval`
|
|
124
152
|
: `Ready as far as ${checks.filter((c) => c.standing === 'met').length} check(s) can show${unknown.length > 0 ? `, with ${unknown.length} not checked` : ''}`,
|
|
125
153
|
checks,
|
|
126
154
|
notChecked: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.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": [
|