@qa-gentic/stlc-agents 1.0.27 → 1.0.29
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/ARCHITECTURE-ADO.md +350 -0
- package/ARCHITECTURE-JIRA.md +203 -0
- package/QUICKSTART-ADO.md +400 -0
- package/QUICKSTART-JIRA.md +334 -0
- package/README.md +49 -0
- package/bin/postinstall.js +14 -4
- package/package.json +19 -7
- package/skills/migrate-framework/SKILL.md +207 -0
- package/src/stlc_agents/agent_migration/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
- package/src/stlc_agents/agent_migration/cli.py +217 -0
- package/src/stlc_agents/agent_migration/detector.py +81 -0
- package/src/stlc_agents/agent_migration/mapper.py +439 -0
- package/src/stlc_agents/agent_migration/reporter.py +86 -0
- package/src/stlc_agents/agent_migration/server.py +267 -0
- package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
- package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
- package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
- package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
- package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
- package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
- package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
- package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
- package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent 6: QA Migration Agent — MCP Server
|
|
3
|
+
|
|
4
|
+
Migrates existing Playwright test suites (plain JS/TS or Playwright+Cucumber JS/TS)
|
|
5
|
+
to the Helix-QA agent-compatible structure with full self-healing support.
|
|
6
|
+
|
|
7
|
+
Pipeline per source file:
|
|
8
|
+
JS → TS conversion → locator modernisation (CSS/XPath → getByRole)
|
|
9
|
+
→ healer injection (TimingHealer + VisualIntentChecker on ALL interactions)
|
|
10
|
+
→ import path fixing → Helix-QA disk write → MIGRATION-REPORT.md
|
|
11
|
+
|
|
12
|
+
Tools exposed:
|
|
13
|
+
detect_framework — auto-detect source framework type and language
|
|
14
|
+
preview_migration — dry-run: enumerate what would be written / conflicts
|
|
15
|
+
migrate_framework — full migration with per-file conflict handling
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from mcp.server import Server
|
|
24
|
+
from mcp.server.stdio import stdio_server
|
|
25
|
+
from mcp import types
|
|
26
|
+
|
|
27
|
+
from .detector import detect_framework as _detect
|
|
28
|
+
from .mapper import map_source_files
|
|
29
|
+
from ._migrate import run_migration
|
|
30
|
+
|
|
31
|
+
app = Server("qa-migration")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Tool definitions
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
@app.list_tools()
|
|
39
|
+
async def list_tools() -> list[types.Tool]:
|
|
40
|
+
return [
|
|
41
|
+
types.Tool(
|
|
42
|
+
name="detect_framework",
|
|
43
|
+
description=(
|
|
44
|
+
"Auto-detect the Playwright framework type from a source test directory. "
|
|
45
|
+
"Returns: framework ('playwright-bdd-ts' | 'playwright-bdd-js' | "
|
|
46
|
+
"'playwright-ts' | 'playwright-js'), language, bdd flag, config files "
|
|
47
|
+
"found, and file counts (.ts / .js / .feature)."
|
|
48
|
+
),
|
|
49
|
+
inputSchema={
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"source_dir": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Absolute path to the source test project directory.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"required": ["source_dir"],
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
types.Tool(
|
|
61
|
+
name="preview_migration",
|
|
62
|
+
description=(
|
|
63
|
+
"Dry-run: enumerate all files that would be migrated, their Helix-QA "
|
|
64
|
+
"target paths, roles, and any conflicts (target already exists). "
|
|
65
|
+
"Nothing is written to disk. Use this before migrate_framework to "
|
|
66
|
+
"review the migration plan and spot any surprises."
|
|
67
|
+
),
|
|
68
|
+
inputSchema={
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"source_dir": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Absolute path to source test project directory.",
|
|
74
|
+
},
|
|
75
|
+
"helix_root": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Absolute path to the Helix-QA root directory.",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
"required": ["source_dir", "helix_root"],
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
types.Tool(
|
|
84
|
+
name="migrate_framework",
|
|
85
|
+
description=(
|
|
86
|
+
"Migrate an existing Playwright test framework to the Helix-QA "
|
|
87
|
+
"agent-compatible structure.\n\n"
|
|
88
|
+
"Transformations applied:\n"
|
|
89
|
+
" • JS → TS (CommonJS require → ES import, module.exports → export)\n"
|
|
90
|
+
" • Locator modernisation: [data-testid] → getByTestId, text= → getByText, "
|
|
91
|
+
"[aria-label] → getByLabel, [placeholder] → getByPlaceholder, "
|
|
92
|
+
"role=X[name=Y] → getByRole. Complex XPath/CSS flagged with TODO.\n"
|
|
93
|
+
" • Healer injection on ALL interactions: page.click → "
|
|
94
|
+
"_healer.clickWithHealing, page.fill → _healer.fillWithHealing, "
|
|
95
|
+
"page.goto wrapped with TimingHealer, expect.toBeVisible wrapped "
|
|
96
|
+
"with VisualIntentChecker. Applies to page objects AND step definitions.\n"
|
|
97
|
+
" • Import paths updated to Helix structure "
|
|
98
|
+
"(@utils/locators/*, @hooks/pageFixture, etc.).\n"
|
|
99
|
+
" • playwright.config merged into Helix playwright.config.ts.\n"
|
|
100
|
+
" • cucumber.config profiles merged into src/config/cucumber.config.ts.\n"
|
|
101
|
+
" • MIGRATION-REPORT.md written to helix_root.\n\n"
|
|
102
|
+
"Supports: plain Playwright JS/TS, Playwright+Cucumber JS/TS."
|
|
103
|
+
),
|
|
104
|
+
inputSchema={
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"source_dir": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Absolute path to source test project directory.",
|
|
110
|
+
},
|
|
111
|
+
"helix_root": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Absolute path to the Helix-QA root directory.",
|
|
114
|
+
},
|
|
115
|
+
"conflict_strategy": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"enum": ["interactive", "overwrite", "skip"],
|
|
118
|
+
"description": (
|
|
119
|
+
"'overwrite' — replace existing Helix files. "
|
|
120
|
+
"'skip' — keep existing files untouched. "
|
|
121
|
+
"'interactive' (default) — write and report conflicts "
|
|
122
|
+
"for user review."
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
"dry_run": {
|
|
126
|
+
"type": "boolean",
|
|
127
|
+
"description": (
|
|
128
|
+
"When true, return the migration plan without writing any files."
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
"required": ["source_dir", "helix_root"],
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Tool dispatch
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
@app.call_tool()
|
|
143
|
+
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
144
|
+
try:
|
|
145
|
+
result = await _dispatch(name, arguments)
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
result = {"error": str(exc), "tool": name}
|
|
148
|
+
|
|
149
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def _dispatch(name: str, args: dict) -> dict:
|
|
153
|
+
if name == "detect_framework":
|
|
154
|
+
return _detect(args["source_dir"])
|
|
155
|
+
|
|
156
|
+
if name == "preview_migration":
|
|
157
|
+
return _preview(args["source_dir"], args["helix_root"])
|
|
158
|
+
|
|
159
|
+
if name == "migrate_framework":
|
|
160
|
+
return _migrate(
|
|
161
|
+
args["source_dir"],
|
|
162
|
+
args["helix_root"],
|
|
163
|
+
args.get("conflict_strategy", "interactive"),
|
|
164
|
+
bool(args.get("dry_run", False)),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return {"error": f"Unknown tool: {name}"}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Tool implementations
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _preview(source_dir: str, helix_root: str) -> dict:
|
|
175
|
+
detection = _detect(source_dir)
|
|
176
|
+
if "error" in detection:
|
|
177
|
+
return detection
|
|
178
|
+
|
|
179
|
+
files = map_source_files(source_dir)
|
|
180
|
+
mappable = [f for f in files if f["target"] is not None]
|
|
181
|
+
unknown = [f for f in files if f["role"] == "unknown"]
|
|
182
|
+
root = Path(helix_root)
|
|
183
|
+
|
|
184
|
+
conflicts = [
|
|
185
|
+
{"source": f["relative"], "target": f["target"]}
|
|
186
|
+
for f in mappable
|
|
187
|
+
if f["target"] and (root / f["target"]).exists()
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"framework": detection.get("framework", "unknown"),
|
|
192
|
+
"language": detection.get("language", "unknown"),
|
|
193
|
+
"bdd": detection.get("bdd", False),
|
|
194
|
+
"file_counts": detection.get("file_counts", {}),
|
|
195
|
+
"total_files": len(files),
|
|
196
|
+
"mappable": len(mappable),
|
|
197
|
+
"unknown": len(unknown),
|
|
198
|
+
"conflicts": len(conflicts),
|
|
199
|
+
"conflict_list": conflicts,
|
|
200
|
+
"file_plan": [
|
|
201
|
+
{
|
|
202
|
+
"source": f["relative"],
|
|
203
|
+
"target": f["target"],
|
|
204
|
+
"role": f["role"],
|
|
205
|
+
"needs_js_to_ts": f.get("needs_js_to_ts", False),
|
|
206
|
+
}
|
|
207
|
+
for f in mappable
|
|
208
|
+
],
|
|
209
|
+
"unknown_files": [f["relative"] for f in unknown],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _migrate(
|
|
214
|
+
source_dir: str,
|
|
215
|
+
helix_root: str,
|
|
216
|
+
conflict_strategy: str,
|
|
217
|
+
dry_run: bool,
|
|
218
|
+
) -> dict:
|
|
219
|
+
detection = _detect(source_dir)
|
|
220
|
+
if "error" in detection:
|
|
221
|
+
return detection
|
|
222
|
+
|
|
223
|
+
framework = detection.get("framework", "unknown")
|
|
224
|
+
files = map_source_files(source_dir)
|
|
225
|
+
mappable = [f for f in files if f["target"] is not None]
|
|
226
|
+
|
|
227
|
+
if dry_run:
|
|
228
|
+
return {
|
|
229
|
+
"dry_run": True,
|
|
230
|
+
"framework": framework,
|
|
231
|
+
"total": len(mappable),
|
|
232
|
+
"would_migrate": [
|
|
233
|
+
{"source": f["relative"], "target": f["target"], "role": f["role"]}
|
|
234
|
+
for f in mappable
|
|
235
|
+
],
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
result = run_migration(
|
|
239
|
+
source_dir=source_dir,
|
|
240
|
+
helix_root=helix_root,
|
|
241
|
+
file_mappings=mappable,
|
|
242
|
+
conflict_strategy=conflict_strategy,
|
|
243
|
+
framework=framework,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Summarise for the MCP response
|
|
247
|
+
result["summary"] = {
|
|
248
|
+
"framework": framework,
|
|
249
|
+
"written": len(result["written"]),
|
|
250
|
+
"skipped": len(result["skipped"]),
|
|
251
|
+
"conflicts": len(result["conflicts"]),
|
|
252
|
+
"todos": result["todo_count"],
|
|
253
|
+
"report_path": result.get("report_path"),
|
|
254
|
+
}
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Entry point
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def main() -> None:
|
|
263
|
+
asyncio.run(stdio_server(app))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|
|
File without changes
|