@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.
Files changed (46) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +77 -0
  3. package/bin/suitest-mcp.js +123 -0
  4. package/package.json +50 -0
  5. package/python/suitest_lifecycle/__init__.py +3 -0
  6. package/python/suitest_lifecycle/analyzers/__init__.py +1 -0
  7. package/python/suitest_lifecycle/analyzers/crawl.py +187 -0
  8. package/python/suitest_lifecycle/analyzers/express.py +226 -0
  9. package/python/suitest_lifecycle/analyzers/openapi.py +163 -0
  10. package/python/suitest_lifecycle/analyzers/postman.py +132 -0
  11. package/python/suitest_lifecycle/analyzers/react.py +107 -0
  12. package/python/suitest_lifecycle/analyzers/zod_schema.py +131 -0
  13. package/python/suitest_lifecycle/blackbox/__init__.py +11 -0
  14. package/python/suitest_lifecycle/blackbox/bootstrap.py +249 -0
  15. package/python/suitest_lifecycle/blackbox/crawler.py +383 -0
  16. package/python/suitest_lifecycle/blackbox/detector.py +169 -0
  17. package/python/suitest_lifecycle/blackbox/generator.py +608 -0
  18. package/python/suitest_lifecycle/blackbox/graph.py +107 -0
  19. package/python/suitest_lifecycle/blackbox/mcp.py +546 -0
  20. package/python/suitest_lifecycle/blackbox/models.py +299 -0
  21. package/python/suitest_lifecycle/blackbox/prd_ingest.py +108 -0
  22. package/python/suitest_lifecycle/blackbox/reporter.py +76 -0
  23. package/python/suitest_lifecycle/blackbox/selector.py +111 -0
  24. package/python/suitest_lifecycle/cli.py +127 -0
  25. package/python/suitest_lifecycle/config.py +314 -0
  26. package/python/suitest_lifecycle/enrich.py +140 -0
  27. package/python/suitest_lifecycle/exporters/__init__.py +1 -0
  28. package/python/suitest_lifecycle/exporters/backend.py +345 -0
  29. package/python/suitest_lifecycle/exporters/frontend.py +459 -0
  30. package/python/suitest_lifecycle/frontend_runtime.py +77 -0
  31. package/python/suitest_lifecycle/llm_bridge.py +365 -0
  32. package/python/suitest_lifecycle/mcp_server.py +187 -0
  33. package/python/suitest_lifecycle/models.py +166 -0
  34. package/python/suitest_lifecycle/orchestrator.py +500 -0
  35. package/python/suitest_lifecycle/paths.py +90 -0
  36. package/python/suitest_lifecycle/plan.py +366 -0
  37. package/python/suitest_lifecycle/plan_frontend.py +252 -0
  38. package/python/suitest_lifecycle/prd.py +92 -0
  39. package/python/suitest_lifecycle/process.py +111 -0
  40. package/python/suitest_lifecycle/publish.py +218 -0
  41. package/python/suitest_lifecycle/readiness.py +83 -0
  42. package/python/suitest_lifecycle/report.py +179 -0
  43. package/python/suitest_lifecycle/runner.py +138 -0
  44. package/python/suitest_lifecycle/serialize.py +131 -0
  45. package/python/suitest_lifecycle/tcm.py +149 -0
  46. 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()]