@qa-gentic/stlc-agents 1.0.5 → 1.0.7
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 +175 -34
- package/bin/postinstall.js +125 -44
- package/bin/qa-stlc.js +15 -8
- package/package.json +2 -2
- package/skills/{qa-stlc/AGENT-BEHAVIOR.md → AGENT-BEHAVIOR.md} +7 -6
- package/{.github/copilot-instructions/deduplication-protocol.md → skills/deduplication-protocol/SKILL.md} +16 -21
- package/skills/generate-gherkin/SKILL.md +287 -0
- package/skills/generate-gherkin/references/step-by-step.md +267 -0
- package/skills/{qa-stlc/generate-playwright-code.md → generate-playwright-code/SKILL.md} +13 -23
- package/{.github/copilot-instructions/generate-test-cases.md → skills/generate-test-cases/SKILL.md} +16 -2
- package/skills/qa-jira-manager/SKILL.md +287 -0
- package/{.github/copilot-instructions/write-helix-files.md → skills/write-helix-files/SKILL.md} +11 -17
- package/src/{boilerplate-bundle.js → cli/boilerplate-bundle.js} +8 -8
- package/src/cli/cmd-init.js +145 -0
- package/src/{cmd-mcp-config.js → cli/cmd-mcp-config.js} +72 -9
- package/src/cli/cmd-skills.js +209 -0
- package/src/{cmd-verify.js → cli/cmd-verify.js} +35 -3
- package/src/cli/prompt-integration.js +87 -0
- package/src/stlc_agents/agent_helix_writer/tools/boilerplate.py +8 -8
- package/src/stlc_agents/agent_jira_manager/__init__.py +0 -0
- package/src/stlc_agents/agent_jira_manager/server.py +500 -0
- package/src/stlc_agents/agent_jira_manager/tools/__init__.py +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +467 -0
- package/src/stlc_agents/shared_jira/__init__.py +0 -0
- package/src/stlc_agents/shared_jira/auth.py +270 -0
- package/.github/copilot-instructions/AGENT-BEHAVIOR.md +0 -448
- package/.github/copilot-instructions/generate-gherkin.md +0 -550
- package/.github/copilot-instructions/generate-playwright-code.md +0 -464
- package/skills/qa-stlc/deduplication-protocol.md +0 -303
- package/skills/qa-stlc/generate-gherkin.md +0 -550
- package/skills/qa-stlc/generate-test-cases.md +0 -176
- package/skills/qa-stlc/write-helix-files.md +0 -374
- package/src/cmd-init.js +0 -92
- package/src/cmd-skills.js +0 -124
- /package/src/{cmd-scaffold.js → cli/cmd-scaffold.js} +0 -0
|
File without changes
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent 5: QA Jira Manager — MCP Server
|
|
3
|
+
|
|
4
|
+
Tools:
|
|
5
|
+
fetch_jira_issue — Fetch a Jira issue with acceptance criteria and coverage hints
|
|
6
|
+
fetch_jira_epic_hierarchy — Fetch an Epic with all child issues
|
|
7
|
+
create_and_link_test_cases — Create test case issues in Jira and link them to the source issue
|
|
8
|
+
get_linked_test_cases — Return test cases already linked to a Jira issue
|
|
9
|
+
|
|
10
|
+
Authentication:
|
|
11
|
+
Jira OAuth 2.0 (3LO). Silent from ~/.jira-cache/jira-token.json.
|
|
12
|
+
If no cached token exists, the browser opens once for interactive login.
|
|
13
|
+
Refresh tokens keep the session alive without re-prompting.
|
|
14
|
+
No credentials appear in the UI or in configuration files.
|
|
15
|
+
|
|
16
|
+
Required env vars (in .env):
|
|
17
|
+
JIRA_CLIENT_ID Atlassian OAuth 2.0 app client ID
|
|
18
|
+
JIRA_CLIENT_SECRET Atlassian OAuth 2.0 app client secret
|
|
19
|
+
JIRA_CLOUD_ID Your Atlassian cloud ID
|
|
20
|
+
|
|
21
|
+
Skills: see skills/qa-jira-manager.md
|
|
22
|
+
"""
|
|
23
|
+
import asyncio
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from dotenv import load_dotenv
|
|
28
|
+
from mcp.server import Server
|
|
29
|
+
from mcp.server.stdio import stdio_server
|
|
30
|
+
from mcp import types
|
|
31
|
+
|
|
32
|
+
from stlc_agents.shared_jira.auth import get_auth_headers, get_cloud_id, get_signed_in_user
|
|
33
|
+
from stlc_agents.agent_jira_manager.tools.jira_workitem import (
|
|
34
|
+
fetch_work_item as _fetch_work_item,
|
|
35
|
+
fetch_epic_hierarchy as _fetch_epic_hierarchy,
|
|
36
|
+
create_test_case as _create_test_case,
|
|
37
|
+
link_test_cases_to_issue as _link_test_cases,
|
|
38
|
+
get_linked_test_cases as _get_linked_test_cases,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
load_dotenv()
|
|
42
|
+
|
|
43
|
+
app = Server("qa-jira-manager")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Pre-output validation helpers
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def _validate_test_case_inputs(test_cases: list[dict]) -> dict:
|
|
51
|
+
"""Validate test case payloads before sending to Jira.
|
|
52
|
+
|
|
53
|
+
Checks:
|
|
54
|
+
1. Every test case has a non-empty summary.
|
|
55
|
+
2. Every test case has at least one step.
|
|
56
|
+
3. Every step has both action and expected_result fields.
|
|
57
|
+
4. No duplicate summaries within the batch.
|
|
58
|
+
5. Priority is one of the valid Jira values when specified.
|
|
59
|
+
|
|
60
|
+
Returns { valid: bool, errors: list[str], warnings: list[str] }.
|
|
61
|
+
"""
|
|
62
|
+
errors: list[str] = []
|
|
63
|
+
warnings: list[str] = []
|
|
64
|
+
seen_summaries: set[str] = set()
|
|
65
|
+
valid_priorities = {"Highest", "High", "Medium", "Low", "Lowest"}
|
|
66
|
+
|
|
67
|
+
for i, tc in enumerate(test_cases, start=1):
|
|
68
|
+
summary = (tc.get("summary") or tc.get("title") or "").strip()
|
|
69
|
+
if not summary:
|
|
70
|
+
errors.append(f"Test case #{i}: summary is empty or missing.")
|
|
71
|
+
else:
|
|
72
|
+
normalised = summary.lower()
|
|
73
|
+
if normalised in seen_summaries:
|
|
74
|
+
errors.append(f"Test case #{i}: duplicate summary '{summary}'.")
|
|
75
|
+
seen_summaries.add(normalised)
|
|
76
|
+
|
|
77
|
+
steps = tc.get("steps", [])
|
|
78
|
+
if not steps:
|
|
79
|
+
errors.append(f"Test case #{i} ('{summary}'): no steps defined.")
|
|
80
|
+
for j, step in enumerate(steps, start=1):
|
|
81
|
+
if not (step.get("action") or "").strip():
|
|
82
|
+
errors.append(f"Test case #{i} ('{summary}'), step #{j}: 'action' is empty.")
|
|
83
|
+
if not (step.get("expected_result") or "").strip():
|
|
84
|
+
errors.append(
|
|
85
|
+
f"Test case #{i} ('{summary}'), step #{j}: 'expected_result' is empty."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
priority = tc.get("priority", "Medium")
|
|
89
|
+
if priority and priority not in valid_priorities:
|
|
90
|
+
errors.append(
|
|
91
|
+
f"Test case #{i} ('{summary}'): priority '{priority}' is not valid. "
|
|
92
|
+
f"Use one of: {', '.join(sorted(valid_priorities))}."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if len(test_cases) > 25:
|
|
96
|
+
warnings.append(
|
|
97
|
+
f"Large batch: {len(test_cases)} test cases. Consider splitting "
|
|
98
|
+
"into smaller batches for easier review."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _validate_fetch_response(result: dict) -> dict:
|
|
105
|
+
errors: list[str] = []
|
|
106
|
+
warnings: list[str] = []
|
|
107
|
+
|
|
108
|
+
if "error" in result:
|
|
109
|
+
return {"valid": True, "errors": [], "warnings": []}
|
|
110
|
+
|
|
111
|
+
wi = result.get("work_item", {})
|
|
112
|
+
if not wi.get("summary"):
|
|
113
|
+
errors.append("Issue has no summary — Jira data may be incomplete.")
|
|
114
|
+
|
|
115
|
+
if not wi.get("acceptance_criteria") and not wi.get("description"):
|
|
116
|
+
warnings.append(
|
|
117
|
+
"Issue has no acceptance criteria or description. "
|
|
118
|
+
"Test case generation may produce low-quality results — "
|
|
119
|
+
"ask the user to add acceptance criteria in Jira."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if result.get("existing_test_cases_count", 0) > 0:
|
|
123
|
+
warnings.append(
|
|
124
|
+
f"Issue already has {result['existing_test_cases_count']} linked test case(s). "
|
|
125
|
+
"Use get_linked_test_cases to check for duplicates before creating new ones."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _validate_linked_test_cases_response(result: dict) -> dict:
|
|
132
|
+
errors: list[str] = []
|
|
133
|
+
warnings: list[str] = []
|
|
134
|
+
|
|
135
|
+
if "error" in result:
|
|
136
|
+
return {"valid": True, "errors": [], "warnings": []}
|
|
137
|
+
|
|
138
|
+
test_cases = result.get("test_cases", [])
|
|
139
|
+
if not test_cases:
|
|
140
|
+
warnings.append("No linked test cases found. This issue has no 'is tested by' links.")
|
|
141
|
+
return {"valid": True, "errors": [], "warnings": warnings}
|
|
142
|
+
|
|
143
|
+
for i, tc in enumerate(test_cases, start=1):
|
|
144
|
+
if not tc.get("key"):
|
|
145
|
+
errors.append(f"Test case #{i}: missing key.")
|
|
146
|
+
if not tc.get("summary"):
|
|
147
|
+
warnings.append(f"Test case #{i} (key={tc.get('key', '?')}): missing summary.")
|
|
148
|
+
|
|
149
|
+
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Tool definitions
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
@app.list_tools()
|
|
157
|
+
async def list_tools() -> list[types.Tool]:
|
|
158
|
+
return [
|
|
159
|
+
types.Tool(
|
|
160
|
+
name="fetch_jira_issue",
|
|
161
|
+
description=(
|
|
162
|
+
"Fetch a Jira issue (Story, Bug, Task, Sub-task) from Jira Cloud. "
|
|
163
|
+
"Returns issue fields (summary, description, acceptance criteria, "
|
|
164
|
+
"status, priority, assignee, labels, story points, fix versions, components), "
|
|
165
|
+
"parent issue, count of existing linked test cases, and coverage hints "
|
|
166
|
+
"derived from the issue text. Returns epic_use_hierarchy error for Epics."
|
|
167
|
+
),
|
|
168
|
+
inputSchema={
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"issue_key": {
|
|
172
|
+
"type": "string",
|
|
173
|
+
"description": "Jira issue key, e.g. 'PROJ-123'",
|
|
174
|
+
},
|
|
175
|
+
"cloud_id": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": (
|
|
178
|
+
"Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var."
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
"required": ["issue_key"],
|
|
183
|
+
},
|
|
184
|
+
),
|
|
185
|
+
types.Tool(
|
|
186
|
+
name="fetch_jira_epic_hierarchy",
|
|
187
|
+
description=(
|
|
188
|
+
"Fetch a Jira Epic with all child issues (Stories, Tasks, Bugs, Sub-tasks). "
|
|
189
|
+
"Use this when the caller passes an Epic key. "
|
|
190
|
+
"Returns the Epic fields and a children list with key, type, summary, "
|
|
191
|
+
"status, priority, and assignee for each child."
|
|
192
|
+
),
|
|
193
|
+
inputSchema={
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"epic_key": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Epic issue key, e.g. 'PROJ-10'",
|
|
199
|
+
},
|
|
200
|
+
"cloud_id": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
"required": ["epic_key"],
|
|
206
|
+
},
|
|
207
|
+
),
|
|
208
|
+
types.Tool(
|
|
209
|
+
name="create_and_link_test_cases",
|
|
210
|
+
description=(
|
|
211
|
+
"Create one or more test case issues in Jira and link them to the source issue "
|
|
212
|
+
"via an 'is tested by' / 'Test' link. "
|
|
213
|
+
"Steps are stored as a formatted ADF table in the issue description — "
|
|
214
|
+
"compatible with both Jira Software and Xray-free environments.\n\n"
|
|
215
|
+
"Priority values: Highest, High (default), Medium, Low, Lowest.\n\n"
|
|
216
|
+
"Epic confirmation gate:\n"
|
|
217
|
+
" When the target issue is an Epic, return confirmation_required=true "
|
|
218
|
+
"and do NOT create any test cases. Surface the message to the user and wait "
|
|
219
|
+
"for their reply. Once confirmed, retry with confirmed=true."
|
|
220
|
+
),
|
|
221
|
+
inputSchema={
|
|
222
|
+
"type": "object",
|
|
223
|
+
"properties": {
|
|
224
|
+
"issue_key": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "Jira issue key to link test cases to, e.g. 'PROJ-123'",
|
|
227
|
+
},
|
|
228
|
+
"project_key": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": (
|
|
231
|
+
"Jira project key where test cases will be created, e.g. 'PROJ'. "
|
|
232
|
+
"Defaults to the project of the source issue when omitted."
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
"cloud_id": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
238
|
+
},
|
|
239
|
+
"test_cases": {
|
|
240
|
+
"type": "array",
|
|
241
|
+
"description": "Test cases to create",
|
|
242
|
+
"items": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"properties": {
|
|
245
|
+
"summary": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "Test case title / summary",
|
|
248
|
+
},
|
|
249
|
+
"priority": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"description": "Highest|High|Medium|Low|Lowest (default: High)",
|
|
252
|
+
},
|
|
253
|
+
"labels": {
|
|
254
|
+
"type": "array",
|
|
255
|
+
"items": {"type": "string"},
|
|
256
|
+
"description": "Optional labels to apply to the test case issue",
|
|
257
|
+
},
|
|
258
|
+
"steps": {
|
|
259
|
+
"type": "array",
|
|
260
|
+
"items": {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"action": {"type": "string"},
|
|
264
|
+
"expected_result": {"type": "string"},
|
|
265
|
+
},
|
|
266
|
+
"required": ["action", "expected_result"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
"required": ["summary", "steps"],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
"confirmed": {
|
|
274
|
+
"type": "boolean",
|
|
275
|
+
"description": (
|
|
276
|
+
"Set to true only when retrying after the user has explicitly confirmed "
|
|
277
|
+
"an Epic-scoped test case upload. Omit (or false) on the first call."
|
|
278
|
+
),
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
"required": ["issue_key", "test_cases"],
|
|
282
|
+
},
|
|
283
|
+
),
|
|
284
|
+
types.Tool(
|
|
285
|
+
name="get_linked_test_cases",
|
|
286
|
+
description=(
|
|
287
|
+
"Return all test case issues already linked to a Jira issue via "
|
|
288
|
+
"'is tested by' / 'Test' link type. "
|
|
289
|
+
"Use this before creating new test cases to avoid duplicates."
|
|
290
|
+
),
|
|
291
|
+
inputSchema={
|
|
292
|
+
"type": "object",
|
|
293
|
+
"properties": {
|
|
294
|
+
"issue_key": {"type": "string"},
|
|
295
|
+
"cloud_id": {
|
|
296
|
+
"type": "string",
|
|
297
|
+
"description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
"required": ["issue_key"],
|
|
301
|
+
},
|
|
302
|
+
),
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Tool dispatcher
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
@app.call_tool()
|
|
311
|
+
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
312
|
+
try:
|
|
313
|
+
cloud_id = (arguments.get("cloud_id") or "").strip() or get_cloud_id()
|
|
314
|
+
|
|
315
|
+
if name == "fetch_jira_issue":
|
|
316
|
+
result = await asyncio.to_thread(
|
|
317
|
+
_fetch_work_item,
|
|
318
|
+
cloud_id,
|
|
319
|
+
arguments["issue_key"],
|
|
320
|
+
)
|
|
321
|
+
result["_validation"] = _validate_fetch_response(result)
|
|
322
|
+
|
|
323
|
+
elif name == "fetch_jira_epic_hierarchy":
|
|
324
|
+
result = await asyncio.to_thread(
|
|
325
|
+
_fetch_epic_hierarchy,
|
|
326
|
+
cloud_id,
|
|
327
|
+
arguments["epic_key"],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
elif name == "create_and_link_test_cases":
|
|
331
|
+
issue_key = arguments["issue_key"]
|
|
332
|
+
test_cases = arguments["test_cases"]
|
|
333
|
+
|
|
334
|
+
# Normalise: accept either 'summary' or 'title' on input
|
|
335
|
+
for tc in test_cases:
|
|
336
|
+
if "title" in tc and "summary" not in tc:
|
|
337
|
+
tc["summary"] = tc.pop("title")
|
|
338
|
+
|
|
339
|
+
# ── Input validation — reject before hitting Jira ─────────────
|
|
340
|
+
input_validation = _validate_test_case_inputs(test_cases)
|
|
341
|
+
if not input_validation["valid"]:
|
|
342
|
+
result = {
|
|
343
|
+
"error": "input_validation_failed",
|
|
344
|
+
"validation": input_validation,
|
|
345
|
+
"message": (
|
|
346
|
+
"Test case inputs failed validation. Fix the errors "
|
|
347
|
+
"below and retry. No test cases were created in Jira."
|
|
348
|
+
),
|
|
349
|
+
}
|
|
350
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
|
|
351
|
+
|
|
352
|
+
# ── Peek at the issue to get type and project_key ─────────────
|
|
353
|
+
issue_data: dict = {}
|
|
354
|
+
try:
|
|
355
|
+
peek = await asyncio.to_thread(_fetch_work_item, cloud_id, issue_key)
|
|
356
|
+
if not peek.get("error"):
|
|
357
|
+
issue_data = peek
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
issue_type = (issue_data.get("work_item") or {}).get("type", "")
|
|
362
|
+
project_key = (
|
|
363
|
+
(arguments.get("project_key") or "").strip()
|
|
364
|
+
or (issue_data.get("work_item") or {}).get("project_key", "")
|
|
365
|
+
)
|
|
366
|
+
epic_key_for_link = (issue_data.get("work_item") or {}).get("epic_key", "")
|
|
367
|
+
|
|
368
|
+
# ── Epic confirmation gate ────────────────────────────────────
|
|
369
|
+
if issue_type == "Epic" and not arguments.get("confirmed", False):
|
|
370
|
+
issue_summary = (issue_data.get("work_item") or {}).get("summary", issue_key)
|
|
371
|
+
result = {
|
|
372
|
+
"confirmation_required": True,
|
|
373
|
+
"issue_key": issue_key,
|
|
374
|
+
"issue_type": "Epic",
|
|
375
|
+
"issue_summary": issue_summary,
|
|
376
|
+
"proposed_test_case_count": len(test_cases),
|
|
377
|
+
"retry_instructions": (
|
|
378
|
+
"Surface this message to the user. "
|
|
379
|
+
"If they confirm, retry this tool call with the IDENTICAL "
|
|
380
|
+
"arguments and add confirmed=true. "
|
|
381
|
+
"Do NOT change issue_key or any other parameter. "
|
|
382
|
+
"If they decline, abort."
|
|
383
|
+
),
|
|
384
|
+
"message": (
|
|
385
|
+
f"Issue {issue_key} is an Epic: \"{issue_summary}\". "
|
|
386
|
+
f"You are about to create and upload {len(test_cases)} test "
|
|
387
|
+
f"case(s) linked to this Epic in Jira. "
|
|
388
|
+
"Please confirm before proceeding. "
|
|
389
|
+
"Reply 'yes' or 'confirm' to proceed, or 'no' / 'cancel' to abort."
|
|
390
|
+
),
|
|
391
|
+
}
|
|
392
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
|
|
393
|
+
|
|
394
|
+
if not project_key:
|
|
395
|
+
result = {
|
|
396
|
+
"error": "project_key_required",
|
|
397
|
+
"message": (
|
|
398
|
+
"Could not determine the Jira project key. "
|
|
399
|
+
"Pass project_key explicitly, e.g. 'PROJ'."
|
|
400
|
+
),
|
|
401
|
+
}
|
|
402
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
|
|
403
|
+
|
|
404
|
+
# ── Create test cases ─────────────────────────────────────────
|
|
405
|
+
created = []
|
|
406
|
+
failed = []
|
|
407
|
+
for tc in test_cases:
|
|
408
|
+
try:
|
|
409
|
+
r = await asyncio.to_thread(
|
|
410
|
+
_create_test_case,
|
|
411
|
+
cloud_id,
|
|
412
|
+
project_key,
|
|
413
|
+
tc["summary"],
|
|
414
|
+
tc.get("steps", []),
|
|
415
|
+
tc.get("priority", "High"),
|
|
416
|
+
tc.get("labels"),
|
|
417
|
+
epic_key_for_link or None,
|
|
418
|
+
)
|
|
419
|
+
created.append(r)
|
|
420
|
+
except Exception as e:
|
|
421
|
+
failed.append({"summary": tc["summary"], "error": str(e)})
|
|
422
|
+
|
|
423
|
+
link_result: dict = {}
|
|
424
|
+
if created:
|
|
425
|
+
tc_keys = [r["issue_key"] for r in created]
|
|
426
|
+
try:
|
|
427
|
+
link_result = await asyncio.to_thread(
|
|
428
|
+
_link_test_cases, cloud_id, issue_key, tc_keys
|
|
429
|
+
)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
link_result = {"error": str(e)}
|
|
432
|
+
|
|
433
|
+
result = {
|
|
434
|
+
"summary": {
|
|
435
|
+
"requested": len(test_cases),
|
|
436
|
+
"created": len(created),
|
|
437
|
+
"failed": len(failed),
|
|
438
|
+
"linked_to_issue": issue_key,
|
|
439
|
+
},
|
|
440
|
+
"created_test_cases": created,
|
|
441
|
+
"failed": failed,
|
|
442
|
+
"link_result": link_result,
|
|
443
|
+
"_validation": {
|
|
444
|
+
"valid": len(failed) == 0 and link_result.get("success", False),
|
|
445
|
+
"input_validation": input_validation,
|
|
446
|
+
"errors": [f["error"] for f in failed] if failed else [],
|
|
447
|
+
"warnings": input_validation.get("warnings", []),
|
|
448
|
+
"post_creation_check": {
|
|
449
|
+
"all_created": len(created) == len(test_cases),
|
|
450
|
+
"all_linked": link_result.get("success", False),
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
elif name == "get_linked_test_cases":
|
|
456
|
+
result = await asyncio.to_thread(
|
|
457
|
+
_get_linked_test_cases,
|
|
458
|
+
cloud_id,
|
|
459
|
+
arguments["issue_key"],
|
|
460
|
+
)
|
|
461
|
+
result["_validation"] = _validate_linked_test_cases_response(result)
|
|
462
|
+
|
|
463
|
+
else:
|
|
464
|
+
result = {"error": f"Unknown tool: {name}"}
|
|
465
|
+
|
|
466
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
|
|
467
|
+
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
return [types.TextContent(
|
|
470
|
+
type="text",
|
|
471
|
+
text=json.dumps({"error": str(exc), "tool": name}, indent=2),
|
|
472
|
+
)]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
# Entry point
|
|
477
|
+
# ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
async def _run():
|
|
480
|
+
async with stdio_server() as (r, w):
|
|
481
|
+
await app.run(r, w, app.create_initialization_options())
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def main():
|
|
485
|
+
# Validate auth on startup — triggers interactive login if needed
|
|
486
|
+
try:
|
|
487
|
+
get_auth_headers()
|
|
488
|
+
except Exception as e:
|
|
489
|
+
print(f"[qa-jira-manager] Auth error: {e}", file=sys.stderr)
|
|
490
|
+
sys.exit(1)
|
|
491
|
+
|
|
492
|
+
user = get_signed_in_user()
|
|
493
|
+
if user:
|
|
494
|
+
print(f"[qa-jira-manager] Authenticated as: {user}", file=sys.stderr)
|
|
495
|
+
|
|
496
|
+
asyncio.run(_run())
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
if __name__ == "__main__":
|
|
500
|
+
main()
|
|
File without changes
|