@qa-gentic/agents 1.1.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.
Files changed (52) hide show
  1. package/README.md +203 -0
  2. package/bin/postinstall.js +75 -0
  3. package/bin/qa-stlc.js +76 -0
  4. package/package.json +48 -0
  5. package/skills/qa-stlc/AGENT-BEHAVIOR.md +373 -0
  6. package/skills/qa-stlc/deduplication-protocol.md +303 -0
  7. package/skills/qa-stlc/generate-gherkin.md +550 -0
  8. package/skills/qa-stlc/generate-playwright-code.md +439 -0
  9. package/skills/qa-stlc/generate-test-cases.md +176 -0
  10. package/skills/qa-stlc/write-helix-files.md +349 -0
  11. package/src/cmd-init.js +84 -0
  12. package/src/cmd-mcp-config.js +177 -0
  13. package/src/cmd-skills.js +124 -0
  14. package/src/cmd-verify.js +129 -0
  15. package/src/qa_stlc_agents/__init__.py +0 -0
  16. package/src/qa_stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
  17. package/src/qa_stlc_agents/agent_gherkin_generator/__init__.py +0 -0
  18. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  19. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
  20. package/src/qa_stlc_agents/agent_gherkin_generator/server.py +502 -0
  21. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__init__.py +0 -0
  22. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  23. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
  24. package/src/qa_stlc_agents/agent_gherkin_generator/tools/ado_gherkin.py +854 -0
  25. package/src/qa_stlc_agents/agent_helix_writer/__init__.py +0 -0
  26. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
  27. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
  28. package/src/qa_stlc_agents/agent_helix_writer/server.py +529 -0
  29. package/src/qa_stlc_agents/agent_helix_writer/tools/__init__.py +0 -0
  30. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  31. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
  32. package/src/qa_stlc_agents/agent_helix_writer/tools/helix_write.py +622 -0
  33. package/src/qa_stlc_agents/agent_playwright_generator/__init__.py +0 -0
  34. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  35. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
  36. package/src/qa_stlc_agents/agent_playwright_generator/server.py +2771 -0
  37. package/src/qa_stlc_agents/agent_playwright_generator/tools/__init__.py +0 -0
  38. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  39. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
  40. package/src/qa_stlc_agents/agent_playwright_generator/tools/ado_attach.py +62 -0
  41. package/src/qa_stlc_agents/agent_test_case_manager/__init__.py +0 -0
  42. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
  43. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
  44. package/src/qa_stlc_agents/agent_test_case_manager/server.py +483 -0
  45. package/src/qa_stlc_agents/agent_test_case_manager/tools/__init__.py +0 -0
  46. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  47. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
  48. package/src/qa_stlc_agents/agent_test_case_manager/tools/ado_workitem.py +302 -0
  49. package/src/qa_stlc_agents/shared/__init__.py +0 -0
  50. package/src/qa_stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/qa_stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
  52. package/src/qa_stlc_agents/shared/auth.py +119 -0
@@ -0,0 +1,302 @@
1
+ """
2
+ ado_workitem.py — Azure DevOps Work Item REST API calls for the Test Case Manager.
3
+
4
+ All functions use get_auth_headers() from auth.py — no credentials in this file.
5
+
6
+ Public API:
7
+ fetch_work_item(org_url, project, work_item_id) -> dict
8
+ create_test_case(org_url, project, title, steps, ...) -> dict
9
+ link_test_cases_to_work_item(org_url, project, wi_id, tc_ids) -> dict
10
+ get_linked_test_cases(org_url, project, work_item_id) -> dict
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import xml.etree.ElementTree as ET
16
+ from typing import Dict, List, Optional
17
+
18
+ import requests
19
+
20
+ from qa_stlc_agents.shared.auth import get_auth_headers
21
+
22
+ _API = "7.1"
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # fetch_work_item
27
+ # ---------------------------------------------------------------------------
28
+
29
+ def fetch_work_item(org_url: str, project: str, work_item_id: int) -> dict:
30
+ """Fetch a PBI or Bug with all relevant fields and parent feature."""
31
+ org_url = org_url.rstrip("/")
32
+ headers = get_auth_headers()
33
+
34
+ resp = requests.get(
35
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
36
+ headers=headers,
37
+ params={"$expand": "relations", "api-version": _API},
38
+ timeout=30,
39
+ )
40
+ resp.raise_for_status()
41
+ data = resp.json()
42
+ fields = data.get("fields", {})
43
+
44
+ wi_type = fields.get("System.WorkItemType", "")
45
+
46
+ # ── Epic guard — test cases must NEVER be created for Epics ─────────────
47
+ if wi_type == "Epic":
48
+ return {
49
+ "error": "epic_not_supported",
50
+ "work_item_type": wi_type,
51
+ "work_item_id": work_item_id,
52
+ "message": (
53
+ f"Work item {work_item_id} is an Epic. "
54
+ "Manual test cases cannot be created or linked to an Epic. "
55
+ "Test cases in ADO are always scoped to PBIs, Bugs, or Features — "
56
+ "never to Epics. To generate tests for this Epic, use "
57
+ "qa-gherkin-generator:fetch_epic_hierarchy to walk its child Features "
58
+ "and PBIs/Bugs, then call create_and_link_test_cases on each individual "
59
+ "Feature or PBI/Bug work item ID, not on the Epic itself."
60
+ ),
61
+ }
62
+
63
+ wi = {
64
+ "id": data["id"],
65
+ "type": fields.get("System.WorkItemType", ""),
66
+ "title": fields.get("System.Title", ""),
67
+ "description": _strip_html(fields.get("System.Description", "") or ""),
68
+ "acceptance_criteria": _strip_html(
69
+ fields.get("Microsoft.VSTS.Common.AcceptanceCriteria", "") or ""
70
+ ),
71
+ "repro_steps": _strip_html(
72
+ fields.get("Microsoft.VSTS.TCM.ReproSteps", "") or ""
73
+ ),
74
+ "state": fields.get("System.State", ""),
75
+ "priority": fields.get("Microsoft.VSTS.Common.Priority", ""),
76
+ "story_points": fields.get("Microsoft.VSTS.Scheduling.StoryPoints", ""),
77
+ "tags": fields.get("System.Tags", ""),
78
+ "assigned_to": (
79
+ (fields.get("System.AssignedTo") or {}).get("displayName", "")
80
+ ),
81
+ "area_path": fields.get("System.AreaPath", ""),
82
+ "iteration_path": fields.get("System.IterationPath", ""),
83
+ }
84
+
85
+ # Fetch parent feature if present
86
+ parent_feature = None
87
+ for rel in data.get("relations", []):
88
+ if rel.get("rel") == "System.LinkTypes.Hierarchy-Reverse":
89
+ try:
90
+ pr = requests.get(
91
+ rel["url"] + f"?api-version={_API}",
92
+ headers=get_auth_headers(),
93
+ timeout=30,
94
+ )
95
+ if pr.ok:
96
+ pf = pr.json().get("fields", {})
97
+ parent_feature = {
98
+ "id": pr.json().get("id"),
99
+ "type": pf.get("System.WorkItemType", ""),
100
+ "title": pf.get("System.Title", ""),
101
+ }
102
+ except Exception:
103
+ pass
104
+ break
105
+
106
+ # Existing linked test cases count
107
+ existing_tc_count = sum(
108
+ 1 for r in data.get("relations", [])
109
+ if r.get("rel") == "Microsoft.VSTS.Common.TestedBy-Forward"
110
+ )
111
+
112
+ # Coverage hints derived from text
113
+ coverage_hints = _extract_coverage_hints(wi)
114
+
115
+ return {
116
+ "work_item": wi,
117
+ "parent_feature": parent_feature,
118
+ "existing_test_cases_count": existing_tc_count,
119
+ "coverage_hints": coverage_hints,
120
+ }
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # create_test_case
125
+ # ---------------------------------------------------------------------------
126
+
127
+ def create_test_case(
128
+ org_url: str,
129
+ project: str,
130
+ title: str,
131
+ steps: List[Dict],
132
+ priority: int = 2,
133
+ area_path: Optional[str] = None,
134
+ iteration_path: Optional[str] = None,
135
+ ) -> dict:
136
+ """Create a Test Case work item with XML steps in ADO."""
137
+ org_url = org_url.rstrip("/")
138
+ headers = get_auth_headers("application/json-patch+json")
139
+
140
+ steps_xml = _build_steps_xml(steps)
141
+
142
+ patch = [
143
+ {"op": "add", "path": "/fields/System.Title", "value": title},
144
+ {"op": "add", "path": "/fields/System.State", "value": "Design"},
145
+ {"op": "add", "path": "/fields/Microsoft.VSTS.Common.Priority", "value": priority},
146
+ ]
147
+ if steps_xml:
148
+ patch.append({"op": "add", "path": "/fields/Microsoft.VSTS.TCM.Steps", "value": steps_xml})
149
+ if area_path:
150
+ patch.append({"op": "add", "path": "/fields/System.AreaPath", "value": area_path})
151
+ if iteration_path:
152
+ patch.append({"op": "add", "path": "/fields/System.IterationPath", "value": iteration_path})
153
+
154
+ resp = requests.post(
155
+ f"{org_url}/{project}/_apis/wit/workitems/$Test%20Case",
156
+ headers=headers,
157
+ params={"api-version": _API},
158
+ json=patch,
159
+ timeout=30,
160
+ )
161
+ resp.raise_for_status()
162
+ created = resp.json()
163
+ return {
164
+ "success": True,
165
+ "test_case_id": created["id"],
166
+ "title": title,
167
+ "url": created.get("_links", {}).get("html", {}).get("href", ""),
168
+ }
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # link_test_cases_to_work_item
173
+ # ---------------------------------------------------------------------------
174
+
175
+ def link_test_cases_to_work_item(
176
+ org_url: str,
177
+ project: str,
178
+ work_item_id: int,
179
+ test_case_ids: List[int],
180
+ ) -> dict:
181
+ """Create TestedBy-Forward links from a work item to test cases."""
182
+ org_url = org_url.rstrip("/")
183
+ headers = get_auth_headers("application/json-patch+json")
184
+
185
+ relations = [
186
+ {
187
+ "op": "add",
188
+ "path": "/relations/-",
189
+ "value": {
190
+ "rel": "Microsoft.VSTS.Common.TestedBy-Forward",
191
+ "url": f"{org_url}/{project}/_apis/wit/workItems/{tc_id}",
192
+ },
193
+ }
194
+ for tc_id in test_case_ids
195
+ ]
196
+
197
+ resp = requests.patch(
198
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
199
+ headers=headers,
200
+ params={"api-version": _API},
201
+ json=relations,
202
+ timeout=30,
203
+ )
204
+ resp.raise_for_status()
205
+ return {
206
+ "success": True,
207
+ "work_item_id": work_item_id,
208
+ "linked_count": len(test_case_ids),
209
+ "test_case_ids": test_case_ids,
210
+ }
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # get_linked_test_cases
215
+ # ---------------------------------------------------------------------------
216
+
217
+ def get_linked_test_cases(org_url: str, project: str, work_item_id: int) -> dict:
218
+ """Return all test cases linked via TestedBy to a work item."""
219
+ org_url = org_url.rstrip("/")
220
+ headers = get_auth_headers()
221
+
222
+ resp = requests.get(
223
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
224
+ headers=headers,
225
+ params={"$expand": "relations", "api-version": _API},
226
+ timeout=30,
227
+ )
228
+ resp.raise_for_status()
229
+
230
+ linked = []
231
+ for rel in resp.json().get("relations", []):
232
+ if rel.get("rel") == "Microsoft.VSTS.Common.TestedBy-Forward":
233
+ tc_id = int(rel["url"].split("/")[-1])
234
+ try:
235
+ tc = requests.get(
236
+ rel["url"] + f"?api-version={_API}",
237
+ headers=get_auth_headers(),
238
+ timeout=30,
239
+ )
240
+ if tc.ok:
241
+ f = tc.json().get("fields", {})
242
+ linked.append({
243
+ "id": tc_id,
244
+ "title": f.get("System.Title", ""),
245
+ "state": f.get("System.State", ""),
246
+ "priority": f.get("Microsoft.VSTS.Common.Priority", ""),
247
+ })
248
+ except Exception:
249
+ linked.append({"id": tc_id})
250
+
251
+ return {"work_item_id": work_item_id, "linked_test_cases": linked, "count": len(linked)}
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Helpers
256
+ # ---------------------------------------------------------------------------
257
+
258
+ def _build_steps_xml(steps: List[Dict]) -> str:
259
+ if not steps:
260
+ return ""
261
+
262
+ def esc(s: str) -> str:
263
+ return (s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
264
+
265
+ inner = "".join(
266
+ f'<step id="{i+1}" type="ValidateStep">'
267
+ f'<parameterizedString isformatted="false">{esc(s.get("action",""))}</parameterizedString>'
268
+ f'<parameterizedString isformatted="false">{esc(s.get("expected_result",""))}</parameterizedString>'
269
+ f"<description/></step>"
270
+ for i, s in enumerate(steps)
271
+ )
272
+ return f'<steps id="0" last="{len(steps)}">{inner}</steps>'
273
+
274
+
275
+ def _strip_html(html: str) -> str:
276
+ if not html:
277
+ return ""
278
+ import re
279
+ text = re.sub(r"<br\s*/?>", "\n", html, flags=re.IGNORECASE)
280
+ text = re.sub(r"</p>|</li>|</tr>", "\n", text, flags=re.IGNORECASE)
281
+ text = re.sub(r"<li[^>]*>", "• ", text, flags=re.IGNORECASE)
282
+ text = re.sub(r"<[^>]+>", "", text)
283
+ for old, new in [("&nbsp;", " "), ("&amp;", "&"), ("&lt;", "<"), ("&gt;", ">"), ("&quot;", '"')]:
284
+ text = text.replace(old, new)
285
+ return re.sub(r"\n{3,}", "\n\n", text).strip()
286
+
287
+
288
+ def _extract_coverage_hints(wi: dict) -> list:
289
+ text = " ".join([wi.get("description", ""), wi.get("acceptance_criteria", "")]).lower()
290
+ checks = [
291
+ (["toast", "notification", "success message", "confirmation"], "toast_notifications"),
292
+ (["mb", "kb", "size limit", "file size", "maximum size"], "file_size_boundary"),
293
+ (["display", "shows", "reflects", "after upload", "after save", "updated"], "post_action_state"),
294
+ (["mobile", "webview", "ios", "android"], "platform_specific"),
295
+ (["initials", "first name", "last name", "derived", "generated"], "computed_values"),
296
+ (["refresh", "reload", "re-login", "persist", "retained"], "data_persistence"),
297
+ (["tab", "keyboard", "accessible", "aria", "wcag", "a11y"], "accessibility"),
298
+ (["crop", "resize", "zoom", "drag", "rotate"], "image_manipulation"),
299
+ (["format", "png", "jpg", "jpeg", "webp", "svg"], "file_formats"),
300
+ (["cancel", "close", "discard", "dismiss"], "cancel_flows"),
301
+ ]
302
+ return [hint for keywords, hint in checks if any(kw in text for kw in keywords)]
File without changes
@@ -0,0 +1,119 @@
1
+ """
2
+ auth.py — Authentication for the QA Test Case Manager MCP Agent.
3
+
4
+ Uses MSAL with a persistent file-based token cache — identical approach to
5
+ the official microsoft/azure-devops-mcp server and the ado-qa-mcp-agent.
6
+
7
+ Flow:
8
+ 1. Check ~/.msal-cache/msal-cache.json for a cached token.
9
+ If the browser is already signed into a Microsoft account (via VS Code,
10
+ az login, or a previous run), the token is loaded silently — no browser
11
+ prompt appears.
12
+ 2. Only if no valid cached token exists: open browser once for interactive
13
+ login. Token is written to cache. Browser never opens again until the
14
+ refresh token expires (~90 days).
15
+
16
+ The cache path is shared with microsoft/azure-devops-mcp and ado-qa-mcp-agent,
17
+ so signing in via any of those tools means this agent is already authenticated.
18
+
19
+ Optional env vars:
20
+ AZURE_TENANT_ID Pin to a specific Entra tenant (default: "organizations")
21
+ AZURE_CLIENT_ID Custom app registration (default: Azure CLI client ID)
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from pathlib import Path
27
+ from typing import Dict
28
+
29
+ # ADO OAuth scope — same as microsoft/azure-devops-mcp
30
+ _ADO_SCOPE = "499b84ac-1321-427f-aa17-267ca6975798/.default"
31
+
32
+ # Azure CLI public client ID — works without a custom app registration
33
+ _DEFAULT_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
34
+
35
+ # Shared cache location with microsoft/azure-devops-mcp
36
+ _CACHE_FILE = Path.home() / ".msal-cache" / "msal-cache.json"
37
+
38
+ _msal_app = None
39
+
40
+
41
+ def get_auth_headers(content_type: str = "application/json") -> Dict[str, str]:
42
+ """Return HTTP headers with a valid ADO Bearer token."""
43
+ token = _acquire_token()
44
+ return {
45
+ "Authorization": f"Bearer {token}",
46
+ "Content-Type": content_type,
47
+ "Accept": "application/json",
48
+ }
49
+
50
+
51
+ def get_signed_in_user() -> str:
52
+ """Return the display name / UPN of the cached account, or empty string."""
53
+ try:
54
+ app, _ = _get_msal_app()
55
+ accounts = app.get_accounts()
56
+ if accounts:
57
+ return accounts[0].get("name") or accounts[0].get("username") or ""
58
+ except Exception:
59
+ pass
60
+ return ""
61
+
62
+
63
+ def _get_msal_app():
64
+ global _msal_app
65
+ if _msal_app is None:
66
+ _msal_app = _create_msal_app()
67
+ return _msal_app
68
+
69
+
70
+ def _create_msal_app():
71
+ import msal # type: ignore
72
+
73
+ client_id = os.getenv("AZURE_CLIENT_ID", _DEFAULT_CLIENT_ID)
74
+ tenant_id = os.getenv("AZURE_TENANT_ID", "organizations")
75
+ authority = f"https://login.microsoftonline.com/{tenant_id}"
76
+
77
+ cache = msal.SerializableTokenCache()
78
+ _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
79
+ if _CACHE_FILE.exists():
80
+ cache.deserialize(_CACHE_FILE.read_text(encoding="utf-8"))
81
+
82
+ app = msal.PublicClientApplication(
83
+ client_id=client_id,
84
+ authority=authority,
85
+ token_cache=cache,
86
+ )
87
+ return app, cache
88
+
89
+
90
+ def _acquire_token() -> str:
91
+ """
92
+ 1. Try silent acquisition from the shared cache.
93
+ 2. Fall back to acquire_token_interactive (opens browser once).
94
+ """
95
+ app, cache = _get_msal_app()
96
+ scopes = [_ADO_SCOPE]
97
+
98
+ accounts = app.get_accounts()
99
+ if accounts:
100
+ result = app.acquire_token_silent(scopes, account=accounts[0])
101
+ if result and "access_token" in result:
102
+ _persist_cache(cache)
103
+ return result["access_token"]
104
+
105
+ # No cached token — open browser
106
+ result = app.acquire_token_interactive(scopes=scopes)
107
+
108
+ if "access_token" not in result:
109
+ err = result.get("error_description") or result.get("error") or str(result)
110
+ raise RuntimeError(f"MSAL authentication failed: {err}")
111
+
112
+ _persist_cache(cache)
113
+ return result["access_token"]
114
+
115
+
116
+ def _persist_cache(cache) -> None:
117
+ if cache.has_state_changed:
118
+ _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
119
+ _CACHE_FILE.write_text(cache.serialize(), encoding="utf-8")