@qa-gentic/stlc-agents 1.0.25 → 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.
- package/package.json +1 -1
- package/skills/generate-test-cases/SKILL.md +5 -0
- package/src/cli/cmd-cost.js +61 -30
- package/src/cli/cmd-init.js +88 -8
- 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/server.py +41 -6
- 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/server.py +419 -213
- 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/server.py +12 -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/agent_test_case_manager/tools/ado_workitem.py +65 -1
- 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
- package/src/stlc_agents/shared/cost_tracker.py +378 -70
- package/src/stlc_agents/shared/pricing.py +115 -24
- package/src/stlc_agents/webhook_orchestrator/__init__.py +0 -0
- package/src/stlc_agents/webhook_orchestrator/agent_runner.py +599 -0
- package/src/stlc_agents/webhook_orchestrator/main.py +43 -0
- package/src/stlc_agents/webhook_orchestrator/models.py +63 -0
- package/src/stlc_agents/webhook_orchestrator/orchestrator.py +103 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/__init__.py +0 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/_base.py +57 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/ado_test_cases.py +55 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/full_pipeline.py +202 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/gherkin_playwright.py +156 -0
- package/src/stlc_agents/webhook_orchestrator/pipelines/jira_test_cases.py +48 -0
- package/src/stlc_agents/webhook_orchestrator/webhook_bridge.py +368 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-310.pyc +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
models.py — Shared data models for the webhook orchestrator.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import enum
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Platform(str, enum.Enum):
|
|
12
|
+
ADO = "ado"
|
|
13
|
+
JIRA = "jira"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PipelineStage(str, enum.Enum):
|
|
17
|
+
TEST_CASES = "test_cases"
|
|
18
|
+
GHERKIN_PLAYWRIGHT = "gherkin_playwright"
|
|
19
|
+
FULL = "full"
|
|
20
|
+
SKIP = "skip"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class WebhookEvent:
|
|
25
|
+
platform: Platform
|
|
26
|
+
work_item_id: str
|
|
27
|
+
state: str
|
|
28
|
+
work_item_type: str
|
|
29
|
+
title: str = ""
|
|
30
|
+
previous_state: str = ""
|
|
31
|
+
|
|
32
|
+
# ADO-specific
|
|
33
|
+
organization_url: str = ""
|
|
34
|
+
project_name: str = ""
|
|
35
|
+
|
|
36
|
+
# Jira-specific
|
|
37
|
+
cloud_id: str = ""
|
|
38
|
+
project_key: str = ""
|
|
39
|
+
|
|
40
|
+
# Common optional
|
|
41
|
+
helix_project_root: str = ""
|
|
42
|
+
app_base_url: str = ""
|
|
43
|
+
app_username: str = ""
|
|
44
|
+
app_password: str = ""
|
|
45
|
+
|
|
46
|
+
# LLM
|
|
47
|
+
llm_provider: str = "anthropic"
|
|
48
|
+
llm_model: str = ""
|
|
49
|
+
llm_api_key: str = ""
|
|
50
|
+
llm_base_url: str = ""
|
|
51
|
+
|
|
52
|
+
# Agent extra kwargs
|
|
53
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class PipelineResult:
|
|
58
|
+
success: bool
|
|
59
|
+
steps: list[str] = field(default_factory=list)
|
|
60
|
+
error: str = ""
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
return {"success": self.success, "steps": self.steps, "error": self.error}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
orchestrator.py — Routes webhook events to the appropriate agent pipeline.
|
|
3
|
+
|
|
4
|
+
State → Pipeline mapping
|
|
5
|
+
────────────────────────
|
|
6
|
+
Active / In Progress / Approved → test_cases (Agent 1 for ADO, Agent 5 for Jira)
|
|
7
|
+
Resolved / Done / Closed → gherkin_playwright (Agents 2 → 3 → 4)
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from .models import Platform, PipelineResult, PipelineStage, WebhookEvent
|
|
16
|
+
|
|
17
|
+
# Re-export models so existing importers (webhook_bridge.py) continue to work.
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Platform", "PipelineResult", "PipelineStage", "WebhookEvent",
|
|
20
|
+
"classify_state", "run_pipeline",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("stlc.orchestrator")
|
|
24
|
+
|
|
25
|
+
_ADO_TEST_CASE_STATES = frozenset({
|
|
26
|
+
"active", "approved", "committed", "in progress", "in review", "ready for testing",
|
|
27
|
+
})
|
|
28
|
+
_ADO_FULL_PIPELINE_STATES = frozenset({
|
|
29
|
+
"resolved", "done", "closed", "completed", "released",
|
|
30
|
+
})
|
|
31
|
+
_JIRA_TEST_CASE_STATES = frozenset({
|
|
32
|
+
"in progress", "active", "in development", "in review",
|
|
33
|
+
"ready for qa", "approved",
|
|
34
|
+
})
|
|
35
|
+
_JIRA_FULL_PIPELINE_STATES = frozenset({
|
|
36
|
+
"resolved", "done", "closed", "completed", "released",
|
|
37
|
+
})
|
|
38
|
+
_SKIP_TYPES = frozenset({"epic", "test case", "test suite", "test plan"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
# State classifier — routing logic only
|
|
43
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def classify_state(event: WebhookEvent) -> PipelineStage:
|
|
46
|
+
wi_type = (event.work_item_type or "").lower()
|
|
47
|
+
if wi_type in _SKIP_TYPES:
|
|
48
|
+
return PipelineStage.SKIP
|
|
49
|
+
|
|
50
|
+
# Explicit full-pipeline override takes priority over state-based routing.
|
|
51
|
+
if (
|
|
52
|
+
event.extra.get("pipeline") == "full"
|
|
53
|
+
or os.environ.get("STLC_FULL_PIPELINE", "").lower() == "true"
|
|
54
|
+
):
|
|
55
|
+
return PipelineStage.FULL
|
|
56
|
+
|
|
57
|
+
state = (event.state or "").lower()
|
|
58
|
+
if event.platform == Platform.ADO:
|
|
59
|
+
if state in _ADO_TEST_CASE_STATES:
|
|
60
|
+
return PipelineStage.TEST_CASES
|
|
61
|
+
if state in _ADO_FULL_PIPELINE_STATES:
|
|
62
|
+
return PipelineStage.GHERKIN_PLAYWRIGHT
|
|
63
|
+
else:
|
|
64
|
+
if state in _JIRA_TEST_CASE_STATES:
|
|
65
|
+
return PipelineStage.TEST_CASES
|
|
66
|
+
if state in _JIRA_FULL_PIPELINE_STATES:
|
|
67
|
+
return PipelineStage.GHERKIN_PLAYWRIGHT
|
|
68
|
+
|
|
69
|
+
return PipelineStage.SKIP
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
# Entry point — delegates entirely to the appropriate pipeline module
|
|
74
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async def run_pipeline(event: WebhookEvent) -> PipelineResult:
|
|
77
|
+
stage = classify_state(event)
|
|
78
|
+
logger.info(
|
|
79
|
+
"Pipeline stage: %s (platform=%s wi=%s state=%s)",
|
|
80
|
+
stage, event.platform, event.work_item_id, event.state,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if stage == PipelineStage.SKIP:
|
|
84
|
+
return PipelineResult(success=True, steps=["skipped"])
|
|
85
|
+
|
|
86
|
+
if stage == PipelineStage.FULL:
|
|
87
|
+
from .pipelines.full_pipeline import run
|
|
88
|
+
elif stage == PipelineStage.TEST_CASES:
|
|
89
|
+
if event.platform == Platform.ADO:
|
|
90
|
+
from .pipelines.ado_test_cases import run
|
|
91
|
+
else:
|
|
92
|
+
from .pipelines.jira_test_cases import run
|
|
93
|
+
else:
|
|
94
|
+
from .pipelines.gherkin_playwright import run
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return await asyncio.wait_for(run(event), timeout=600)
|
|
98
|
+
except asyncio.TimeoutError:
|
|
99
|
+
return PipelineResult(
|
|
100
|
+
success=False,
|
|
101
|
+
steps=[f"{stage.value}: wi={event.work_item_id}"],
|
|
102
|
+
error="Pipeline timed out after 600s",
|
|
103
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_base.py — Shared utilities for pipeline modules.
|
|
3
|
+
|
|
4
|
+
Each pipeline module imports mcp_servers() and event_env() from here.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from ..models import WebhookEvent
|
|
13
|
+
|
|
14
|
+
_IS_WINDOWS = sys.platform == "win32"
|
|
15
|
+
# project_root/.venv/bin (5 parents up from this file inside src/stlc_agents/webhook_orchestrator/pipelines/)
|
|
16
|
+
_BIN_DIR = (
|
|
17
|
+
pathlib.Path(__file__).parent.parent.parent.parent.parent.resolve()
|
|
18
|
+
/ ".venv"
|
|
19
|
+
/ ("Scripts" if _IS_WINDOWS else "bin")
|
|
20
|
+
)
|
|
21
|
+
_EXT = ".exe" if _IS_WINDOWS else ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mcp_servers(*names: str) -> dict:
|
|
25
|
+
"""Build an MCP config dict containing only the named server executables."""
|
|
26
|
+
return {
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
name: {"command": str(_BIN_DIR / f"{name}{_EXT}"), "args": []}
|
|
29
|
+
for name in names
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def event_env(event: WebhookEvent) -> dict[str, str]:
|
|
35
|
+
"""Merge event-level credentials into a copy of os.environ for subprocess isolation."""
|
|
36
|
+
env: dict[str, str] = dict(os.environ)
|
|
37
|
+
if event.organization_url:
|
|
38
|
+
env["ADO_ORGANIZATION_URL"] = event.organization_url
|
|
39
|
+
if event.project_name:
|
|
40
|
+
env["ADO_PROJECT_NAME"] = event.project_name
|
|
41
|
+
if event.cloud_id:
|
|
42
|
+
env["JIRA_CLOUD_ID"] = event.cloud_id
|
|
43
|
+
if event.llm_provider:
|
|
44
|
+
env["LLM_PROVIDER"] = event.llm_provider
|
|
45
|
+
if event.llm_model:
|
|
46
|
+
env["LLM_MODEL"] = event.llm_model
|
|
47
|
+
if event.llm_api_key:
|
|
48
|
+
env["LLM_API_KEY"] = event.llm_api_key
|
|
49
|
+
if event.llm_base_url:
|
|
50
|
+
env["LLM_BASE_URL"] = event.llm_base_url
|
|
51
|
+
if event.app_base_url:
|
|
52
|
+
env["APP_BASE_URL"] = event.app_base_url
|
|
53
|
+
if event.app_username:
|
|
54
|
+
env["APP_USERNAME"] = event.app_username
|
|
55
|
+
if event.app_password:
|
|
56
|
+
env["APP_PASSWORD"] = event.app_password
|
|
57
|
+
return env
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ado_test_cases.py — Agent 1 pipeline: generate and link test cases for an ADO work item.
|
|
3
|
+
|
|
4
|
+
Delegates entirely to qa-test-case-manager.
|
|
5
|
+
The agent server owns all test-case generation logic; this module only defines
|
|
6
|
+
the task and the stop conditions the LLM must respect.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from ..agent_runner import run_agent_loop
|
|
13
|
+
from ..models import PipelineResult, WebhookEvent
|
|
14
|
+
from ._base import event_env, mcp_servers
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("stlc.pipeline.ado_test_cases")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _task_prompt(event: WebhookEvent) -> str:
|
|
20
|
+
wi_type = (event.work_item_type or "").lower()
|
|
21
|
+
feature_note = (
|
|
22
|
+
"This is a Feature work item. The tool will return confirmation_required=true — "
|
|
23
|
+
"treat it as auto-confirmed and proceed to create_and_link_test_cases.\n"
|
|
24
|
+
if wi_type == "feature" else ""
|
|
25
|
+
)
|
|
26
|
+
return (
|
|
27
|
+
f"You are a QA automation agent running in webhook mode — auto-confirm all prompts.\n\n"
|
|
28
|
+
f"Task: Generate and link test cases for ADO work item {event.work_item_id}.\n"
|
|
29
|
+
f" Organisation: {event.organization_url}\n"
|
|
30
|
+
f" Project: {event.project_name}\n"
|
|
31
|
+
f" Type: {event.work_item_type}\n"
|
|
32
|
+
f" State: {event.state}\n"
|
|
33
|
+
f"{feature_note}\n"
|
|
34
|
+
f"Use the qa-test-case-manager tools to complete this task.\n\n"
|
|
35
|
+
f"Stop immediately if:\n"
|
|
36
|
+
f" - The work item is an Epic (tool returns epic_not_supported)\n"
|
|
37
|
+
f" - Linked test cases already exist (get_linked_test_cases returns non-empty list)\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run(event: WebhookEvent) -> PipelineResult:
|
|
42
|
+
label = f"ado-test-cases: wi={event.work_item_id}"
|
|
43
|
+
success, error = await run_agent_loop(
|
|
44
|
+
mcp_config=mcp_servers("qa-test-case-manager"),
|
|
45
|
+
prompt=_task_prompt(event),
|
|
46
|
+
llm_provider=event.llm_provider,
|
|
47
|
+
llm_model=event.llm_model,
|
|
48
|
+
llm_api_key=event.llm_api_key,
|
|
49
|
+
llm_base_url=event.llm_base_url,
|
|
50
|
+
env=event_env(event),
|
|
51
|
+
work_item_id=event.work_item_id,
|
|
52
|
+
)
|
|
53
|
+
if not success:
|
|
54
|
+
return PipelineResult(success=False, steps=[label], error=error)
|
|
55
|
+
return PipelineResult(success=True, steps=[label, "completed"])
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
full_pipeline.py — All 6 STLC steps in a single agent loop.
|
|
3
|
+
|
|
4
|
+
Runs the complete pipeline end-to-end:
|
|
5
|
+
1. Fetch work item
|
|
6
|
+
2. Deduplication pre-flight (get_linked_test_cases + list_helix_tree)
|
|
7
|
+
3. Create and link test cases
|
|
8
|
+
4. Generate and attach Gherkin
|
|
9
|
+
5. Capture live app context (requires APP_BASE_URL)
|
|
10
|
+
6. Generate Playwright TypeScript
|
|
11
|
+
6b. Write to Helix project on disk
|
|
12
|
+
|
|
13
|
+
Agents: qa-test-case-manager, qa-gherkin-generator,
|
|
14
|
+
qa-playwright-generator, qa-helix-writer.
|
|
15
|
+
|
|
16
|
+
Triggered by:
|
|
17
|
+
- event.extra["pipeline"] == "full"
|
|
18
|
+
- env var STLC_FULL_PIPELINE=true
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
from ..agent_runner import run_agent_loop
|
|
26
|
+
from ..models import Platform, PipelineResult, WebhookEvent
|
|
27
|
+
from ._base import event_env, mcp_servers
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("stlc.pipeline.full")
|
|
30
|
+
|
|
31
|
+
_AGENTS = (
|
|
32
|
+
"qa-test-case-manager",
|
|
33
|
+
"qa-gherkin-generator",
|
|
34
|
+
"qa-playwright-generator",
|
|
35
|
+
"qa-helix-writer",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _task_prompt(event: WebhookEvent) -> str:
|
|
40
|
+
wi_id = event.work_item_id
|
|
41
|
+
wi_type = (event.work_item_type or "").lower()
|
|
42
|
+
is_feature = wi_type == "feature"
|
|
43
|
+
is_ado = event.platform == Platform.ADO
|
|
44
|
+
|
|
45
|
+
# ── Platform context ────────────────────────────────────────────────────
|
|
46
|
+
if is_ado:
|
|
47
|
+
platform_label = "ADO"
|
|
48
|
+
platform_ctx = (
|
|
49
|
+
f" Organisation: {event.organization_url}\n"
|
|
50
|
+
f" Project: {event.project_name}\n"
|
|
51
|
+
)
|
|
52
|
+
if is_feature:
|
|
53
|
+
fetch_step = f"1. fetch_feature_hierarchy(work_item_id={wi_id})"
|
|
54
|
+
attach_tool = "attach_gherkin_to_feature"
|
|
55
|
+
else:
|
|
56
|
+
fetch_step = f"1. fetch_work_item_for_gherkin(work_item_id={wi_id})"
|
|
57
|
+
attach_tool = "attach_gherkin_to_work_item"
|
|
58
|
+
else:
|
|
59
|
+
platform_label = "Jira"
|
|
60
|
+
platform_ctx = (
|
|
61
|
+
f" Cloud ID: {event.cloud_id}\n"
|
|
62
|
+
f" Project: {event.project_key}\n"
|
|
63
|
+
)
|
|
64
|
+
fetch_step = f"1. fetch_work_item_for_gherkin(work_item_id={wi_id})"
|
|
65
|
+
attach_tool = "attach_gherkin_to_work_item"
|
|
66
|
+
|
|
67
|
+
# ── App credentials (event fields take priority over env vars) ──────────
|
|
68
|
+
app_url = event.app_base_url or os.environ.get("APP_BASE_URL", "")
|
|
69
|
+
app_username = event.app_username or os.environ.get("APP_USERNAME", "")
|
|
70
|
+
app_password = event.app_password or os.environ.get("APP_PASSWORD", "")
|
|
71
|
+
|
|
72
|
+
# ── Helix root ──────────────────────────────────────────────────────────
|
|
73
|
+
helix_root = event.helix_project_root
|
|
74
|
+
|
|
75
|
+
# ── Deduplication pre-flight ────────────────────────────────────────────
|
|
76
|
+
if helix_root:
|
|
77
|
+
dedup_block = (
|
|
78
|
+
f"DEDUPLICATION PRE-FLIGHT — run these BEFORE any creation steps:\n"
|
|
79
|
+
f" a) get_linked_test_cases(work_item_id={wi_id}) — store as existing_test_cases.\n"
|
|
80
|
+
f" b) list_helix_tree(helix_root={helix_root!r}) — check for files matching\n"
|
|
81
|
+
f" '*{wi_id}*' in both the 'features' AND 'steps' categories.\n"
|
|
82
|
+
f" If existing_test_cases is non-empty AND both Helix categories have matching files,\n"
|
|
83
|
+
f" this work item is fully covered — STOP and report the existing paths.\n\n"
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
dedup_block = (
|
|
87
|
+
f"DEDUPLICATION PRE-FLIGHT — run this BEFORE any creation step:\n"
|
|
88
|
+
f" a) get_linked_test_cases(work_item_id={wi_id}) — store as existing_test_cases.\n\n"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ── Step 3: test case creation ──────────────────────────────────────────
|
|
92
|
+
feature_auto_confirm = (
|
|
93
|
+
" Auto-confirm if the tool returns confirmation_required=true.\n"
|
|
94
|
+
if is_feature else ""
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# ── Steps 5–6: live capture + code generation ───────────────────────────
|
|
98
|
+
if app_url:
|
|
99
|
+
capture_step = (
|
|
100
|
+
f"5. capture_app_context(\n"
|
|
101
|
+
f" app_url={app_url!r},\n"
|
|
102
|
+
f" app_username={app_username!r},\n"
|
|
103
|
+
f" app_password={app_password!r},\n"
|
|
104
|
+
f" gherkin_content=<gherkin from step 4>)\n"
|
|
105
|
+
f" STOP if result.locator_source='gherkin-inferred' OR result.abort=true OR result.error is set.\n"
|
|
106
|
+
f" Only proceed to step 6 when locator_source='playwright-mcp-verified' AND context_map is non-empty.\n"
|
|
107
|
+
)
|
|
108
|
+
pw_step = (
|
|
109
|
+
f"6. generate_playwright_code(\n"
|
|
110
|
+
f" gherkin_content=<gherkin from step 4>,\n"
|
|
111
|
+
f" context_map=<context_map from step 5>)\n"
|
|
112
|
+
f" Store the cache_key from the result.\n"
|
|
113
|
+
)
|
|
114
|
+
step_offset = 6
|
|
115
|
+
else:
|
|
116
|
+
capture_step = (
|
|
117
|
+
"5. STOP — APP_BASE_URL is not configured.\n"
|
|
118
|
+
" A live context_map is required to generate correct locators.\n"
|
|
119
|
+
" Placeholder selectors are not acceptable output.\n"
|
|
120
|
+
" To unblock: set APP_BASE_URL (+ APP_USERNAME / APP_PASSWORD) in .env,\n"
|
|
121
|
+
" start 'npx @playwright/mcp@latest --port 8931', then re-trigger.\n"
|
|
122
|
+
" Do NOT call generate_playwright_code without a verified context_map.\n"
|
|
123
|
+
)
|
|
124
|
+
pw_step = ""
|
|
125
|
+
step_offset = 4
|
|
126
|
+
|
|
127
|
+
# ── Steps 6b: Helix write ───────────────────────────────────────────────
|
|
128
|
+
if helix_root and app_url:
|
|
129
|
+
n = step_offset
|
|
130
|
+
helix_steps = (
|
|
131
|
+
f"{n+1}. inspect_helix_project(helix_root={helix_root!r})\n"
|
|
132
|
+
f" Proceed regardless of framework_state or missing_infra — do NOT stop after inspect.\n"
|
|
133
|
+
f"{n+2}. write_helix_files(helix_root={helix_root!r}, cache_key=<cache_key from step {n}>)\n"
|
|
134
|
+
f" Pass cache_key directly — do NOT pass a files dict.\n"
|
|
135
|
+
f" This step is MANDATORY — the pipeline is not complete until write_helix_files succeeds.\n"
|
|
136
|
+
)
|
|
137
|
+
elif helix_root:
|
|
138
|
+
# APP_BASE_URL missing — Playwright steps skipped, but still report Helix state
|
|
139
|
+
helix_steps = (
|
|
140
|
+
f"5b. inspect_helix_project(helix_root={helix_root!r})\n"
|
|
141
|
+
f" Report current state; no write_helix_files without generated code.\n"
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
helix_steps = ""
|
|
145
|
+
|
|
146
|
+
# ── Stop conditions ─────────────────────────────────────────────────────
|
|
147
|
+
stop_conditions = (
|
|
148
|
+
"\nStop immediately if:\n"
|
|
149
|
+
" - fetch returns epic_not_supported or an error\n"
|
|
150
|
+
" - APP_BASE_URL is empty (steps 5+ require a live context_map)\n"
|
|
151
|
+
" - capture_app_context returns locator_source='gherkin-inferred', abort=true, or any error field\n"
|
|
152
|
+
" - capture_app_context returns an empty context_map\n"
|
|
153
|
+
" - generate_and_attach_gherkin returns status='duplicate' or 'already_attached'\n"
|
|
154
|
+
f" - Deduplication pre-flight finds existing .feature + .steps.ts for WI #{wi_id}\n"
|
|
155
|
+
"\nNEVER call generate_playwright_code unless capture_app_context returned\n"
|
|
156
|
+
" locator_source='playwright-mcp-verified' with a non-empty context_map.\n"
|
|
157
|
+
" Gherkin-inferred locators reference elements that do not exist in the real app.\n"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
f"You are a QA automation agent running in webhook mode — auto-confirm all prompts.\n\n"
|
|
162
|
+
f"Task: Run the FULL STLC pipeline (test cases + Gherkin + Playwright + Helix) for\n"
|
|
163
|
+
f"{platform_label} work item {wi_id}.\n"
|
|
164
|
+
f" Type: {event.work_item_type}\n"
|
|
165
|
+
f" State: {event.state}\n"
|
|
166
|
+
f"{platform_ctx}\n"
|
|
167
|
+
f"{dedup_block}"
|
|
168
|
+
f"Steps (execute in order):\n"
|
|
169
|
+
f"{fetch_step}\n"
|
|
170
|
+
f" Store result as work_item_summary.\n"
|
|
171
|
+
f"2. create_and_link_test_cases(work_item_id={wi_id}, ...)\n"
|
|
172
|
+
f" Derive test cases from work_item_summary acceptance criteria.\n"
|
|
173
|
+
f" SKIP if existing_test_cases from dedup pre-flight is non-empty.\n"
|
|
174
|
+
f"{feature_auto_confirm}"
|
|
175
|
+
f"3. {attach_tool}(work_item_id={wi_id}, ...)\n"
|
|
176
|
+
f" Generate BDD scenarios from work_item_summary.\n"
|
|
177
|
+
f" SKIP if Helix dedup pre-flight found a matching .feature file.\n"
|
|
178
|
+
f" Store gherkin_content from result.\n"
|
|
179
|
+
f"4. validate_gherkin_content(gherkin_content=<from step 3>, scope='work_item')\n"
|
|
180
|
+
f" Proceed only if valid=true.\n"
|
|
181
|
+
f"{capture_step}"
|
|
182
|
+
f"{pw_step}"
|
|
183
|
+
f"{helix_steps}"
|
|
184
|
+
f"{stop_conditions}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def run(event: WebhookEvent) -> PipelineResult:
|
|
189
|
+
label = f"full-pipeline: wi={event.work_item_id}"
|
|
190
|
+
success, error = await run_agent_loop(
|
|
191
|
+
mcp_config=mcp_servers(*_AGENTS),
|
|
192
|
+
prompt=_task_prompt(event),
|
|
193
|
+
llm_provider=event.llm_provider,
|
|
194
|
+
llm_model=event.llm_model,
|
|
195
|
+
llm_api_key=event.llm_api_key,
|
|
196
|
+
llm_base_url=event.llm_base_url,
|
|
197
|
+
env=event_env(event),
|
|
198
|
+
work_item_id=event.work_item_id,
|
|
199
|
+
)
|
|
200
|
+
if not success:
|
|
201
|
+
return PipelineResult(success=False, steps=[label], error=error)
|
|
202
|
+
return PipelineResult(success=True, steps=[label, "completed"])
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gherkin_playwright.py — Agents 2 → 3 → 4 pipeline.
|
|
3
|
+
|
|
4
|
+
Generates Gherkin, Playwright tests, and writes output to the Helix repo.
|
|
5
|
+
Delegates to: qa-gherkin-generator, qa-playwright-generator, qa-helix-writer.
|
|
6
|
+
The agent servers own all generation logic; this module only defines the task
|
|
7
|
+
sequence and the stop conditions the LLM must respect.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from ..agent_runner import run_agent_loop
|
|
15
|
+
from ..models import Platform, PipelineResult, WebhookEvent
|
|
16
|
+
from ._base import event_env, mcp_servers
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("stlc.pipeline.gherkin_playwright")
|
|
19
|
+
|
|
20
|
+
_AGENTS = ("qa-gherkin-generator", "qa-playwright-generator", "qa-helix-writer")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _task_prompt(event: WebhookEvent) -> str:
|
|
24
|
+
wi_type = (event.work_item_type or "").lower()
|
|
25
|
+
is_feature = wi_type == "feature"
|
|
26
|
+
|
|
27
|
+
# ── Step 1: fetch ────────────────────────────────────────────────────────
|
|
28
|
+
if event.platform == Platform.ADO:
|
|
29
|
+
if is_feature:
|
|
30
|
+
fetch_step = f"1. fetch_feature_hierarchy({event.work_item_id})"
|
|
31
|
+
attach_tool = "attach_gherkin_to_feature"
|
|
32
|
+
platform_ctx = (
|
|
33
|
+
f" Organisation: {event.organization_url}\n"
|
|
34
|
+
f" Project: {event.project_name}\n"
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
fetch_step = f"1. fetch_work_item_for_gherkin({event.work_item_id})"
|
|
38
|
+
attach_tool = "attach_gherkin_to_work_item"
|
|
39
|
+
platform_ctx = (
|
|
40
|
+
f" Organisation: {event.organization_url}\n"
|
|
41
|
+
f" Project: {event.project_name}\n"
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
fetch_step = f"1. fetch_work_item_for_gherkin({event.work_item_id})"
|
|
45
|
+
attach_tool = "attach_gherkin_to_work_item"
|
|
46
|
+
platform_ctx = (
|
|
47
|
+
f" Cloud ID: {event.cloud_id}\n"
|
|
48
|
+
f" Project: {event.project_key}\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ── Step 2: gherkin ──────────────────────────────────────────────────────
|
|
52
|
+
gherkin_step = f"2. generate_and_attach_gherkin — generate BDD scenarios and attach via {attach_tool}"
|
|
53
|
+
|
|
54
|
+
# ── Step 3: optional live capture ───────────────────────────────────────
|
|
55
|
+
app_url = os.environ.get("APP_BASE_URL", "")
|
|
56
|
+
app_username = os.environ.get("APP_USERNAME", "")
|
|
57
|
+
app_password = os.environ.get("APP_PASSWORD", "")
|
|
58
|
+
|
|
59
|
+
if app_url:
|
|
60
|
+
capture_step = (
|
|
61
|
+
f"3. capture_app_context("
|
|
62
|
+
f"app_url={app_url!r}, "
|
|
63
|
+
f"app_username={app_username!r}, "
|
|
64
|
+
f"app_password={app_password!r}, "
|
|
65
|
+
f"gherkin_content=<gherkin from step 2>)\n"
|
|
66
|
+
f" STOP if result contains locator_source='gherkin-inferred' — "
|
|
67
|
+
f"the Playwright MCP server must be reachable. Do not proceed with inferred selectors.\n"
|
|
68
|
+
)
|
|
69
|
+
pw_step = "4. generate_playwright_code(gherkin_content=<gherkin>, context_map=<from step 3>)"
|
|
70
|
+
step_offset = 4
|
|
71
|
+
else:
|
|
72
|
+
# No APP_BASE_URL — cannot capture live selectors; pipeline must not produce placeholder output.
|
|
73
|
+
capture_step = (
|
|
74
|
+
"3. STOP — APP_BASE_URL is not configured.\n"
|
|
75
|
+
" Context mapping is required to generate correct locators. Placeholder selectors\n"
|
|
76
|
+
" are not acceptable output. To unblock this pipeline:\n"
|
|
77
|
+
" a) Set APP_BASE_URL (and optionally APP_USERNAME / APP_PASSWORD) in .env\n"
|
|
78
|
+
" b) Start the Playwright MCP server: npx @playwright/mcp@latest --port 8931\n"
|
|
79
|
+
" c) Re-trigger the webhook.\n"
|
|
80
|
+
" Do NOT call generate_playwright_code without a verified context_map.\n"
|
|
81
|
+
)
|
|
82
|
+
pw_step = ""
|
|
83
|
+
step_offset = 3
|
|
84
|
+
|
|
85
|
+
# ── Helix steps (optional) ───────────────────────────────────────────────
|
|
86
|
+
helix_root = event.helix_project_root
|
|
87
|
+
if helix_root:
|
|
88
|
+
n = step_offset
|
|
89
|
+
helix_steps = (
|
|
90
|
+
f"{n+1}. inspect_helix_project(helix_root={helix_root!r})\n"
|
|
91
|
+
f" NOTE: inspect result is for context only — proceed to the next steps regardless of\n"
|
|
92
|
+
f" framework_state or missing_infra. Do NOT stop after inspect.\n"
|
|
93
|
+
f"{n+2}. write_helix_files(helix_root={helix_root!r}, cache_key=<cache_key from step {step_offset}>)\n"
|
|
94
|
+
f" Pass cache_key directly — do NOT pass a files dict, do NOT call get_generated_files first.\n"
|
|
95
|
+
f" This step is MANDATORY — the pipeline is not complete until write_helix_files succeeds.\n"
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
helix_steps = ""
|
|
99
|
+
|
|
100
|
+
# ── Deduplication pre-flight (only when helix_root is set) ──────────────
|
|
101
|
+
wi_id = event.work_item_id
|
|
102
|
+
if helix_root:
|
|
103
|
+
dedup_preflight = (
|
|
104
|
+
f"DEDUPLICATION PRE-FLIGHT — run this BEFORE the steps below:\n"
|
|
105
|
+
f" Call list_helix_tree(helix_root={helix_root!r}).\n"
|
|
106
|
+
f" If files matching '*{wi_id}*' already exist in BOTH the 'features' AND 'steps'\n"
|
|
107
|
+
f" categories, this work item has already been fully processed.\n"
|
|
108
|
+
f" STOP immediately and report the existing file paths — do not regenerate.\n\n"
|
|
109
|
+
)
|
|
110
|
+
dedup_stop = (
|
|
111
|
+
f" - Deduplication pre-flight found existing .feature and .steps.ts files for WI #{wi_id}\n"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
dedup_preflight = ""
|
|
115
|
+
dedup_stop = ""
|
|
116
|
+
|
|
117
|
+
stop_conditions = (
|
|
118
|
+
f"\nStop immediately if:\n"
|
|
119
|
+
f" - fetch returns epic_not_supported or an error\n"
|
|
120
|
+
f" - generate_and_attach_gherkin returns status='duplicate' or 'already_attached'\n"
|
|
121
|
+
f"{dedup_stop}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
f"You are a QA automation agent running in webhook mode — auto-confirm all prompts.\n\n"
|
|
126
|
+
f"Task: Run the full STLC generation pipeline for "
|
|
127
|
+
f"{'ADO' if event.platform == Platform.ADO else 'Jira'} work item {wi_id}.\n"
|
|
128
|
+
f" Type: {event.work_item_type}\n"
|
|
129
|
+
f" State: {event.state}\n"
|
|
130
|
+
f"{platform_ctx}\n"
|
|
131
|
+
f"{dedup_preflight}"
|
|
132
|
+
f"Steps:\n"
|
|
133
|
+
f"{fetch_step}\n"
|
|
134
|
+
f"{gherkin_step}\n"
|
|
135
|
+
f"{capture_step}"
|
|
136
|
+
f"{pw_step}\n"
|
|
137
|
+
f"{helix_steps}"
|
|
138
|
+
f"{stop_conditions}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def run(event: WebhookEvent) -> PipelineResult:
|
|
143
|
+
label = f"gherkin-playwright: wi={event.work_item_id}"
|
|
144
|
+
success, error = await run_agent_loop(
|
|
145
|
+
mcp_config=mcp_servers(*_AGENTS),
|
|
146
|
+
prompt=_task_prompt(event),
|
|
147
|
+
llm_provider=event.llm_provider,
|
|
148
|
+
llm_model=event.llm_model,
|
|
149
|
+
llm_api_key=event.llm_api_key,
|
|
150
|
+
llm_base_url=event.llm_base_url,
|
|
151
|
+
env=event_env(event),
|
|
152
|
+
work_item_id=event.work_item_id,
|
|
153
|
+
)
|
|
154
|
+
if not success:
|
|
155
|
+
return PipelineResult(success=False, steps=[label], error=error)
|
|
156
|
+
return PipelineResult(success=True, steps=[label, "completed"])
|