tewip-ai 0.69.9 → 0.69.10
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.
|
@@ -48,7 +48,16 @@ function section(title, findings, lines) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export function renderAccess(result, scanned) {
|
|
51
|
-
|
|
51
|
+
// A missing list means an empty one. `accepted` and the browser run's own lists were read
|
|
52
|
+
// straight off the object, so a caller that left one out got
|
|
53
|
+
// `TypeError: Cannot read properties of undefined (reading 'length')` from the renderer rather
|
|
54
|
+
// than a report - and this is an exported function the MCP tool also calls. It happened twice in
|
|
55
|
+
// one hour during Phase 68, and the test that pins it was written by `tewip incident` from the
|
|
56
|
+
// real stack trace of the second time.
|
|
57
|
+
const { barriers = [], questions = [], accepted = [], filesRead = 0 } = result;
|
|
58
|
+
const browser = result.browser
|
|
59
|
+
? { ...result.browser, barriers: result.browser.barriers ?? [], questions: result.browser.questions ?? [], advice: result.browser.advice ?? [], superseded: result.browser.superseded ?? [] }
|
|
60
|
+
: result.browser;
|
|
52
61
|
const lines = ['# Tewip accessibility check', ''];
|
|
53
62
|
lines.push(`Read ${filesRead} markup ${filesRead === 1 ? 'file' : 'files'} under \`${scanned}\`, without rendering any of them.`, '');
|
|
54
63
|
if (browser) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// never ran is reported as exactly that and the file is removed, because a test wrongly labelled
|
|
12
12
|
// "reproduces the outage" is worse than no test: the next person will trust it.
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
15
15
|
import { noModelHelp } from '../llm/providers.js';
|
|
16
16
|
import { writerFromEnv } from '../quality/model.js';
|
|
17
17
|
import { usageLine } from '../llm/usage.js';
|
|
@@ -54,6 +54,16 @@ function asScenario(incident) {
|
|
|
54
54
|
expected: `it fails with ${[incident.errorType, incident.errorMessage].filter(Boolean).join(': ') || incident.title}. Write the test asserting the behaviour that SHOULD happen, so it is red now and green once the bug is fixed.`,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* How to import a source file from the folder the test will be written in.
|
|
59
|
+
*
|
|
60
|
+
* Always relative and always with a `./` or `../` in front, because a bare `access/render.ts` is a
|
|
61
|
+
* package specifier to Node, not a path.
|
|
62
|
+
*/
|
|
63
|
+
export function importPathFor(root, testDir, file) {
|
|
64
|
+
const path = relative(resolve(testDir), resolve(root, file)).split('\\').join('/');
|
|
65
|
+
return path.startsWith('.') ? path : `./${path}`;
|
|
66
|
+
}
|
|
57
67
|
export async function runIncident(cmd) {
|
|
58
68
|
const incident = readIncident(readFileSync(resolve(cmd.file), 'utf8'));
|
|
59
69
|
const framework = chooseFramework(detectFrameworks(cmd.root), cmd.framework, false);
|
|
@@ -69,7 +79,11 @@ export async function runIncident(cmd) {
|
|
|
69
79
|
const fileName = framework.fileFor(`incident-${Date.now().toString(36)}`);
|
|
70
80
|
const testFile = join(framework.testDir, fileName);
|
|
71
81
|
mkdirSync(dirname(testFile), { recursive: true });
|
|
72
|
-
|
|
82
|
+
// Each source file with the path to import it by from the test's own folder. Without this the
|
|
83
|
+
// model only had project-relative paths and every import it wrote was wrong for any framework
|
|
84
|
+
// whose test folder is not the project root.
|
|
85
|
+
const withImports = code.map((c) => ({ ...c, importPath: importPathFor(cmd.root, framework.testDir, c.file) }));
|
|
86
|
+
const draft = await draftTest(model, { framework, fileName, pages: [], feature: `A production incident: ${incident.title}`, code: withImports, testDir: relative(cmd.root, framework.testDir).split('\\').join('/') || '.' }, asScenario(incident));
|
|
73
87
|
writeFileSync(testFile, draft.code, 'utf8');
|
|
74
88
|
console.error('Wrote the test; running it now, because a test nobody ran proves nothing…');
|
|
75
89
|
const run = runTest(framework, testFile);
|
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
const LIBRARY = /[\\/](?:node_modules|site-packages|dist-packages|vendor|\.venv|venv|target[\\/]dependency|gems)[\\/]/;
|
|
2
2
|
const RUNTIME = /^(?:node:|internal[\\/]|java\.|javax\.|sun\.|System\.|Microsoft\.)/;
|
|
3
3
|
const ours = (file) => Boolean(file) && !LIBRARY.test(file) && !RUNTIME.test(file);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A `file://` URL back into the path it names.
|
|
6
|
+
*
|
|
7
|
+
* Node has printed ESM stack frames as URLs since v12, and ESM is the default for every project with
|
|
8
|
+
* `"type": "module"` - which this one is. `file:///app/src/pay.ts` and `file:///C:/app/src/pay.ts`
|
|
9
|
+
* are what a real trace contains, and a path is what every later step needs.
|
|
10
|
+
*/
|
|
11
|
+
function asPath(file) {
|
|
12
|
+
const text = String(file ?? '').replace(/\\/g, '/');
|
|
13
|
+
if (!/^file:\/\//i.test(text))
|
|
14
|
+
return text;
|
|
15
|
+
const withoutScheme = text.replace(/^file:\/\/\/?/i, '');
|
|
16
|
+
// A Windows drive keeps its letter; everything else is rooted.
|
|
17
|
+
const path = /^[a-z]:/i.test(withoutScheme) ? withoutScheme : `/${withoutScheme}`;
|
|
18
|
+
try {
|
|
19
|
+
return decodeURIComponent(path);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return path; // a malformed escape is not a reason to lose the frame
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const frame = (file, line, fn) => {
|
|
26
|
+
const path = asPath(file);
|
|
27
|
+
return { file: path, line: Number(line) || 0, fn: String(fn ?? ''), ours: ours(path) };
|
|
28
|
+
};
|
|
10
29
|
/** A stack trace as every language prints it, which is the fallback when there is no tool. */
|
|
11
30
|
export function framesFromStack(stack) {
|
|
12
31
|
const out = [];
|
|
13
32
|
for (const line of stack.split('\n')) {
|
|
14
|
-
// JavaScript: "at handler (/app/src/pay.ts:42:9)" or "at /app/src/pay.ts:42:9"
|
|
15
|
-
|
|
33
|
+
// JavaScript: "at handler (/app/src/pay.ts:42:9)" or "at /app/src/pay.ts:42:9", and - what a
|
|
34
|
+
// modern Node actually prints - "at handler (file:///C:/app/src/pay.ts:42:9)".
|
|
35
|
+
//
|
|
36
|
+
// The file group was `[^():]+?`: no colons allowed. A `file://` URL has one in the scheme and a
|
|
37
|
+
// Windows path has another in the drive letter, so **not one ESM frame could be read** - on any
|
|
38
|
+
// platform, in any project with `"type": "module"`, which is most of them and is this one. The
|
|
39
|
+
// command's whole premise is writing a test against the code the trace names, and it could not
|
|
40
|
+
// name the code. Found on a real trace from this repository: `incident` reported "it names no
|
|
41
|
+
// file inside this project" about `engine/access/render.ts:129`, which is in this project.
|
|
42
|
+
//
|
|
43
|
+
// The group now takes anything and the tail anchors it: `:digits:digits` at the end of the line
|
|
44
|
+
// is what makes a JavaScript frame, and a JVM frame has only one number so it still falls through.
|
|
45
|
+
const js = /^\s*at\s+(?:(?<fn>[^(]+?)\s+\()?(?<file>.+?):(?<line>\d+):\d+\)?\s*$/.exec(line);
|
|
16
46
|
if (js?.groups) {
|
|
17
47
|
out.push(frame(js.groups.file, js.groups.line, (js.groups.fn ?? '').trim()));
|
|
18
48
|
continue;
|
|
@@ -14,7 +14,8 @@ const RULES = `Rules:
|
|
|
14
14
|
- Wait for conditions, never for fixed times.
|
|
15
15
|
- Do not hard-code values that change as the app's content grows (counts, totals, dates, ids) unless the scenario is about that exact value: check their shape instead (a number of books, a date), so the test does not break the day a record is added.
|
|
16
16
|
- One test file, complete, runnable as is, no placeholders. It must actually run here: Tewip runs it against the app now, and a skipped test proves nothing. A guard like the example's (skip where the app lacks something) is fine, as long as it does not skip here.
|
|
17
|
-
- mutation.find must be an exact substring of your code that appears once, inside the main assertion (the value or condition it expects); mutation.replace is what to put there instead so that the test must fail if it is a real check. Keep the code valid: change an expected value, not the syntax
|
|
17
|
+
- mutation.find must be an exact substring of your code that appears once, inside the main assertion (the value or condition it expects); mutation.replace is what to put there instead so that the test must fail if it is a real check. Keep the code valid: change an expected value, not the syntax.
|
|
18
|
+
- To import something from codeUnderTest, use that entry's importPath exactly as given. It is already relative to the folder the test will be written in (testWillBeWrittenIn); its \`file\` is relative to the project root and is there to tell you where the code lives, not how to import it.`;
|
|
18
19
|
const DRAFT_SYSTEM = `You are Tewip's test writer, a senior SDET. Write one automated test for the scenario given.\n\n${RULES}`;
|
|
19
20
|
const REPAIR_SYSTEM = `You are Tewip's test writer, a senior SDET. Your test failed when run against the working app. Read the real error and decide:
|
|
20
21
|
- fault "test": the test is wrong (a locator, a wait, a wrong assumption about the page). Return the corrected full test.
|
|
@@ -44,6 +45,9 @@ function contextFor(ctx, scenario) {
|
|
|
44
45
|
exampleTest: example,
|
|
45
46
|
pages: ctx.pages.map((p) => ({ readAt: p.url, title: p.title, accessibilitySnapshot: p.aria })),
|
|
46
47
|
codeUnderTest: ctx.code?.length ? ctx.code : undefined,
|
|
48
|
+
// Where the test file itself will sit, so a relative import can be written correctly rather
|
|
49
|
+
// than guessed from a project-relative path.
|
|
50
|
+
testWillBeWrittenIn: ctx.testDir || undefined,
|
|
47
51
|
houseRules: ctx.houseRules || undefined,
|
|
48
52
|
};
|
|
49
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.69.
|
|
3
|
+
"version": "0.69.10",
|
|
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": [
|