tewip-ai 0.70.1 → 0.70.2
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.
|
@@ -28,6 +28,14 @@ export const MAX_CHANGED_LINES = 20;
|
|
|
28
28
|
/** How many times the model may try before Tewip gives up and explains instead. */
|
|
29
29
|
export const MAX_ATTEMPTS = 3;
|
|
30
30
|
const CR = String.fromCharCode(13);
|
|
31
|
+
/**
|
|
32
|
+
* Does this text come back as the same text after a trip through UTF-8?
|
|
33
|
+
*
|
|
34
|
+
* The caller hands over a string already decoded from disk, so a byte that was not valid UTF-8 is
|
|
35
|
+
* already a U+FFFD by the time it gets here - and that replacement character is the tell. Nothing
|
|
36
|
+
* legitimate in source code contains it.
|
|
37
|
+
*/
|
|
38
|
+
const roundTrips = (text) => !text.includes(String.fromCharCode(0xfffd));
|
|
31
39
|
const NL = String.fromCharCode(10);
|
|
32
40
|
/**
|
|
33
41
|
* Which other tests a change broke, by name. A tail of runner output is mostly noise; the names of
|
|
@@ -132,6 +140,14 @@ async function tryFile(target, input, deps, { keep, result, alreadyFailing }) {
|
|
|
132
140
|
// threw away four correct fixes in a real measurement, so both sides are read in LF here and the
|
|
133
141
|
// file is written back in whatever it used.
|
|
134
142
|
const raw = deps.readFile(target.file);
|
|
143
|
+
// A file whose text does not survive a round trip through UTF-8 cannot be written back as it was,
|
|
144
|
+
// and every failure path here writes `raw` back. A legacy latin-1 source file - `// caf\xe9`, which
|
|
145
|
+
// older codebases are full of - would be returned re-encoded, three bytes where one had been, the
|
|
146
|
+
// character destroyed and nothing reported. Refusing is the only honest option: a fix that cannot
|
|
147
|
+
// be undone is not a fix Tewip is willing to try.
|
|
148
|
+
if (!roundTrips(raw)) {
|
|
149
|
+
return stop('cannot-restore', `${rel} is not valid UTF-8, so Tewip cannot change it and put it back exactly as it was. Nothing was written. Convert the file to UTF-8 if you want it fixed here.`);
|
|
150
|
+
}
|
|
135
151
|
const crlf = raw.includes(CR + NL);
|
|
136
152
|
const asFile = (text) => (crlf ? text.split(NL).join(CR + NL) : text);
|
|
137
153
|
const original = crlf ? raw.split(CR + NL).join(NL) : raw;
|
|
@@ -7,6 +7,14 @@ import { mayChange } from '../autofix/code-target.js';
|
|
|
7
7
|
import { changedLines } from '../autofix/code-fix.js';
|
|
8
8
|
export class Workspace {
|
|
9
9
|
root;
|
|
10
|
+
/**
|
|
11
|
+
* What each touched file held, as **bytes**.
|
|
12
|
+
*
|
|
13
|
+
* It was a utf8 string, and "put back byte for byte" was then only true of files that are valid
|
|
14
|
+
* UTF-8. A legacy latin-1 source file - `// caf\xe9`, which older codebases are full of - was
|
|
15
|
+
* decoded to U+FFFD and written back as three different bytes: 16 bytes in, 18 bytes out, the
|
|
16
|
+
* character destroyed, and nothing reported. Measured, not reasoned about.
|
|
17
|
+
*/
|
|
10
18
|
touched = new Map();
|
|
11
19
|
constructor(root) {
|
|
12
20
|
this.root = resolve(root);
|
|
@@ -15,7 +23,7 @@ export class Workspace {
|
|
|
15
23
|
remember(path) {
|
|
16
24
|
if (this.touched.has(path))
|
|
17
25
|
return;
|
|
18
|
-
this.touched.set(path, existsSync(path) ? readFileSync(path
|
|
26
|
+
this.touched.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
19
27
|
}
|
|
20
28
|
write(path, content) {
|
|
21
29
|
this.remember(path);
|
|
@@ -53,7 +53,7 @@ function stillParses(full) {
|
|
|
53
53
|
* same control the write gate uses: a check that cannot fail proves nothing, and neither does a test
|
|
54
54
|
* that cannot see the code.
|
|
55
55
|
*/
|
|
56
|
-
function testsLoadIt(root, full, original, test, framework, runMs) {
|
|
56
|
+
function testsLoadIt(root, full, original, originalBytes, test, framework, runMs) {
|
|
57
57
|
const control = /\.py$/i.test(full)
|
|
58
58
|
? `raise RuntimeError("tewip control")${String.fromCharCode(10)}${original}`
|
|
59
59
|
: /\.(js|cjs|mjs|ts|tsx|mts|cts|jsx)$/i.test(full)
|
|
@@ -64,7 +64,7 @@ function testsLoadIt(root, full, original, test, framework, runMs) {
|
|
|
64
64
|
return !runTest(framework, resolve(root, test), runMs).passed;
|
|
65
65
|
}
|
|
66
66
|
finally {
|
|
67
|
-
restore(full,
|
|
67
|
+
restore(full, originalBytes);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
export function proveFile(root, file, covering, framework, opts = {}) {
|
|
@@ -79,8 +79,21 @@ export function proveFile(root, file, covering, framework, opts = {}) {
|
|
|
79
79
|
proof.blocked = 'no test imports this file, so no test could notice any change to it. That is the finding: this code is not covered by anything Tewip can name.';
|
|
80
80
|
return finish(proof, started);
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Bytes for putting it back, text for making the mutants - and a refusal when the two cannot be
|
|
83
|
+
// reconciled. Holding only the utf8 string meant a file that is not valid UTF-8 was written back
|
|
84
|
+
// re-encoded: a latin-1 `// caf\xe9` came back 18 bytes instead of 16, with the character
|
|
85
|
+
// destroyed. Worse, `restore` verified its own work by comparing the **decoded strings**, which
|
|
86
|
+
// were equally lossy on both sides, so it reported success every time. A check that cannot fail
|
|
87
|
+
// proves nothing - this project's own rule, inside the function written to enforce it.
|
|
88
|
+
const originalBytes = readFileSync(full);
|
|
89
|
+
const original = originalBytes.toString('utf8');
|
|
90
|
+
if (!Buffer.from(original, 'utf8').equals(originalBytes)) {
|
|
91
|
+
proof.blocked =
|
|
92
|
+
'this file is not valid UTF-8, so Tewip cannot change it and put it back exactly as it was. Nothing was written. ' +
|
|
93
|
+
'Convert the file to UTF-8 if you want it proved; a mutation Tewip cannot undo is not worth the answer.';
|
|
94
|
+
return finish(proof, started);
|
|
95
|
+
}
|
|
96
|
+
const stopGuarding = guardAgainstBeingKilled(full, originalBytes);
|
|
84
97
|
// The whole set first, then the sample: `found` is what the file allows and `mutants` is what
|
|
85
98
|
// there was time to ask about, and the report has to be able to tell the reader the difference.
|
|
86
99
|
const candidates = candidateMutants(original);
|
|
@@ -114,7 +127,7 @@ export function proveFile(root, file, covering, framework, opts = {}) {
|
|
|
114
127
|
// The control: break the file outright and check that a covering test notices. Without this, a file
|
|
115
128
|
// the tests never load would produce a full report of things they "failed to catch".
|
|
116
129
|
say('checking the tests really load this file');
|
|
117
|
-
if (!testsLoadIt(root, full, original, usable[0], framework, runMs)) {
|
|
130
|
+
if (!testsLoadIt(root, full, original, originalBytes, usable[0], framework, runMs)) {
|
|
118
131
|
proof.blocked = `${usable[0]} passes even when this file is made unusable, so the tests are not loading the file that was edited - they are reading an installed copy or a build output. Nothing here can be proved until they run against this source (for a Python src layout, install it in editable mode: pip install -e .).`;
|
|
119
132
|
return finish(proof, started, stopGuarding);
|
|
120
133
|
}
|
|
@@ -157,7 +170,7 @@ export function proveFile(root, file, covering, framework, opts = {}) {
|
|
|
157
170
|
noticedBy = error instanceof Error ? error.message.slice(0, 120) : '';
|
|
158
171
|
}
|
|
159
172
|
finally {
|
|
160
|
-
restore(full,
|
|
173
|
+
restore(full, originalBytes);
|
|
161
174
|
}
|
|
162
175
|
proof.tried.push({ mutant, fate, noticedBy, seconds: Math.round((Date.now() - at) / 100) / 10 });
|
|
163
176
|
}
|
|
@@ -168,8 +181,8 @@ export function proveFile(root, file, covering, framework, opts = {}) {
|
|
|
168
181
|
* leave somebody's source broken, which would make it unusable however good its answers were.
|
|
169
182
|
*/
|
|
170
183
|
function restore(full, original) {
|
|
171
|
-
writeFileSync(full, original
|
|
172
|
-
if (readFileSync(full
|
|
184
|
+
writeFileSync(full, original);
|
|
185
|
+
if (!readFileSync(full).equals(original)) {
|
|
173
186
|
throw new Error(`${full} could not be put back as it was. Check it before doing anything else: git checkout -- "${full}"`);
|
|
174
187
|
}
|
|
175
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.70.
|
|
3
|
+
"version": "0.70.2",
|
|
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": [
|