genat-mcp 2.3.1 → 2.3.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/README.md +11 -0
- package/detect-incomplete-step-def.js +140 -0
- package/finalize-pytest-bdd-steps.js +22 -0
- package/fix-pytest-bdd-scenario-path.js +28 -0
- package/index.js +22 -1
- package/package.json +2 -2
- package/pytest-bdd-paths.js +28 -0
- package/write-files.js +8 -25
package/README.md
CHANGED
|
@@ -225,8 +225,19 @@ Login credentials: provide in the prompt (loginUsername, loginPassword) or in pr
|
|
|
225
225
|
|
|
226
226
|
Framework detection: language (TypeScript vs JavaScript vs Python), BDD (Cucumber, pytest-bdd, Behave), and Page Object pattern (e.g. `pages/` directory or `*Page.ts`, `*Page.js`, `*_page.py` files). Returns JSON with `testCode`, `featureFile`, `stepDefCode`, `pageObjectCode` (and `_written` paths when writing).
|
|
227
227
|
|
|
228
|
+
**Response fields for calling applications:** When `stepDefCode` is present, the MCP may set **`_stepDefIncomplete`** (boolean) and **`_stepDefIncompleteReason`** (string) using a lightweight heuristic (unclosed strings/delimiters, line continuation at EOF). Treat `true` as a signal to warn the user, retry generation, or block merge—do not assume the step file is runnable. Other optional fields from the workflow include **`usedLogin`** and **`loginReason`** when using login-capable workflows.
|
|
229
|
+
|
|
228
230
|
Generated tests include keyboard accessibility checks (Tab order, focus) by default. For complex pages, the workflow uses page complexity (regions, tables, widgets) to scale assertion count.
|
|
229
231
|
|
|
232
|
+
### Feature ↔ step parity (consumer CI)
|
|
233
|
+
|
|
234
|
+
GenAT does not guarantee that every Gherkin line has an implemented step. For **pytest-bdd** projects, the calling application or CI can:
|
|
235
|
+
|
|
236
|
+
1. Parse `*.feature` step lines (after `Given`/`When`/`Then`/`And`/`But`).
|
|
237
|
+
2. Confirm `accessibility_steps.py` (or your step module) defines matching `@given`/`@when`/`@then` patterns (or run `pytest --collect-only` / a dry run).
|
|
238
|
+
|
|
239
|
+
Keep this as an optional pre-merge check alongside `_stepDefIncomplete`.
|
|
240
|
+
|
|
230
241
|
## Sample prompts
|
|
231
242
|
|
|
232
243
|
See [prompts.md](prompts.md) for a full list of prompts including specific parent folders, login, JIRA, and combined options.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight heuristic: detect likely truncated or syntactically incomplete pytest-bdd step Python.
|
|
3
|
+
* For calling apps to surface _stepDefIncomplete and retry or warn.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} code
|
|
8
|
+
* @returns {{ incomplete: boolean, reason: string | null }}
|
|
9
|
+
*/
|
|
10
|
+
export function detectStepDefIncomplete(code) {
|
|
11
|
+
if (code == null || typeof code !== 'string') {
|
|
12
|
+
return { incomplete: false, reason: null };
|
|
13
|
+
}
|
|
14
|
+
const t = code.replace(/\r\n/g, '\n').trimEnd();
|
|
15
|
+
if (t.length === 0) {
|
|
16
|
+
return { incomplete: true, reason: 'empty' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const triples = (t.match(/"""/g) || []).length;
|
|
20
|
+
if (triples % 2 !== 0) {
|
|
21
|
+
return { incomplete: true, reason: 'unclosed_triple_double_quoted_string' };
|
|
22
|
+
}
|
|
23
|
+
const tripleSingle = (t.match(/'''/g) || []).length;
|
|
24
|
+
if (tripleSingle % 2 !== 0) {
|
|
25
|
+
return { incomplete: true, reason: 'unclosed_triple_single_quoted_string' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lines = t.split('\n');
|
|
29
|
+
const lastLine = lines[lines.length - 1];
|
|
30
|
+
if (/\\\s*$/.test(lastLine)) {
|
|
31
|
+
return { incomplete: true, reason: 'line_continuation_at_eof' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const depth = bracketDepthAtEnd(t);
|
|
35
|
+
if (depth !== 0) {
|
|
36
|
+
return { incomplete: true, reason: 'unclosed_delimiters' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { incomplete: false, reason: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rough balance of (), [], {} at end of file, skipping strings and comments.
|
|
44
|
+
* @param {string} s
|
|
45
|
+
* @returns {number} 0 if balanced, non-zero if likely incomplete
|
|
46
|
+
*/
|
|
47
|
+
function bracketDepthAtEnd(s) {
|
|
48
|
+
let i = 0;
|
|
49
|
+
const stack = [];
|
|
50
|
+
let state = 'code';
|
|
51
|
+
|
|
52
|
+
while (i < s.length) {
|
|
53
|
+
const c = s[i];
|
|
54
|
+
|
|
55
|
+
if (state === 'code') {
|
|
56
|
+
if (c === '#') {
|
|
57
|
+
while (i < s.length && s[i] !== '\n') i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (s.slice(i, i + 3) === '"""') {
|
|
61
|
+
state = 'triple_double';
|
|
62
|
+
i += 3;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (s.slice(i, i + 3) === "'''") {
|
|
66
|
+
state = 'triple_single';
|
|
67
|
+
i += 3;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === '"' || c === "'") {
|
|
71
|
+
state = c === '"' ? 'string_double' : 'string_single';
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === '(') stack.push('(');
|
|
76
|
+
else if (c === '[') stack.push('[');
|
|
77
|
+
else if (c === '{') stack.push('{');
|
|
78
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
79
|
+
const want = c === ')' ? '(' : c === ']' ? '[' : '{';
|
|
80
|
+
if (stack.length === 0 || stack[stack.length - 1] !== want) {
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
stack.pop();
|
|
84
|
+
}
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (state === 'string_single') {
|
|
90
|
+
if (c === '\\') {
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (c === "'") {
|
|
95
|
+
state = 'code';
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (state === 'string_double') {
|
|
102
|
+
if (c === '\\') {
|
|
103
|
+
i += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (c === '"') {
|
|
107
|
+
state = 'code';
|
|
108
|
+
}
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state === 'triple_double') {
|
|
114
|
+
if (s.slice(i, i + 3) === '"""') {
|
|
115
|
+
state = 'code';
|
|
116
|
+
i += 3;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (state === 'triple_single') {
|
|
124
|
+
if (s.slice(i, i + 3) === "'''") {
|
|
125
|
+
state = 'code';
|
|
126
|
+
i += 3;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (state !== 'code') {
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
return stack.length;
|
|
140
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip accidental Gherkin prefix and rewrite scenario() paths for pytest-bdd step modules.
|
|
3
|
+
*/
|
|
4
|
+
import { rewriteScenarioFeaturePaths } from './fix-pytest-bdd-scenario-path.js';
|
|
5
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
6
|
+
import { stripLeadingGherkinFromPythonSteps } from './strip-gherkin-from-python-steps.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} root - project root
|
|
10
|
+
* @param {string} stepDefCode
|
|
11
|
+
* @param {{ featureRel?: string, stepRel?: string }} [paths] - optional overrides (e.g. from suggestedPaths) for where feature and step files are written
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function finalizePytestBddStepDef(root, stepDefCode, paths) {
|
|
15
|
+
if (!stepDefCode || typeof stepDefCode !== 'string') return stepDefCode;
|
|
16
|
+
const pb = resolvePytestBddPaths(root);
|
|
17
|
+
const featureRel = paths?.featureRel ?? pb.featureRel;
|
|
18
|
+
const stepRel = paths?.stepRel ?? pb.stepRel;
|
|
19
|
+
let s = stripLeadingGherkinFromPythonSteps(stepDefCode);
|
|
20
|
+
s = rewriteScenarioFeaturePaths(s, root, featureRel, stepRel);
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite pytest-bdd scenario() / @scenario() first argument so the .feature path is correct
|
|
3
|
+
* relative to the step file directory (matches where write-files places the feature).
|
|
4
|
+
*/
|
|
5
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @param {string} root - project root (absolute or resolved)
|
|
10
|
+
* @param {string} featureRel - path to .feature relative to project root (posix-style segments)
|
|
11
|
+
* @param {string} stepRel - path to step .py relative to project root
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function rewriteScenarioFeaturePaths(code, root, featureRel, stepRel) {
|
|
15
|
+
if (!code || typeof code !== 'string') return code;
|
|
16
|
+
const rootAbs = resolve(root);
|
|
17
|
+
const featureAbs = join(rootAbs, featureRel);
|
|
18
|
+
const stepDir = dirname(join(rootAbs, stepRel));
|
|
19
|
+
let posixRel = relative(stepDir, featureAbs);
|
|
20
|
+
if (!posixRel.startsWith('.')) {
|
|
21
|
+
posixRel = posixRel ? `./${posixRel}` : '.';
|
|
22
|
+
}
|
|
23
|
+
posixRel = posixRel.replace(/\\/g, '/');
|
|
24
|
+
|
|
25
|
+
return code.replace(/(@scenario|scenario)\s*\(\s*(['"])([^'"]*\.feature)\2/gi, (_m, decorator, quote) => {
|
|
26
|
+
return `${decorator}(${quote}${posixRel}${quote}`;
|
|
27
|
+
});
|
|
28
|
+
}
|
package/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { detectFramework } from './detect-framework.js';
|
|
13
13
|
import { detectLogin } from './detect-login.js';
|
|
14
|
+
import { detectStepDefIncomplete } from './detect-incomplete-step-def.js';
|
|
15
|
+
import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
|
|
16
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
14
17
|
import { writeGeneratedFiles } from './write-files.js';
|
|
15
18
|
import { loadOclcAccessibilityContext } from './oclc-context.js';
|
|
16
19
|
|
|
@@ -56,7 +59,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
|
|
|
56
59
|
const server = new McpServer(
|
|
57
60
|
{
|
|
58
61
|
name: 'GenAT',
|
|
59
|
-
version: '2.3.
|
|
62
|
+
version: '2.3.3',
|
|
60
63
|
},
|
|
61
64
|
{
|
|
62
65
|
capabilities: {
|
|
@@ -269,6 +272,24 @@ server.registerTool(
|
|
|
269
272
|
};
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
const bddResolved = (data?.bddFramework ?? framework.bddFramework ?? 'none').toString().toLowerCase();
|
|
276
|
+
if (framework.scriptType === 'python' && bddResolved === 'pytest-bdd' && resolvedPath) {
|
|
277
|
+
const pb = resolvePytestBddPaths(resolvedPath);
|
|
278
|
+
const sp = data.suggestedPaths || {};
|
|
279
|
+
const pathOverrides = {
|
|
280
|
+
featureRel: sp.feature || pb.featureRel,
|
|
281
|
+
stepRel: sp.stepDef || pb.stepRel,
|
|
282
|
+
};
|
|
283
|
+
if (data.stepDefCode) data.stepDefCode = finalizePytestBddStepDef(resolvedPath, data.stepDefCode, pathOverrides);
|
|
284
|
+
if (data.testCode) data.testCode = finalizePytestBddStepDef(resolvedPath, data.testCode, pathOverrides);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (data.stepDefCode && typeof data.stepDefCode === 'string') {
|
|
288
|
+
const inc = detectStepDefIncomplete(data.stepDefCode);
|
|
289
|
+
data._stepDefIncomplete = inc.incomplete;
|
|
290
|
+
if (inc.reason) data._stepDefIncompleteReason = inc.reason;
|
|
291
|
+
}
|
|
292
|
+
|
|
272
293
|
if (writeToProject && resolvedPath && data) {
|
|
273
294
|
if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
|
|
274
295
|
try {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genat-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"mcpName": "io.github.asokans@oclc.org/genat",
|
|
5
5
|
"description": "MCP server GenAT: generate accessibility tests via n8n workflow (url + project folder)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": { "genat-mcp": "index.js" },
|
|
9
9
|
"engines": { "node": ">=20" },
|
|
10
|
-
"files": ["index.js", "detect-framework.js", "detect-login.js", "write-files.js", "strip-gherkin-from-python-steps.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
|
|
10
|
+
"files": ["index.js", "detect-framework.js", "detect-login.js", "detect-incomplete-step-def.js", "write-files.js", "strip-gherkin-from-python-steps.js", "finalize-pytest-bdd-steps.js", "fix-pytest-bdd-scenario-path.js", "pytest-bdd-paths.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
|
|
11
11
|
"keywords": ["mcp", "accessibility", "playwright", "n8n", "model-context-protocol", "a11y", "testing"],
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve pytest-bdd feature / step / spec paths from project layout (same rules as write-files).
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DIR = 'tests/accessibility';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @returns {{ featureRel: string, stepRel: string, specRel: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePytestBddPaths(root) {
|
|
14
|
+
let featureRel;
|
|
15
|
+
if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
|
|
16
|
+
else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
|
|
17
|
+
else featureRel = join('features', 'accessibility.feature');
|
|
18
|
+
|
|
19
|
+
let stepRel;
|
|
20
|
+
if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
|
|
21
|
+
else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
|
|
22
|
+
else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
|
|
23
|
+
else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
|
|
24
|
+
else stepRel = join('step_defs', 'accessibility_steps.py');
|
|
25
|
+
|
|
26
|
+
const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
|
|
27
|
+
return { featureRel, stepRel, specRel };
|
|
28
|
+
}
|
package/write-files.js
CHANGED
|
@@ -2,34 +2,13 @@
|
|
|
2
2
|
* Write generated accessibility test files into the project folder.
|
|
3
3
|
* Uses suggested paths from the workflow response when present.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync } from 'fs';
|
|
6
5
|
import { writeFile, mkdir } from 'fs/promises';
|
|
7
6
|
import { dirname, join, resolve } from 'path';
|
|
8
|
-
import {
|
|
7
|
+
import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
|
|
8
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
9
9
|
|
|
10
10
|
const DEFAULT_DIR = 'tests/accessibility';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* @param {string} root
|
|
14
|
-
* @returns {{ featureRel: string, stepRel: string, specRel: string }}
|
|
15
|
-
*/
|
|
16
|
-
function resolvePytestBddPaths(root) {
|
|
17
|
-
let featureRel;
|
|
18
|
-
if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
|
|
19
|
-
else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
|
|
20
|
-
else featureRel = join('features', 'accessibility.feature');
|
|
21
|
-
|
|
22
|
-
let stepRel;
|
|
23
|
-
if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
|
|
24
|
-
else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
|
|
25
|
-
else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
|
|
26
|
-
else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
|
|
27
|
-
else stepRel = join('step_defs', 'accessibility_steps.py');
|
|
28
|
-
|
|
29
|
-
const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
|
|
30
|
-
return { featureRel, stepRel, specRel };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
12
|
/**
|
|
34
13
|
* @param {string} parentProjectFolder - project root path
|
|
35
14
|
* @param {{ testCode?: string, scriptType?: string, bddFramework?: string, featureFile?: string, stepDefCode?: string, pageObjectCode?: string, suggestedPaths?: Record<string, string> }} data - workflow response
|
|
@@ -55,10 +34,14 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
|
|
|
55
34
|
|
|
56
35
|
if (usePytestBddLayout) {
|
|
57
36
|
const pb = resolvePytestBddPaths(root);
|
|
37
|
+
const pathOverrides = {
|
|
38
|
+
featureRel: suggested.feature || pb.featureRel,
|
|
39
|
+
stepRel: suggested.stepDef || pb.stepRel,
|
|
40
|
+
};
|
|
58
41
|
const stepDefStripped = data.stepDefCode
|
|
59
|
-
?
|
|
42
|
+
? finalizePytestBddStepDef(root, data.stepDefCode, pathOverrides)
|
|
60
43
|
: '';
|
|
61
|
-
const testStripped = data.testCode ?
|
|
44
|
+
const testStripped = data.testCode ? finalizePytestBddStepDef(root, data.testCode, pathOverrides) : '';
|
|
62
45
|
const dup =
|
|
63
46
|
testStripped &&
|
|
64
47
|
stepDefStripped &&
|