tewip-ai 0.71.0 → 0.72.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 +19 -3
- package/dist/engine/vouch/assertions.js +13 -2
- package/dist/engine/vouch/text.js +25 -0
- package/dist/engine/vouch/vouch.js +23 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1173,9 +1173,25 @@ Two things it refuses to do, which is why the list is worth reading:
|
|
|
1173
1173
|
**Recall, measured by execution** (`eval/vouch-recall.ts`): mutate a source file, record which tests
|
|
1174
1174
|
catch which mutation, remove the last statement of the ones that provably caught something, and run it
|
|
1175
1175
|
all again. A test that caught a mutation before and catches nothing now has provably lost its check -
|
|
1176
|
-
a label that owes nothing to vouch's own patterns.
|
|
1177
|
-
|
|
1178
|
-
|
|
1176
|
+
a label that owes nothing to vouch's own patterns.
|
|
1177
|
+
|
|
1178
|
+
Run across all 48 source/test pairs in this project (Phase 138): 28 produce labelled cases, 22 of
|
|
1179
|
+
those settle, and across them **vouch names 48 of 64 (75.0%)**, with **0 of 157 sound tests wrongly
|
|
1180
|
+
accused**. Precision is the strong half, and it is the half that matters: it did not once accuse a
|
|
1181
|
+
test that still catches something.
|
|
1182
|
+
|
|
1183
|
+
A rate is printed only when two different mutant budgets label the same tests. They often do not,
|
|
1184
|
+
and that is the point: the first run of this reported "2 of 4 (50.0%)" at 8 mutants and **0 of 0** at
|
|
1185
|
+
14 mutants of the same file, because "caught nothing after the deletion" is evidence of hollowness
|
|
1186
|
+
only to the degree that the mutants are many. Six pairs still do not settle and are left out; they
|
|
1187
|
+
were the favourable ones, so 75.0% is a floor.
|
|
1188
|
+
|
|
1189
|
+
Measuring it this way found **two real defects in vouch**, both reporting a hollow test as sound, and
|
|
1190
|
+
neither visible by reading the code: a one-line arrow helper was credited with assertions from the
|
|
1191
|
+
next block in the file, and a test fixture that merely *defines* a stub named `verify` was read as
|
|
1192
|
+
asserting. Fixing them took recall from 58.7% to 70.3% to 75.0%. The figure is measured on this
|
|
1193
|
+
project's own suite rather than on strangers' code, which is weaker evidence than the triage numbers
|
|
1194
|
+
above, and is the next thing to fix. See [eval/README.md](eval/README.md).
|
|
1179
1195
|
|
|
1180
1196
|
## A goal instead of a verb (Phases 19 and 21)
|
|
1181
1197
|
|
|
@@ -21,8 +21,19 @@ const CHECKS = [
|
|
|
21
21
|
// Tewip condemn all 59 tests in chalk - a third repeat of the Phase 8 lesson, so the pattern here
|
|
22
22
|
// is deliberately generous about what an assertion on `t` can be called.
|
|
23
23
|
/\bt\s*\.\s*(is|not|deepEqual|notDeepEqual|like|true|false|truthy|falsy|throws(Async)?|notThrows(Async)?|regex|notRegex|snapshot|pass|equal|same|strictSame|ok|notOk|match|type|has|hasStrict|rejects|resolves)\b/,
|
|
24
|
-
// Mocking libraries: verifying a call is a real check.
|
|
25
|
-
|
|
24
|
+
// Mocking libraries: verifying a call is a real check - but only when it is a call. This was a
|
|
25
|
+
// bare word match, so a fixture that *defines* a stub named `verify` matched it:
|
|
26
|
+
// verify: async () => ({ passed: outcomes[run++], detail: 'run' }),
|
|
27
|
+
// asserts nothing, and five of this project's own autofix fixtures have exactly that line. A test
|
|
28
|
+
// whose last statement was removed and whose only remaining call was `deps(...)` was then told
|
|
29
|
+
// "it checks through deps(), which asserts" - a hollow test reported as sound. Mockito's
|
|
30
|
+
// `verify(mock)` and `Mockito.verify(mock)` both still match; a property key does not, because
|
|
31
|
+
// `: async` is not `(`. Found by measuring recall against execution, not by reading this list.
|
|
32
|
+
/\bverify(NoMoreInteractions)?\s*\(/,
|
|
33
|
+
// Kept deliberately, though `expect(fn).toHaveBeenCalled()` is already read by the `expect(`
|
|
34
|
+
// pattern above: this one is what reads the bare `spy.toHaveBeenCalled()` form. Narrowing it from
|
|
35
|
+
// a bare word to a property access is enough to stop a fixture that *defines* such a key.
|
|
36
|
+
/\.\s*toHaveBeenCalled/,
|
|
26
37
|
/\bsinon\s*\.\s*assert\b/,
|
|
27
38
|
// Python: pytest, unittest, hypothesis.
|
|
28
39
|
/(^|[^\w.])assert\s+[^=]/m,
|
|
@@ -105,6 +105,31 @@ export function firstRealLine(body) {
|
|
|
105
105
|
const code = lines.find((l) => !/^(#|\/\/|\/\*|\*|"""|'''|--)/.test(l) && /[A-Za-z0-9]/.test(l));
|
|
106
106
|
return (code ?? lines[0] ?? '').slice(0, 120);
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* A concise arrow body: `(a) => a.join(x)` has no braces, so it ends at the end of the statement.
|
|
110
|
+
*
|
|
111
|
+
* Without this, a scan for the next `{` walks past the arrow entirely and lands in whatever block
|
|
112
|
+
* comes next in the file. In a test file that is the next test, full of assertions, so every
|
|
113
|
+
* one-line helper looked as though it asserted and any test calling one was called sound.
|
|
114
|
+
*/
|
|
115
|
+
export const conciseBody = (text, start) => {
|
|
116
|
+
let depth = 0;
|
|
117
|
+
for (let i = start; i < text.length; i += 1) {
|
|
118
|
+
const ch = text[i];
|
|
119
|
+
if ('([{'.includes(ch))
|
|
120
|
+
depth += 1;
|
|
121
|
+
else if (')]}'.includes(ch)) {
|
|
122
|
+
depth -= 1;
|
|
123
|
+
// A closing bracket we never opened belongs to whatever encloses this arrow: the body stops.
|
|
124
|
+
if (depth < 0)
|
|
125
|
+
return text.slice(start, i);
|
|
126
|
+
}
|
|
127
|
+
else if (depth === 0 && (ch === ';' || ch === String.fromCharCode(10) || ch === ',')) {
|
|
128
|
+
return text.slice(start, i);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return text.slice(start);
|
|
132
|
+
};
|
|
108
133
|
export const balanced = (text, open) => {
|
|
109
134
|
if (open === -1)
|
|
110
135
|
return null;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// their time and their trust, and a reviewer who is wrong once is ignored afterwards. So a body whose
|
|
10
10
|
// checking happens somewhere this cannot follow is reported as undecided, by name, and never as
|
|
11
11
|
// vacuous.
|
|
12
|
-
import { balanced, firstRealLine, stripLines, withoutComments } from './text.js';
|
|
12
|
+
import { balanced, conciseBody, firstRealLine, stripLines, withoutComments } from './text.js';
|
|
13
13
|
import { swallowsFailure } from './swallowed.js';
|
|
14
14
|
import { checksSomething, tautologies, unfinishedExpectations } from './assertions.js';
|
|
15
15
|
import { languageOf, looksLikeTests, testsIn, textRanges } from './tests-in.js';
|
|
@@ -142,12 +142,33 @@ function helpersThatCheck(text, language) {
|
|
|
142
142
|
if (!name)
|
|
143
143
|
continue;
|
|
144
144
|
const from = (hit.index ?? 0) + hit[0].length;
|
|
145
|
-
const body = language === 'python' ? indentedFrom(text, from) :
|
|
145
|
+
const body = language === 'python' ? indentedFrom(text, from) : jsHelperBody(text, from, hit[0].trimEnd().endsWith('=>'));
|
|
146
146
|
if (body && checksSomething(body))
|
|
147
147
|
names.add(name);
|
|
148
148
|
}
|
|
149
149
|
return names;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The body of a helper declared in JavaScript or TypeScript.
|
|
153
|
+
*
|
|
154
|
+
* An arrow's body begins where the match ends: `{` for a block, otherwise a single expression. The
|
|
155
|
+
* distinction is the whole point. This used to look for the next `{` from here regardless, which
|
|
156
|
+
* for `const file = (...lines) => lines.join(NL)` is not this helper's brace at all but the start
|
|
157
|
+
* of whatever block follows in the file. In a test file that is the next `describe` or `it`, so
|
|
158
|
+
* `checksSomething` saw its assertions and credited them to the helper - and then any test whose
|
|
159
|
+
* only remaining call was `file(...)` was told it checks something. A hollow test reported as
|
|
160
|
+
* sound, which is the one direction Tewip must never fail in.
|
|
161
|
+
*
|
|
162
|
+
* Found by measuring vouch's recall against execution: engine/test/line-diff.test.ts has exactly
|
|
163
|
+
* this helper, and a test stripped of its only assertion came back as "it checks through file(),
|
|
164
|
+
* which asserts".
|
|
165
|
+
*/
|
|
166
|
+
function jsHelperBody(text, from, isArrow) {
|
|
167
|
+
if (!isArrow)
|
|
168
|
+
return balanced(text, text.indexOf('{', from - 1));
|
|
169
|
+
const start = from + (/^\s*/.exec(text.slice(from))?.[0].length ?? 0);
|
|
170
|
+
return text[start] === '{' ? balanced(text, start) : conciseBody(text, start);
|
|
171
|
+
}
|
|
151
172
|
/** Python's block after an offset: every line indented further than the line the offset is on. */
|
|
152
173
|
function indentedFrom(text, from) {
|
|
153
174
|
const lines = text.slice(from).split(String.fromCharCode(10));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.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": [
|