tewip-ai 0.71.0 → 0.73.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 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. On 17 such cases across seven source/test pairs in
1177
- two projects, **vouch named 12 (70.6%)**, and the five it did not name still hold a real assertion, so
1178
- it is right about those too. Nothing that still caught a mutation was wrongly accused.
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
 
@@ -14,6 +14,14 @@ const CHECKS = [
14
14
  /\bshould\s*\.\s*not\b/,
15
15
  /\bassert\b\s*[.(]/,
16
16
  /\bassert(Equals?|True|False|Null|NotNull|That|Throws|Match|Contains|Rejects|Fails)\b/i,
17
+ // Any call named assertSomething. The list above needs the suffix to end on a word boundary, so
18
+ // keycloak's own `assertNullAdditionalJdbcProperty(config, null, prop)` matched nothing: `Null` is
19
+ // followed by `A`. Four of its ConfigurationTest tests delegate to a helper built out of those
20
+ // calls, and the helper was being credited only because the *string* `--db-tls-mode=verify-server`
21
+ // happened to contain the word `verify` - so narrowing `verify` to a call turned four correct
22
+ // verdicts into accusations. The corpus caught it. A call named `assertX(` is the universal
23
+ // convention for a custom assertion, and reading it as one is both broader and the actual reason.
24
+ /\bassert[A-Z]\w*\s*\(/,
17
25
  /\.\s*to\s*\.\s*(equal|be|deep|have|include|throw|match|exist)\b/,
18
26
  /\btoMatch(Snapshot|InlineSnapshot|ImageSnapshot)\s*\(/,
19
27
  /\bexpect(Type|TypeOf)\s*[<(]/,
@@ -21,8 +29,19 @@ const CHECKS = [
21
29
  // Tewip condemn all 59 tests in chalk - a third repeat of the Phase 8 lesson, so the pattern here
22
30
  // is deliberately generous about what an assertion on `t` can be called.
23
31
  /\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
- /\b(verify|verifyNoMoreInteractions|toHaveBeenCalled)\b/,
32
+ // Mocking libraries: verifying a call is a real check - but only when it is a call. This was a
33
+ // bare word match, so a fixture that *defines* a stub named `verify` matched it:
34
+ // verify: async () => ({ passed: outcomes[run++], detail: 'run' }),
35
+ // asserts nothing, and five of this project's own autofix fixtures have exactly that line. A test
36
+ // whose last statement was removed and whose only remaining call was `deps(...)` was then told
37
+ // "it checks through deps(), which asserts" - a hollow test reported as sound. Mockito's
38
+ // `verify(mock)` and `Mockito.verify(mock)` both still match; a property key does not, because
39
+ // `: async` is not `(`. Found by measuring recall against execution, not by reading this list.
40
+ /\bverify(NoMoreInteractions)?\s*\(/,
41
+ // Kept deliberately, though `expect(fn).toHaveBeenCalled()` is already read by the `expect(`
42
+ // pattern above: this one is what reads the bare `spy.toHaveBeenCalled()` form. Narrowing it from
43
+ // a bare word to a property access is enough to stop a fixture that *defines* such a key.
44
+ /\.\s*toHaveBeenCalled/,
26
45
  /\bsinon\s*\.\s*assert\b/,
27
46
  // Python: pytest, unittest, hypothesis.
28
47
  /(^|[^\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) : balanced(text, text.indexOf('{', from - 1));
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));
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // The cap now counts the files the caller actually wants. A budget spent on files the caller will
9
9
  // discard is not a budget.
10
- import { readdirSync, statSync } from 'node:fs';
10
+ import { lstatSync, readdirSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  const SKIP_DIR = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'coverage', 'target', 'vendor', '__pycache__', '.venv', 'venv', '.tewip', '.next', '.cache']);
13
13
  const BINARY = /\.(?:png|jpe?g|gif|webp|ico|svg|pdf|zip|gz|tar|exe|dll|so|dylib|woff2?|ttf|eot|mp4|mp3|wav|class|jar|pyc|lock)$/i;
@@ -39,11 +39,29 @@ export function scanSourceFiles(root, limit = 5000, keep) {
39
39
  const path = join(dir, entry);
40
40
  let info;
41
41
  try {
42
- info = statSync(path);
42
+ // `lstat`, not `stat`: a symlink must be seen as a symlink. See below.
43
+ info = lstatSync(path);
43
44
  }
44
45
  catch {
45
46
  continue;
46
47
  }
48
+ // A symlink is neither walked nor read.
49
+ //
50
+ // `stat` follows them, so a link to a directory looked like a directory and was walked. In
51
+ // immich, `fastlane/metadata` points at `../mobile/android/fastlane/metadata` inside the same
52
+ // repository: following it read 77 files a second time, under a path they do not really have.
53
+ // A finding in one of them would have been reported twice, at two different paths, and the
54
+ // file count no longer meant what the report said it meant.
55
+ //
56
+ // Worse than the double count: a link pointing out of the project would have Tewip reading
57
+ // files outside the directory it was given, which it must never do whatever is on the other
58
+ // end. Anything genuinely inside the project is still reached by its real path.
59
+ //
60
+ // Windows was already doing the right thing by accident - git writes a symlink there as a
61
+ // small text file - which is exactly why this hid: the numbers were recorded on Windows and
62
+ // only the Linux corpus run disagreed.
63
+ if (info.isSymbolicLink())
64
+ continue;
47
65
  if (info.isDirectory()) {
48
66
  if (!SKIP_DIR.has(entry))
49
67
  walk(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.71.0",
3
+ "version": "0.73.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": [