tewip-ai 0.69.2 → 0.69.3
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/dist/engine/review/coverage.js +64 -12
- package/package.json +1 -1
|
@@ -69,6 +69,15 @@ export function testFiles(root, limit = 4000) {
|
|
|
69
69
|
* pull request; saying "nothing tests ROADMAP.md" is noise, and noise is how a review gets skipped.
|
|
70
70
|
*/
|
|
71
71
|
export const isCode = (file) => /\.(?:[cm]?[jt]sx?|py|rb|go|java|kt|cs|php|rs|vue|svelte|scala|swift|sql)$/i.test(file) && !/(?:^|[\/])(?:package-lock\.json|yarn\.lock)$/i.test(file);
|
|
72
|
+
/**
|
|
73
|
+
* Code a test could be expected to cover - which a test file and a declaration file are not.
|
|
74
|
+
*
|
|
75
|
+
* A changed test file is its own test, and a review that lists `fix-data.test.ts` as covering
|
|
76
|
+
* `extension.test.ts` is saying nothing. A `.d.ts` has no behaviour at all: `version-compare.d.ts`
|
|
77
|
+
* was matched to `perf.test.ts` because both contain the words "version" and "compare", and the
|
|
78
|
+
* match was not wrong by the rule - the file simply should never have been asked about.
|
|
79
|
+
*/
|
|
80
|
+
export const isCoverableCode = (file) => isCode(file) && !TEST_FILE.test(file) && !/\.d\.[cm]?ts$/i.test(file);
|
|
72
81
|
/** The module names a file can be imported by: its path, its name, and its folder's front door. */
|
|
73
82
|
export function importNames(root, changed) {
|
|
74
83
|
const full = resolve(root, changed);
|
|
@@ -132,12 +141,35 @@ export function importsIt(source, names) {
|
|
|
132
141
|
}
|
|
133
142
|
const words = (text) => (text.toLowerCase().match(/[a-z][a-z0-9]{2,}/g) ?? []).filter((w) => !STOP.has(w));
|
|
134
143
|
const STOP = new Set(['src', 'lib', 'app', 'test', 'tests', 'spec', 'index', 'main', 'the', 'and', 'for', 'com', 'org', 'utils', 'util', 'js', 'ts', 'py']);
|
|
144
|
+
/**
|
|
145
|
+
* A word that most test files contain is not evidence about any of them.
|
|
146
|
+
*
|
|
147
|
+
* Found by reading what `review` said about a real change to `extensions/vscode/engine-version.js`:
|
|
148
|
+
* **eleven** test files were listed as covering it - `a2a`, `junit`, `perf`, `schema`, `write`,
|
|
149
|
+
* `web-scan` - none of which has anything to do with the extension. Two shared words were required
|
|
150
|
+
* and two were always found: *engine*, because every test lives in `engine/test/`, and *version*,
|
|
151
|
+
* because fixtures everywhere say `schemaVersion: 1`. Both are structural noise. Fourteen of the
|
|
152
|
+
* fifteen tests in that section were guesses and eleven were the same wrong guess, in the headline
|
|
153
|
+
* section of the command - and a section that is mostly noise is a section a reviewer learns to skip.
|
|
154
|
+
*
|
|
155
|
+
* So a word is only evidence if it is rare across the test corpus, which is the rule `recall`
|
|
156
|
+
* already uses on error text. A quarter is generous: 'engine' was in 100% of them.
|
|
157
|
+
*/
|
|
158
|
+
const NOISE_SHARE = 0.25;
|
|
159
|
+
function informative(byTest) {
|
|
160
|
+
const seen = new Map();
|
|
161
|
+
for (const bag of byTest.values())
|
|
162
|
+
for (const word of bag)
|
|
163
|
+
seen.set(word, (seen.get(word) ?? 0) + 1);
|
|
164
|
+
const limit = Math.max(2, Math.floor(byTest.size * NOISE_SHARE));
|
|
165
|
+
return (word) => (seen.get(word) ?? 0) <= limit;
|
|
166
|
+
}
|
|
135
167
|
/**
|
|
136
168
|
* The tests that reach a set of changed files. Imports first; then, for changed files nothing
|
|
137
169
|
* imports, the tests whose words overlap - marked as a guess.
|
|
138
170
|
*/
|
|
139
171
|
export function coverageFor(root, changedAll, tests = testFiles(root)) {
|
|
140
|
-
const changed = changedAll.filter(
|
|
172
|
+
const changed = changedAll.filter(isCoverableCode);
|
|
141
173
|
const sources = new Map();
|
|
142
174
|
const read = (file) => {
|
|
143
175
|
if (!sources.has(file)) {
|
|
@@ -181,18 +213,31 @@ export function coverageFor(root, changedAll, tests = testFiles(root)) {
|
|
|
181
213
|
}
|
|
182
214
|
// Only for the changed files nothing imports: the words, which is a weaker kind of knowing.
|
|
183
215
|
const orphans = changed.filter((f) => !coveredByImport.has(f));
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
if (wanted.size === 0)
|
|
187
|
-
continue;
|
|
216
|
+
if (orphans.length > 0) {
|
|
217
|
+
const bags = new Map();
|
|
188
218
|
for (const test of tests) {
|
|
189
219
|
const rel = relative(root, test).split(sep).join('/');
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
220
|
+
bags.set(rel, new Set(words(`${rel} ${read(test).slice(0, 4000)}`)));
|
|
221
|
+
}
|
|
222
|
+
const isRare = informative(bags);
|
|
223
|
+
for (const file of orphans) {
|
|
224
|
+
// The file's own name, not its folders. A test that matches only on the directory a change
|
|
225
|
+
// happens to live in knows nothing about the change.
|
|
226
|
+
const named = new Set(words(basename(file)).filter(isRare));
|
|
227
|
+
if (named.size === 0)
|
|
194
228
|
continue;
|
|
195
|
-
|
|
229
|
+
const wanted = new Set(words(file).filter(isRare));
|
|
230
|
+
for (const test of tests) {
|
|
231
|
+
const rel = relative(root, test).split(sep).join('/');
|
|
232
|
+
if (byTest.has(rel))
|
|
233
|
+
continue;
|
|
234
|
+
const bag = bags.get(rel) ?? new Set();
|
|
235
|
+
const shared = new Set([...wanted].filter((w) => bag.has(w)));
|
|
236
|
+
// Two rare words, and at least one of them from the changed file's own name.
|
|
237
|
+
if (shared.size < 2 || ![...named].some((w) => shared.has(w)))
|
|
238
|
+
continue;
|
|
239
|
+
add(test, file, 'name');
|
|
240
|
+
}
|
|
196
241
|
}
|
|
197
242
|
}
|
|
198
243
|
// Direct imports first, then the package ones - and inside each group the test whose words match
|
|
@@ -204,8 +249,15 @@ export function coverageFor(root, changedAll, tests = testFiles(root)) {
|
|
|
204
249
|
return [...byTest.values()].sort((a, b) => RANK[a.how] - RANK[b.how] || overlap(b) - overlap(a) || b.covers.length - a.covers.length);
|
|
205
250
|
}
|
|
206
251
|
const RANK = { imports: 0, package: 1, name: 2 };
|
|
207
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Changed files that no test reaches at all: the part of a change nobody will notice breaking.
|
|
254
|
+
*
|
|
255
|
+
* The same rule as the coverage search, and for the same reason. "`version-compare.d.ts` has no
|
|
256
|
+
* test that imports it. A mistake there would reach production unnoticed" is a sentence about a
|
|
257
|
+
* file that declares types and executes nothing - a demand the reader cannot act on, in the section
|
|
258
|
+
* meant to name the real risk of the change.
|
|
259
|
+
*/
|
|
208
260
|
export function uncovered(changed, coverage) {
|
|
209
261
|
const reached = new Set(coverage.flatMap((c) => c.covers));
|
|
210
|
-
return changed.filter((f) =>
|
|
262
|
+
return changed.filter((f) => isCoverableCode(f) && !reached.has(f));
|
|
211
263
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.69.
|
|
3
|
+
"version": "0.69.3",
|
|
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": [
|