@qa-gentic/stlc-agents 1.0.16 → 1.0.18
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/ORCHESTRATION_RULES.md +283 -0
- package/README.md +246 -321
- package/bin/postinstall.js +26 -2
- package/bin/qa-stlc.js +23 -0
- package/package.json +15 -2
- package/skills/write-helix-files/SKILL.md +6 -0
- package/src/cli/cmd-cost.js +253 -0
- package/src/cli/cmd-init.js +19 -2
- package/src/cli/cmd-mcp-config.js +123 -62
- package/src/cli/cmd-skills.js +21 -4
- package/src/stlc_agents/agent_gherkin_generator/server.py +88 -4
- package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +60 -28
- package/src/stlc_agents/agent_jira_manager/server.py +209 -2
- package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +36 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +968 -105
- package/src/stlc_agents/agent_test_case_manager/server.py +121 -2
- package/src/stlc_agents/shared/cost_tracker.py +395 -0
- package/src/stlc_agents/shared/install_hook.py +154 -0
- package/src/stlc_agents/shared/pricing.py +72 -0
- package/src/stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-310.pyc +0 -0
|
@@ -34,6 +34,7 @@ from stlc_agents.agent_jira_manager.tools.jira_workitem import (
|
|
|
34
34
|
create_test_case as _create_test_case,
|
|
35
35
|
link_test_cases_to_issue as _link_test_cases,
|
|
36
36
|
get_linked_test_cases as _get_linked_test_cases,
|
|
37
|
+
attach_gherkin_to_issue as _attach_gherkin,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
load_dotenv()
|
|
@@ -148,9 +149,25 @@ def _validate_linked_test_cases_response(result: dict) -> dict:
|
|
|
148
149
|
|
|
149
150
|
|
|
150
151
|
# ---------------------------------------------------------------------------
|
|
151
|
-
#
|
|
152
|
+
# Deduplication helper
|
|
152
153
|
# ---------------------------------------------------------------------------
|
|
153
154
|
|
|
155
|
+
_TC_STOP_WORDS = frozenset({
|
|
156
|
+
"verify", "ensure", "validate", "check", "test", "the", "a", "an",
|
|
157
|
+
"that", "is", "are", "can", "user", "should", "able", "to", "with",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _normalise_summary(summary: str) -> str:
|
|
162
|
+
"""Lowercase, strip punctuation, remove stop words — for dedup comparison."""
|
|
163
|
+
import re
|
|
164
|
+
cleaned = re.sub(r"[^a-z0-9\s]", "", summary.lower())
|
|
165
|
+
tokens = [w for w in cleaned.split() if w not in _TC_STOP_WORDS]
|
|
166
|
+
return " ".join(tokens)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
154
171
|
@app.list_tools()
|
|
155
172
|
async def list_tools() -> list[types.Tool]:
|
|
156
173
|
return [
|
|
@@ -275,6 +292,116 @@ async def list_tools() -> list[types.Tool]:
|
|
|
275
292
|
"required": ["issue_key"],
|
|
276
293
|
},
|
|
277
294
|
),
|
|
295
|
+
types.Tool(
|
|
296
|
+
name="attach_gherkin_to_jira_issue",
|
|
297
|
+
description=(
|
|
298
|
+
"Upload a validated .feature file as an attachment on a Jira issue. "
|
|
299
|
+
"Use this after generate_gherkin + validate_gherkin_content to link the "
|
|
300
|
+
"Gherkin artefact back to the source Jira issue — mirrors the ADO "
|
|
301
|
+
"attach_gherkin_to_work_item / attach_gherkin_to_feature tools. "
|
|
302
|
+
"File is named {issue_key}_{summary_kebab}_regression.feature automatically. "
|
|
303
|
+
"Requires the Jira OAuth token to have 'write:jira-work' scope."
|
|
304
|
+
),
|
|
305
|
+
inputSchema={
|
|
306
|
+
"type": "object",
|
|
307
|
+
"properties": {
|
|
308
|
+
"issue_key": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"description": "Jira issue key, e.g. 'PROJ-123'",
|
|
311
|
+
},
|
|
312
|
+
"gherkin_content": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"description": "Complete validated .feature file content",
|
|
315
|
+
},
|
|
316
|
+
"cloud_id": {
|
|
317
|
+
"type": "string",
|
|
318
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
"required": ["issue_key", "gherkin_content"],
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
types.Tool(
|
|
325
|
+
name="create_deduped_test_cases", description=(
|
|
326
|
+
"Create test cases in Jira, skipping any whose summary already exists as a "
|
|
327
|
+
"linked test case on the issue. "
|
|
328
|
+
"Internally calls get_linked_test_cases, filters the incoming batch against "
|
|
329
|
+
"existing summaries (case-insensitive, stop-word-normalised), then calls "
|
|
330
|
+
"create_and_link_test_cases on the net-new subset only. "
|
|
331
|
+
"Use this instead of create_and_link_test_cases for webhook/headless runs "
|
|
332
|
+
"where re-triggers would otherwise produce duplicates. "
|
|
333
|
+
"Returns skipped_count, created_count, and the full create result."
|
|
334
|
+
),
|
|
335
|
+
inputSchema={
|
|
336
|
+
"type": "object",
|
|
337
|
+
"properties": {
|
|
338
|
+
"issue_key": {
|
|
339
|
+
"type": "string",
|
|
340
|
+
"description": "Jira issue key to link test cases to, e.g. 'PROJ-123'",
|
|
341
|
+
},
|
|
342
|
+
"cloud_id": {
|
|
343
|
+
"type": "string",
|
|
344
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
345
|
+
},
|
|
346
|
+
"test_cases": {
|
|
347
|
+
"type": "array",
|
|
348
|
+
"description": "Full proposed test case batch (duplicates will be filtered out)",
|
|
349
|
+
"items": {
|
|
350
|
+
"type": "object",
|
|
351
|
+
"properties": {
|
|
352
|
+
"summary": {"type": "string", "description": "Test case title/summary"},
|
|
353
|
+
"priority": {"type": "string", "description": "Highest|High|Medium|Low|Lowest"},
|
|
354
|
+
"steps": {
|
|
355
|
+
"type": "array",
|
|
356
|
+
"items": {
|
|
357
|
+
"type": "object",
|
|
358
|
+
"properties": {
|
|
359
|
+
"action": {"type": "string"},
|
|
360
|
+
"expected_result": {"type": "string"},
|
|
361
|
+
},
|
|
362
|
+
"required": ["action", "expected_result"],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
"required": ["summary", "steps"],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
"required": ["issue_key", "test_cases"],
|
|
371
|
+
},
|
|
372
|
+
),
|
|
373
|
+
types.Tool(
|
|
374
|
+
name="attach_gherkin_to_issue",
|
|
375
|
+
description=(
|
|
376
|
+
"Upload and attach a validated .feature file to a Jira issue as an attachment. "
|
|
377
|
+
"File is named {issue_key}_{summary_kebab}_regression.feature automatically. "
|
|
378
|
+
"Use this after validate_gherkin_content passes. "
|
|
379
|
+
"Called by the pipeline's Jira Gherkin path to link the .feature back to "
|
|
380
|
+
"the Jira issue instead of saving to disk only."
|
|
381
|
+
),
|
|
382
|
+
inputSchema={
|
|
383
|
+
"type": "object",
|
|
384
|
+
"properties": {
|
|
385
|
+
"issue_key": {
|
|
386
|
+
"type": "string",
|
|
387
|
+
"description": "Jira issue key, e.g. 'PROJ-123'",
|
|
388
|
+
},
|
|
389
|
+
"gherkin_content": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"description": "Validated .feature file content",
|
|
392
|
+
},
|
|
393
|
+
"issue_summary": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"description": "Issue summary used in the filename (optional)",
|
|
396
|
+
},
|
|
397
|
+
"cloud_id": {
|
|
398
|
+
"type": "string",
|
|
399
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
"required": ["issue_key", "gherkin_content"],
|
|
403
|
+
},
|
|
404
|
+
),
|
|
278
405
|
]
|
|
279
406
|
|
|
280
407
|
|
|
@@ -420,6 +547,26 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
420
547
|
},
|
|
421
548
|
}
|
|
422
549
|
|
|
550
|
+
elif name == "attach_gherkin_to_jira_issue":
|
|
551
|
+
issue_key = arguments["issue_key"]
|
|
552
|
+
gherkin_content = arguments["gherkin_content"]
|
|
553
|
+
|
|
554
|
+
# Fetch issue summary for filename (best-effort)
|
|
555
|
+
issue_summary = ""
|
|
556
|
+
try:
|
|
557
|
+
peek = await asyncio.to_thread(_fetch_work_item, cloud_id, issue_key)
|
|
558
|
+
issue_summary = (peek.get("work_item") or {}).get("summary", "")
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
result = await asyncio.to_thread(
|
|
563
|
+
_attach_gherkin,
|
|
564
|
+
cloud_id,
|
|
565
|
+
issue_key,
|
|
566
|
+
gherkin_content,
|
|
567
|
+
issue_summary,
|
|
568
|
+
)
|
|
569
|
+
|
|
423
570
|
elif name == "get_linked_test_cases":
|
|
424
571
|
result = await asyncio.to_thread(
|
|
425
572
|
_get_linked_test_cases,
|
|
@@ -428,6 +575,66 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
428
575
|
)
|
|
429
576
|
result["_validation"] = _validate_linked_test_cases_response(result)
|
|
430
577
|
|
|
578
|
+
elif name == "create_deduped_test_cases":
|
|
579
|
+
issue_key = arguments["issue_key"]
|
|
580
|
+
proposed = arguments["test_cases"]
|
|
581
|
+
|
|
582
|
+
# Normalise title/summary on input
|
|
583
|
+
for tc in proposed:
|
|
584
|
+
if "title" in tc and "summary" not in tc:
|
|
585
|
+
tc["summary"] = tc.pop("title")
|
|
586
|
+
|
|
587
|
+
# Step 1: fetch existing linked TCs
|
|
588
|
+
existing_result = await asyncio.to_thread(
|
|
589
|
+
_get_linked_test_cases, cloud_id, issue_key
|
|
590
|
+
)
|
|
591
|
+
existing_summaries: set[str] = {
|
|
592
|
+
_normalise_summary(tc.get("summary", ""))
|
|
593
|
+
for tc in existing_result.get("test_cases", [])
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# Step 2: filter — keep only net-new
|
|
597
|
+
net_new = [
|
|
598
|
+
tc for tc in proposed
|
|
599
|
+
if _normalise_summary(tc.get("summary", "")) not in existing_summaries
|
|
600
|
+
]
|
|
601
|
+
skipped = len(proposed) - len(net_new)
|
|
602
|
+
|
|
603
|
+
if not net_new:
|
|
604
|
+
result = {
|
|
605
|
+
"status": "all_duplicates",
|
|
606
|
+
"skipped_count": skipped,
|
|
607
|
+
"created_count": 0,
|
|
608
|
+
"message": (
|
|
609
|
+
f"All {skipped} proposed test case(s) already exist as linked "
|
|
610
|
+
"test cases on this issue. Nothing was created."
|
|
611
|
+
),
|
|
612
|
+
}
|
|
613
|
+
else:
|
|
614
|
+
# Step 3: delegate to create_and_link_test_cases
|
|
615
|
+
create_result_raw = await call_tool("create_and_link_test_cases", {
|
|
616
|
+
"issue_key": issue_key,
|
|
617
|
+
"cloud_id": cloud_id,
|
|
618
|
+
"test_cases": net_new,
|
|
619
|
+
"confirmed": True,
|
|
620
|
+
})
|
|
621
|
+
inner = json.loads(create_result_raw[0].text)
|
|
622
|
+
result = {
|
|
623
|
+
"status": "ok",
|
|
624
|
+
"skipped_count": skipped,
|
|
625
|
+
"created_count": len(net_new),
|
|
626
|
+
"create_result": inner,
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
elif name == "attach_gherkin_to_issue":
|
|
630
|
+
result = await asyncio.to_thread(
|
|
631
|
+
_attach_gherkin,
|
|
632
|
+
cloud_id,
|
|
633
|
+
arguments["issue_key"],
|
|
634
|
+
arguments["gherkin_content"],
|
|
635
|
+
arguments.get("issue_summary", ""),
|
|
636
|
+
)
|
|
637
|
+
|
|
431
638
|
else:
|
|
432
639
|
result = {"error": f"Unknown tool: {name}"}
|
|
433
640
|
|
|
@@ -465,4 +672,4 @@ def main():
|
|
|
465
672
|
|
|
466
673
|
|
|
467
674
|
if __name__ == "__main__":
|
|
468
|
-
main()
|
|
675
|
+
main()
|
|
@@ -8,6 +8,7 @@ Public API:
|
|
|
8
8
|
create_test_case(cloud_id, project_key, title, steps, priority, labels, epic_link) -> dict
|
|
9
9
|
link_test_cases_to_issue(cloud_id, issue_key, tc_keys) -> dict
|
|
10
10
|
get_linked_test_cases(cloud_id, issue_key) -> dict
|
|
11
|
+
attach_gherkin_to_issue(cloud_id, issue_key, gherkin_content, file_name) -> dict
|
|
11
12
|
|
|
12
13
|
Jira REST v3 base: https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3
|
|
13
14
|
"""
|
|
@@ -280,6 +281,41 @@ def get_linked_test_cases(cloud_id: str, issue_key: str) -> dict:
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# attach_gherkin_to_issue
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def attach_gherkin_to_issue(cloud_id: str, issue_key: str, gherkin_content: str, file_name: str = "feature.feature") -> dict:
|
|
289
|
+
"""Attach a .feature file to a Jira issue.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
cloud_id: Jira cloud ID
|
|
293
|
+
issue_key: Issue key (e.g. PROJ-123)
|
|
294
|
+
gherkin_content: Full Gherkin feature file content
|
|
295
|
+
file_name: Name of the file to attach (default: feature.feature)
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
dict with attachment metadata
|
|
299
|
+
"""
|
|
300
|
+
headers = get_auth_headers()
|
|
301
|
+
|
|
302
|
+
# Upload attachment
|
|
303
|
+
url = f"{_base(cloud_id)}/issue/{issue_key}/attachments"
|
|
304
|
+
files = {"file": (file_name, gherkin_content, "text/plain")}
|
|
305
|
+
|
|
306
|
+
resp = requests.post(url, headers=headers, files=files, timeout=30)
|
|
307
|
+
resp.raise_for_status()
|
|
308
|
+
data = resp.json()
|
|
309
|
+
|
|
310
|
+
attachment = (data.get("results") or [{}])[0] if data.get("results") else {}
|
|
311
|
+
return {
|
|
312
|
+
"issue_key": issue_key,
|
|
313
|
+
"file_name": file_name,
|
|
314
|
+
"attachment_id": attachment.get("id", ""),
|
|
315
|
+
"success": bool(attachment.get("id")),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
283
319
|
# ---------------------------------------------------------------------------
|
|
284
320
|
# Helpers
|
|
285
321
|
# ---------------------------------------------------------------------------
|