@qa-gentic/stlc-agents 1.0.23 → 1.0.26

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 (46) hide show
  1. package/package.json +1 -1
  2. package/skills/generate-test-cases/SKILL.md +5 -0
  3. package/src/cli/cmd-cost.js +61 -30
  4. package/src/cli/cmd-init.js +88 -8
  5. package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  7. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
  8. package/src/stlc_agents/agent_gherkin_generator/server.py +8 -7
  9. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  10. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
  11. package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
  12. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
  13. package/src/stlc_agents/agent_helix_writer/server.py +48 -12
  14. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  15. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
  16. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
  17. package/src/stlc_agents/agent_jira_manager/server.py +9 -8
  18. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  19. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
  20. package/src/stlc_agents/agent_playwright_generator/server.py +419 -213
  21. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  22. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
  23. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
  24. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
  25. package/src/stlc_agents/agent_test_case_manager/server.py +21 -8
  26. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  27. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
  28. package/src/stlc_agents/agent_test_case_manager/tools/ado_workitem.py +65 -1
  29. package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
  30. package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
  31. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
  32. package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
  33. package/src/stlc_agents/shared/cost_tracker.py +378 -70
  34. package/src/stlc_agents/shared/pricing.py +115 -24
  35. package/src/stlc_agents/webhook_orchestrator/__init__.py +0 -0
  36. package/src/stlc_agents/webhook_orchestrator/agent_runner.py +599 -0
  37. package/src/stlc_agents/webhook_orchestrator/main.py +43 -0
  38. package/src/stlc_agents/webhook_orchestrator/models.py +63 -0
  39. package/src/stlc_agents/webhook_orchestrator/orchestrator.py +103 -0
  40. package/src/stlc_agents/webhook_orchestrator/pipelines/__init__.py +0 -0
  41. package/src/stlc_agents/webhook_orchestrator/pipelines/_base.py +57 -0
  42. package/src/stlc_agents/webhook_orchestrator/pipelines/ado_test_cases.py +55 -0
  43. package/src/stlc_agents/webhook_orchestrator/pipelines/full_pipeline.py +202 -0
  44. package/src/stlc_agents/webhook_orchestrator/pipelines/gherkin_playwright.py +156 -0
  45. package/src/stlc_agents/webhook_orchestrator/pipelines/jira_test_cases.py +48 -0
  46. package/src/stlc_agents/webhook_orchestrator/webhook_bridge.py +368 -0
@@ -17,6 +17,7 @@ import asyncio
17
17
  import json
18
18
  import os
19
19
  import sys
20
+ import time
20
21
 
21
22
  from dotenv import load_dotenv
22
23
  from mcp.server import Server
@@ -29,7 +30,9 @@ from stlc_agents.agent_test_case_manager.tools.ado_workitem import (
29
30
  create_test_case as _create_test_case,
30
31
  link_test_cases_to_work_item as _link_test_cases,
31
32
  get_linked_test_cases as _get_linked_test_cases,
33
+ add_tag_to_work_item as _add_tag,
32
34
  )
35
+ from stlc_agents.shared.cost_tracker import track
33
36
 
34
37
  load_dotenv()
35
38
 
@@ -351,6 +354,7 @@ async def list_tools() -> list[types.Tool]:
351
354
 
352
355
  @app.call_tool()
353
356
  async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
357
+ t0 = time.monotonic()
354
358
  try:
355
359
  if name == "fetch_work_item":
356
360
  result = await asyncio.to_thread(
@@ -380,7 +384,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
380
384
  "below and retry. No test cases were created in ADO."
381
385
  ),
382
386
  }
383
- return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
387
+ return track(result, tool_name=name, server="qa-test-case-manager", t0=t0)
384
388
 
385
389
  # ── Epic guard — always check work item type before creating TCs ─
386
390
  try:
@@ -399,7 +403,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
399
403
  "Feature or PBI/Bug ID instead."
400
404
  ),
401
405
  }
402
- return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
406
+ return track(result, tool_name=name, server="qa-test-case-manager", t0=t0)
403
407
  except Exception:
404
408
  pass # if the peek fails, continue and let the real call surface the error
405
409
 
@@ -448,7 +452,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
448
452
  "Reply 'yes' or 'confirm' to proceed, or 'no' / 'cancel' to abort."
449
453
  ),
450
454
  }
451
- return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
455
+ return track(result, tool_name=name, server="qa-test-case-manager", t0=t0)
452
456
  except Exception:
453
457
  pass # wi_data may not be set if the earlier peek failed; continue normally
454
458
 
@@ -480,6 +484,16 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
480
484
  except Exception as e:
481
485
  link_result = {"error": str(e)}
482
486
 
487
+ # Add tag to the parent work item after linking
488
+ tag_result = {}
489
+ if created:
490
+ try:
491
+ tag_result = await asyncio.to_thread(
492
+ _add_tag, org, project, wi_id, "STLCAgentTestCases"
493
+ )
494
+ except Exception as e:
495
+ tag_result = {"error": str(e)}
496
+
483
497
  result = {
484
498
  "summary": {
485
499
  "requested": len(test_cases),
@@ -490,6 +504,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
490
504
  "created_test_cases": created,
491
505
  "failed": failed,
492
506
  "link_result": link_result,
507
+ "tag_result": tag_result,
493
508
  "_validation": {
494
509
  "valid": len(failed) == 0 and bool(link_result.get("success", True)),
495
510
  "input_validation": input_validation,
@@ -565,13 +580,11 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
565
580
  else:
566
581
  result = {"error": f"Unknown tool: {name}"}
567
582
 
568
- return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
583
+ return track(result, tool_name=name, server="qa-test-case-manager", t0=t0)
569
584
 
570
585
  except Exception as exc:
571
- return [types.TextContent(
572
- type="text",
573
- text=json.dumps({"error": str(exc), "tool": name}, indent=2),
574
- )]
586
+ err_result = {"error": str(exc), "tool": name}
587
+ return track(err_result, tool_name=name, server="qa-test-case-manager", t0=t0)
575
588
 
576
589
 
577
590
  # ---------------------------------------------------------------------------
@@ -8,6 +8,8 @@ Public API:
8
8
  create_test_case(org_url, project, title, steps, ...) -> dict
9
9
  link_test_cases_to_work_item(org_url, project, wi_id, tc_ids) -> dict
10
10
  get_linked_test_cases(org_url, project, work_item_id) -> dict
11
+ add_comment_to_work_item(org_url, project, work_item_id, text) -> dict
12
+ add_tag_to_work_item(org_url, project, work_item_id, tag) -> dict
11
13
  """
12
14
  from __future__ import annotations
13
15
 
@@ -175,8 +177,13 @@ def link_test_cases_to_work_item(
175
177
  project: str,
176
178
  work_item_id: int,
177
179
  test_case_ids: List[int],
180
+ link_comment: str = "STLC-Agent generated test case",
178
181
  ) -> dict:
179
- """Create TestedBy-Forward links from a work item to test cases."""
182
+ """Create TestedBy-Forward links from a work item to test cases.
183
+
184
+ link_comment is stored as attributes.comment on each relation and appears
185
+ in the Links tab Comments column in Azure DevOps.
186
+ """
180
187
  org_url = org_url.rstrip("/")
181
188
  headers = get_auth_headers("application/json-patch+json")
182
189
 
@@ -187,6 +194,7 @@ def link_test_cases_to_work_item(
187
194
  "value": {
188
195
  "rel": "Microsoft.VSTS.Common.TestedBy-Forward",
189
196
  "url": f"{org_url}/{project}/_apis/wit/workItems/{tc_id}",
197
+ "attributes": {"comment": link_comment},
190
198
  },
191
199
  }
192
200
  for tc_id in test_case_ids
@@ -249,6 +257,62 @@ def get_linked_test_cases(org_url: str, project: str, work_item_id: int) -> dict
249
257
  return {"work_item_id": work_item_id, "linked_test_cases": linked, "count": len(linked)}
250
258
 
251
259
 
260
+ # ---------------------------------------------------------------------------
261
+ # add_comment_to_work_item
262
+ # ---------------------------------------------------------------------------
263
+
264
+ def add_comment_to_work_item(org_url: str, project: str, work_item_id: int, text: str) -> dict:
265
+ """Add a comment to a work item via the ADO comments API."""
266
+ org_url = org_url.rstrip("/")
267
+ headers = get_auth_headers()
268
+
269
+ resp = requests.post(
270
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}/comments",
271
+ headers=headers,
272
+ params={"api-version": "7.1-preview.3"},
273
+ json={"text": text},
274
+ timeout=30,
275
+ )
276
+ resp.raise_for_status()
277
+ return {"success": True, "comment_id": resp.json().get("id")}
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # add_tag_to_work_item
282
+ # ---------------------------------------------------------------------------
283
+
284
+ def add_tag_to_work_item(org_url: str, project: str, work_item_id: int, tag: str) -> dict:
285
+ """Append a tag to a work item's System.Tags field (no-op if already present)."""
286
+ org_url = org_url.rstrip("/")
287
+
288
+ fetch_resp = requests.get(
289
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
290
+ headers=get_auth_headers(),
291
+ params={"api-version": _API},
292
+ timeout=30,
293
+ )
294
+ fetch_resp.raise_for_status()
295
+ existing_str = fetch_resp.json().get("fields", {}).get("System.Tags", "") or ""
296
+ existing = [t.strip() for t in existing_str.split(";") if t.strip()]
297
+
298
+ if tag in existing:
299
+ return {"success": True, "tag": tag, "already_present": True}
300
+
301
+ existing.append(tag)
302
+ new_tags_str = "; ".join(existing)
303
+
304
+ patch = [{"op": "add", "path": "/fields/System.Tags", "value": new_tags_str}]
305
+ patch_resp = requests.patch(
306
+ f"{org_url}/{project}/_apis/wit/workitems/{work_item_id}",
307
+ headers=get_auth_headers("application/json-patch+json"),
308
+ params={"api-version": _API},
309
+ json=patch,
310
+ timeout=30,
311
+ )
312
+ patch_resp.raise_for_status()
313
+ return {"success": True, "tag": tag, "tags": new_tags_str}
314
+
315
+
252
316
  # ---------------------------------------------------------------------------
253
317
  # Helpers
254
318
  # ---------------------------------------------------------------------------