tewip-ai 0.69.8 → 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.
package/dist/cli/help.js
CHANGED
|
@@ -152,7 +152,9 @@ Usage:
|
|
|
152
152
|
report and it reads that too - Lighthouse runs axe rules, so
|
|
153
153
|
either works. Contrast moves out of the unchecked list, rules
|
|
154
154
|
that map to no WCAG criterion are reported as advice rather than
|
|
155
|
-
barriers (51 of 174 findings on nine real pages
|
|
155
|
+
barriers (51 of 174 findings on nine real pages run through axe;
|
|
156
|
+
a Lighthouse report runs a WCAG-aligned subset and produced 2 of
|
|
157
|
+
363 on six real pages, so expect little advice from one), and what the
|
|
156
158
|
tool could not determine becomes a question. Lighthouse's ten
|
|
157
159
|
manual checks are questions too, never passes. No model, no cost.
|
|
158
160
|
tewip standards [--diff main...HEAD] [--strict] [--out standards.md]
|
|
@@ -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) {
|
|
@@ -61,17 +70,36 @@ export function renderAccess(result, scanned) {
|
|
|
61
70
|
}
|
|
62
71
|
else {
|
|
63
72
|
const stopping = barriers.filter((f) => blocks(f.rule)).length;
|
|
64
|
-
const counts = [
|
|
65
|
-
`${barriers.length} ${barriers.length === 1 ? 'barrier' : 'barriers'} (${stopping} that stop somebody outright)`,
|
|
66
|
-
`${questions.length} for a person to settle`,
|
|
67
|
-
];
|
|
68
|
-
if (accepted.length > 0)
|
|
69
|
-
counts.push(`${accepted.length} already accepted`);
|
|
73
|
+
const counts = [];
|
|
70
74
|
if (browser) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// How many **rules**, then how many places. On Hacker News the browser count was "244
|
|
76
|
+
// barriers", of which **238 were one rule** - colour contrast - on one page. The number was
|
|
77
|
+
// arithmetically right and told the reader nothing they could act on: five things are wrong
|
|
78
|
+
// here, one of them in 238 places. A count of occurrences presented as a count of problems
|
|
79
|
+
// makes a page look hopeless and hides which five things to fix.
|
|
80
|
+
const rules = new Set(browser.barriers.map((f) => f.rule)).size;
|
|
81
|
+
counts.push(browser.barriers.length === 0
|
|
82
|
+
? 'no barrier in the browser run'
|
|
83
|
+
: `${rules} ${rules === 1 ? 'barrier' : 'barriers'} the browser run proves, in ${browser.barriers.length} ${browser.barriers.length === 1 ? 'place' : 'places'}`);
|
|
84
|
+
}
|
|
85
|
+
// And this count is from reading markup, which the sentence never said. Beside a browser count
|
|
86
|
+
// also called "barriers", a reader saw "244 barriers..., 0 barriers" and could not tell whether
|
|
87
|
+
// the second number corrected the first. With no markup read at all there is no count to give:
|
|
88
|
+
// zero files examined is not a finding of zero.
|
|
89
|
+
if (filesRead > 0) {
|
|
90
|
+
counts.push(`${barriers.length} ${barriers.length === 1 ? 'barrier' : 'barriers'} in the markup (${stopping} that stop somebody outright)`);
|
|
91
|
+
counts.push(`${questions.length} for a person to settle`);
|
|
92
|
+
}
|
|
93
|
+
else if (!browser) {
|
|
94
|
+
counts.push('no markup file was read, so nothing was checked');
|
|
74
95
|
}
|
|
96
|
+
else if (questions.length > 0) {
|
|
97
|
+
counts.push(`${questions.length} for a person to settle`);
|
|
98
|
+
}
|
|
99
|
+
if (accepted.length > 0)
|
|
100
|
+
counts.push(`${accepted.length} already accepted`);
|
|
101
|
+
if (browser && browser.questions.length > 0)
|
|
102
|
+
counts.push(`${browser.questions.length} the browser could not determine`);
|
|
75
103
|
lines.push(`**${counts.join(', ')}.**`, '');
|
|
76
104
|
}
|
|
77
105
|
if (browser) {
|
|
@@ -82,8 +110,15 @@ export function renderAccess(result, scanned) {
|
|
|
82
110
|
section('Only a person can settle these', [...questions].sort(worstFirst), lines);
|
|
83
111
|
if (browser && browser.advice.length > 0) {
|
|
84
112
|
// Kept out of the counts above on purpose. These are axe's own best-practice rules, which test
|
|
85
|
-
// no standard: on nine real pages they were 51 of 174 findings, outnumbering
|
|
86
|
-
// Reporting them as equals is how a team learns to ignore the whole report.
|
|
113
|
+
// no standard: on nine real pages **run through axe** they were 51 of 174 findings, outnumbering
|
|
114
|
+
// every real barrier. Reporting them as equals is how a team learns to ignore the whole report.
|
|
115
|
+
//
|
|
116
|
+
// That 51-of-174 is an axe number and only an axe number. Measured again on six real pages
|
|
117
|
+
// through **Lighthouse** - gov.uk, MDN, Hacker News, python.org, w3.org/WAI, Wikipedia - advice
|
|
118
|
+
// was 2 of 363 findings, 0.6%, because Lighthouse's accessibility category runs a WCAG-aligned
|
|
119
|
+
// subset of axe and leaves nearly all the best-practice rules out. Both numbers are real; a
|
|
120
|
+
// published one without the condition it was measured under is the kind of claim this project
|
|
121
|
+
// exists to stop making.
|
|
87
122
|
const rules = [...new Set(browser.advice.map((f) => f.rule))].sort();
|
|
88
123
|
lines.push('## Advice, not barriers', '');
|
|
89
124
|
lines.push(`${browser.advice.length} ${browser.advice.length === 1 ? 'finding comes' : 'findings come'} from axe rules that map to no WCAG criterion - ` +
|
|
@@ -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": [
|