@suiflex/suitest-mcp 0.1.0
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/LICENSE +201 -0
- package/README.md +77 -0
- package/bin/suitest-mcp.js +123 -0
- package/package.json +50 -0
- package/python/suitest_lifecycle/__init__.py +3 -0
- package/python/suitest_lifecycle/analyzers/__init__.py +1 -0
- package/python/suitest_lifecycle/analyzers/crawl.py +187 -0
- package/python/suitest_lifecycle/analyzers/express.py +226 -0
- package/python/suitest_lifecycle/analyzers/openapi.py +163 -0
- package/python/suitest_lifecycle/analyzers/postman.py +132 -0
- package/python/suitest_lifecycle/analyzers/react.py +107 -0
- package/python/suitest_lifecycle/analyzers/zod_schema.py +131 -0
- package/python/suitest_lifecycle/blackbox/__init__.py +11 -0
- package/python/suitest_lifecycle/blackbox/bootstrap.py +249 -0
- package/python/suitest_lifecycle/blackbox/crawler.py +383 -0
- package/python/suitest_lifecycle/blackbox/detector.py +169 -0
- package/python/suitest_lifecycle/blackbox/generator.py +608 -0
- package/python/suitest_lifecycle/blackbox/graph.py +107 -0
- package/python/suitest_lifecycle/blackbox/mcp.py +546 -0
- package/python/suitest_lifecycle/blackbox/models.py +299 -0
- package/python/suitest_lifecycle/blackbox/prd_ingest.py +108 -0
- package/python/suitest_lifecycle/blackbox/reporter.py +76 -0
- package/python/suitest_lifecycle/blackbox/selector.py +111 -0
- package/python/suitest_lifecycle/cli.py +127 -0
- package/python/suitest_lifecycle/config.py +314 -0
- package/python/suitest_lifecycle/enrich.py +140 -0
- package/python/suitest_lifecycle/exporters/__init__.py +1 -0
- package/python/suitest_lifecycle/exporters/backend.py +345 -0
- package/python/suitest_lifecycle/exporters/frontend.py +459 -0
- package/python/suitest_lifecycle/frontend_runtime.py +77 -0
- package/python/suitest_lifecycle/llm_bridge.py +365 -0
- package/python/suitest_lifecycle/mcp_server.py +187 -0
- package/python/suitest_lifecycle/models.py +166 -0
- package/python/suitest_lifecycle/orchestrator.py +500 -0
- package/python/suitest_lifecycle/paths.py +90 -0
- package/python/suitest_lifecycle/plan.py +366 -0
- package/python/suitest_lifecycle/plan_frontend.py +252 -0
- package/python/suitest_lifecycle/prd.py +92 -0
- package/python/suitest_lifecycle/process.py +111 -0
- package/python/suitest_lifecycle/publish.py +218 -0
- package/python/suitest_lifecycle/readiness.py +83 -0
- package/python/suitest_lifecycle/report.py +179 -0
- package/python/suitest_lifecycle/runner.py +138 -0
- package/python/suitest_lifecycle/serialize.py +131 -0
- package/python/suitest_lifecycle/tcm.py +149 -0
- package/python/suitest_lifecycle/tools.py +217 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Structured lifecycle tools — the agent-facing surface.
|
|
2
|
+
|
|
3
|
+
Each tool returns the same envelope so an agent (or the MCP server) gets
|
|
4
|
+
predictable, machine-parseable output::
|
|
5
|
+
|
|
6
|
+
{"success": bool, "summary": str, "data": {...}, "artifacts": [...], "errors": [...]}
|
|
7
|
+
|
|
8
|
+
These wrap the orchestrator; they never raise for expected failures (bad config,
|
|
9
|
+
target not ready) — those become ``success=false`` envelopes with ``errors``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from suitest_lifecycle.blackbox.mcp import BLACKBOX_TOOLS
|
|
15
|
+
from suitest_lifecycle.config import ConfigError, load_config
|
|
16
|
+
from suitest_lifecycle.models import Mode
|
|
17
|
+
from suitest_lifecycle.orchestrator import generate_only, run_lifecycle
|
|
18
|
+
from suitest_lifecycle.paths import build_paths
|
|
19
|
+
from suitest_lifecycle.serialize import (
|
|
20
|
+
code_summary_to_json,
|
|
21
|
+
plan_to_json,
|
|
22
|
+
summary_to_json,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _envelope(
|
|
27
|
+
success: bool,
|
|
28
|
+
summary: str,
|
|
29
|
+
data: dict[str, object] | None = None,
|
|
30
|
+
artifacts: list[str] | None = None,
|
|
31
|
+
errors: list[str] | None = None,
|
|
32
|
+
) -> dict[str, object]:
|
|
33
|
+
return {
|
|
34
|
+
"success": success,
|
|
35
|
+
"summary": summary,
|
|
36
|
+
"data": data or {},
|
|
37
|
+
"artifacts": artifacts or [],
|
|
38
|
+
"errors": errors or [],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _safe_load(config_path: str) -> tuple[object, dict[str, object] | None]:
|
|
43
|
+
try:
|
|
44
|
+
return load_config(config_path), None
|
|
45
|
+
except (ConfigError, OSError) as exc:
|
|
46
|
+
return None, _envelope(False, f"config error: {exc}", errors=[str(exc)])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def analyze_project(config_path: str) -> dict[str, object]:
|
|
50
|
+
"""Static-analyze the target; return endpoints/pages without generating."""
|
|
51
|
+
cfg, err = _safe_load(config_path)
|
|
52
|
+
if err is not None:
|
|
53
|
+
return err
|
|
54
|
+
from suitest_lifecycle.analyzers.express import analyze_express
|
|
55
|
+
from suitest_lifecycle.analyzers.react import analyze_react
|
|
56
|
+
|
|
57
|
+
summary = (
|
|
58
|
+
analyze_express(cfg.project_path, cfg.project_name) # type: ignore[union-attr]
|
|
59
|
+
if cfg.mode is Mode.BACKEND # type: ignore[union-attr]
|
|
60
|
+
else analyze_react(cfg.project_path, cfg.project_name) # type: ignore[union-attr]
|
|
61
|
+
)
|
|
62
|
+
label = (
|
|
63
|
+
f"{len(summary.endpoints)} endpoints"
|
|
64
|
+
if summary.mode is Mode.BACKEND
|
|
65
|
+
else f"{len(summary.pages)} pages"
|
|
66
|
+
)
|
|
67
|
+
return _envelope(
|
|
68
|
+
True, f"analyzed {summary.mode.value}: {label}", data=code_summary_to_json(summary)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_test_cases(config_path: str) -> dict[str, object]:
|
|
73
|
+
"""analyze → PRD → plan → export runnable files (no execution)."""
|
|
74
|
+
cfg, err = _safe_load(config_path)
|
|
75
|
+
if err is not None:
|
|
76
|
+
return err
|
|
77
|
+
_summary, cases, paths = generate_only(cfg) # type: ignore[arg-type]
|
|
78
|
+
artifacts = [str(paths.prd_json), str(paths.test_plan_json), str(paths.code_summary_json)]
|
|
79
|
+
artifacts += [str(paths.test_file(c.automation_file)) for c in cases if c.automation_file]
|
|
80
|
+
return _envelope(
|
|
81
|
+
True,
|
|
82
|
+
f"generated {len(cases)} test case(s) for {cfg.mode.value}", # type: ignore[union-attr]
|
|
83
|
+
data={"cases": plan_to_json(cases)},
|
|
84
|
+
artifacts=artifacts,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_backend_tests(config_path: str) -> dict[str, object]:
|
|
89
|
+
return _mode_guarded_generate(config_path, Mode.BACKEND)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def generate_frontend_tests(config_path: str) -> dict[str, object]:
|
|
93
|
+
return _mode_guarded_generate(config_path, Mode.FRONTEND)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _mode_guarded_generate(config_path: str, expected: Mode) -> dict[str, object]:
|
|
97
|
+
cfg, err = _safe_load(config_path)
|
|
98
|
+
if err is not None:
|
|
99
|
+
return err
|
|
100
|
+
if cfg.mode is not expected: # type: ignore[union-attr]
|
|
101
|
+
return _envelope(
|
|
102
|
+
False,
|
|
103
|
+
f"config mode is {cfg.mode.value}, expected {expected.value}", # type: ignore[union-attr]
|
|
104
|
+
errors=["mode mismatch"],
|
|
105
|
+
)
|
|
106
|
+
return generate_test_cases(config_path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_backend_tests(config_path: str) -> dict[str, object]:
|
|
110
|
+
return _run_guarded(config_path, Mode.BACKEND)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_frontend_tests(config_path: str) -> dict[str, object]:
|
|
114
|
+
return _run_guarded(config_path, Mode.FRONTEND)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_guarded(config_path: str, expected: Mode) -> dict[str, object]:
|
|
118
|
+
cfg, err = _safe_load(config_path)
|
|
119
|
+
if err is not None:
|
|
120
|
+
return err
|
|
121
|
+
if cfg.mode is not expected: # type: ignore[union-attr]
|
|
122
|
+
return _envelope(
|
|
123
|
+
False,
|
|
124
|
+
f"config mode is {cfg.mode.value}, expected {expected.value}", # type: ignore[union-attr]
|
|
125
|
+
errors=["mode mismatch"],
|
|
126
|
+
)
|
|
127
|
+
result = run_lifecycle(cfg) # type: ignore[arg-type]
|
|
128
|
+
return _envelope(
|
|
129
|
+
result.success,
|
|
130
|
+
result.summary,
|
|
131
|
+
data=summary_to_json(result.run) if result.run else {},
|
|
132
|
+
artifacts=result.artifacts,
|
|
133
|
+
errors=result.errors,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_tests(config_path: str) -> dict[str, object]:
|
|
138
|
+
"""Mode-agnostic full lifecycle run."""
|
|
139
|
+
cfg, err = _safe_load(config_path)
|
|
140
|
+
if err is not None:
|
|
141
|
+
return err
|
|
142
|
+
result = run_lifecycle(cfg) # type: ignore[arg-type]
|
|
143
|
+
return _envelope(
|
|
144
|
+
result.success,
|
|
145
|
+
result.summary,
|
|
146
|
+
data=summary_to_json(result.run) if result.run else {},
|
|
147
|
+
artifacts=result.artifacts,
|
|
148
|
+
errors=result.errors,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def generate_report(config_path: str) -> dict[str, object]:
|
|
153
|
+
"""Re-emit reports from the last run's stored summary.json (no re-run)."""
|
|
154
|
+
cfg, err = _safe_load(config_path)
|
|
155
|
+
if err is not None:
|
|
156
|
+
return err
|
|
157
|
+
paths = build_paths(cfg.output_dir, cfg.mode) # type: ignore[union-attr]
|
|
158
|
+
summary_json = paths.reports_dir / "summary.json"
|
|
159
|
+
if not summary_json.is_file():
|
|
160
|
+
return _envelope(
|
|
161
|
+
False, "no prior run found — run the lifecycle first", errors=["summary.json missing"]
|
|
162
|
+
)
|
|
163
|
+
return _envelope(
|
|
164
|
+
True,
|
|
165
|
+
f"report available at {paths.reports_dir}",
|
|
166
|
+
artifacts=[
|
|
167
|
+
str(paths.reports_dir / "summary.md"),
|
|
168
|
+
str(paths.reports_dir / "summary.json"),
|
|
169
|
+
str(paths.reports_dir / "summary.html"),
|
|
170
|
+
str(paths.raw_report_md),
|
|
171
|
+
],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sync_tcm(config_path: str) -> dict[str, object]:
|
|
176
|
+
"""Report the TCM mirror location and case/run counts."""
|
|
177
|
+
import json
|
|
178
|
+
|
|
179
|
+
cfg, err = _safe_load(config_path)
|
|
180
|
+
if err is not None:
|
|
181
|
+
return err
|
|
182
|
+
paths = build_paths(cfg.output_dir, cfg.mode) # type: ignore[union-attr]
|
|
183
|
+
cases = (
|
|
184
|
+
json.loads(paths.tcm_cases_json.read_text("utf-8"))
|
|
185
|
+
if paths.tcm_cases_json.is_file()
|
|
186
|
+
else []
|
|
187
|
+
)
|
|
188
|
+
runs = (
|
|
189
|
+
json.loads(paths.tcm_runs_json.read_text("utf-8")) if paths.tcm_runs_json.is_file() else []
|
|
190
|
+
)
|
|
191
|
+
return _envelope(
|
|
192
|
+
True,
|
|
193
|
+
f"TCM mirror: {len(cases)} case(s), {len(runs)} run(s)",
|
|
194
|
+
data={"cases": len(cases), "runs": len(runs)},
|
|
195
|
+
artifacts=[str(paths.tcm_cases_json), str(paths.tcm_runs_json)],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Tool registry (name -> callable) used by the MCP server.
|
|
200
|
+
TOOLS = {
|
|
201
|
+
"analyze_project": analyze_project,
|
|
202
|
+
"generate_test_cases": generate_test_cases,
|
|
203
|
+
"generate_backend_tests": generate_backend_tests,
|
|
204
|
+
"generate_frontend_tests": generate_frontend_tests,
|
|
205
|
+
"run_backend_tests": run_backend_tests,
|
|
206
|
+
"run_frontend_tests": run_frontend_tests,
|
|
207
|
+
"run_tests": run_tests,
|
|
208
|
+
"sync_tcm": sync_tcm,
|
|
209
|
+
"generate_report": generate_report,
|
|
210
|
+
**BLACKBOX_TOOLS,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Blackbox tools take structured kwargs (url/username/…), not just config_path.
|
|
214
|
+
KWARG_TOOLS = frozenset(BLACKBOX_TOOLS)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
__all__ = ["KWARG_TOOLS", "TOOLS", *TOOLS.keys()]
|