synapse-hub 2.1.1 → 2.2.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/.agents/agents/orchestrator.md +7 -0
- package/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +1 -1
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +1 -2
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +11 -4
- package/.agents/skills/architecture-decision-records/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +32 -11
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +1 -4
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +1 -1
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +1 -3
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +1 -1
- package/.agents/skills/github-actions-templates/SKILL.md +1 -1
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +1 -3
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/lam-script-generator/SKILL.md +1 -2
- package/.agents/skills/playwright-live-interaction/SKILL.md +1 -4
- package/.agents/skills/security-dast-sentinel/SKILL.md +1 -1
- package/.agents/skills/self-healing-locator/SKILL.md +1 -2
- package/.agents/skills/skillspector-security-guard/SKILL.md +115 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/visual-regression-sentinel/SKILL.md +1 -1
- package/.agents/skills/wcag-audit-patterns/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/AGENTS.md +36 -0
- package/README.md +140 -103
- package/package.json +1 -1
- package/skills_manifest.json +42 -29
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +15 -1
- package/templates/ai_bridges/CLAUDE.md +31 -0
- package/templates/ai_bridges/copilot-instructions.md +25 -0
- package/templates/ai_bridges/cursor_synapse_qa.mdc +26 -0
- package/templates/ai_bridges/windsurfrules +25 -0
- package/templates/ci/github-actions-synapse.yml +28 -0
- package/templates/ci/gitlab-ci-synapse.yml +16 -0
- package/tools/TOOLS_RUNBOOK.md +203 -12
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +8 -0
- package/tools/cli.py +74 -2
- package/tools/export_postman_collection.py +535 -218
- package/tools/generate_jmx_plan.py +672 -102
- package/tools/git_pre_commit_guard.py +21 -0
- package/tools/jtl_analyzer.py +474 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/newman_analyzer.py +385 -0
- package/tools/setup_project.py +203 -18
- package/tools/skillspector.py +1141 -0
- package/tools/telemetry_correlator.py +351 -0
- package/tools/templates/jmeter-distributed/README.md +54 -0
- package/tools/templates/jmeter-distributed/docker-compose.yml +83 -0
- package/tools/update_sentinel.py +313 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
tools/newman_analyzer.py — Enterprise Newman Test Results Analyzer & CI/CD Quality Gate
|
|
5
|
+
---------------------------------------------------------------------------------------
|
|
6
|
+
Parses Postman / Newman CLI execution reports (JSON format), calculates statistical
|
|
7
|
+
pass/fail rates, triages failed assertions with root causes, evaluates latency SLAs,
|
|
8
|
+
and enforces deterministic Quality Gates in CI/CD pipelines (Exit Code 0 vs 1).
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
1. Universal Newman JSON Ingestion: Reads standard `--reporters json` exports.
|
|
12
|
+
2. Multi-Metric Aggregation:
|
|
13
|
+
- Total Executions, Passed/Failed Requests, Error Rate (%)
|
|
14
|
+
- Total pm.test Assertions, Failed Assertions, Pass Ratio (%)
|
|
15
|
+
- Latency statistics (Average, Min, Max response time in ms)
|
|
16
|
+
3. Failure Triage & RCA Engine:
|
|
17
|
+
- Isolates exact failing Request Name, HTTP Method, and URL
|
|
18
|
+
- Highlights exact `pm.test` assertion name and Chai error details
|
|
19
|
+
- Distinguishes between HTTP 4xx/5xx crashes vs Schema / Assertion mismatches
|
|
20
|
+
4. CI/CD SLA Quality Gate:
|
|
21
|
+
- `--max-failures <int>`: Zero-tolerance or configurable failure threshold
|
|
22
|
+
- `--sla-avg <ms>`: Average latency SLA threshold
|
|
23
|
+
- Returns deterministic Exit Code 0 on PASS, 1 on FAIL
|
|
24
|
+
5. PR Reporting: Generates clean GitHub/GitLab Markdown reports and JSON telemetry.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
newman run collection.json -e env.json -r cli,json --reporter-json-export report.json
|
|
28
|
+
python tools/newman_analyzer.py report.json --max-failures 0 --sla-avg 500
|
|
29
|
+
agent-qa analyze-newman report.json --markdown pr_api_report.md
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import json
|
|
35
|
+
import argparse
|
|
36
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
37
|
+
|
|
38
|
+
# Enforce UTF-8 stdout encoding for Windows/macOS/Linux console compatibility
|
|
39
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
40
|
+
try:
|
|
41
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
42
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_newman_json(report_path: str) -> Dict[str, Any]:
|
|
48
|
+
"""Parses standard Newman JSON execution report."""
|
|
49
|
+
if not os.path.exists(report_path):
|
|
50
|
+
raise FileNotFoundError(f"Newman report file not found: {report_path}")
|
|
51
|
+
|
|
52
|
+
with open(report_path, "r", encoding="utf-8", errors="replace") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
|
|
55
|
+
run = data.get("run", {})
|
|
56
|
+
stats = run.get("stats", {})
|
|
57
|
+
timings = run.get("timings", {})
|
|
58
|
+
executions = run.get("executions", [])
|
|
59
|
+
failures = run.get("failures", [])
|
|
60
|
+
|
|
61
|
+
# Stats extraction
|
|
62
|
+
req_stats = stats.get("requests", {})
|
|
63
|
+
total_requests = req_stats.get("total", 0)
|
|
64
|
+
failed_requests = req_stats.get("failed", 0)
|
|
65
|
+
passed_requests = total_requests - failed_requests
|
|
66
|
+
|
|
67
|
+
assert_stats = stats.get("assertions", {})
|
|
68
|
+
total_assertions = assert_stats.get("total", 0)
|
|
69
|
+
failed_assertions = assert_stats.get("failed", 0)
|
|
70
|
+
passed_assertions = total_assertions - failed_assertions
|
|
71
|
+
|
|
72
|
+
# Latencies
|
|
73
|
+
response_times = run.get("timings", {}).get("responseAverage", 0)
|
|
74
|
+
min_time = run.get("timings", {}).get("responseMin", 0)
|
|
75
|
+
max_time = run.get("timings", {}).get("responseMax", 0)
|
|
76
|
+
|
|
77
|
+
# Detailed Request Breakdown
|
|
78
|
+
request_rows: List[Dict[str, Any]] = []
|
|
79
|
+
for ex in executions:
|
|
80
|
+
item = ex.get("item", {})
|
|
81
|
+
req = ex.get("request", {})
|
|
82
|
+
resp = ex.get("response", {})
|
|
83
|
+
|
|
84
|
+
name = item.get("name", "Unknown Request")
|
|
85
|
+
method = req.get("method", "GET")
|
|
86
|
+
url_obj = req.get("url", {})
|
|
87
|
+
url_str = url_obj if isinstance(url_obj, str) else url_obj.get("raw", "/")
|
|
88
|
+
status_code = resp.get("code", 0) if resp else 0
|
|
89
|
+
resp_time = resp.get("responseTime", 0) if resp else 0
|
|
90
|
+
|
|
91
|
+
# Assertions inside this execution
|
|
92
|
+
assertions_list = ex.get("assertions", [])
|
|
93
|
+
ex_assert_total = len(assertions_list)
|
|
94
|
+
ex_assert_failed = sum(1 for a in assertions_list if a.get("error"))
|
|
95
|
+
|
|
96
|
+
request_rows.append({
|
|
97
|
+
"name": name,
|
|
98
|
+
"method": method,
|
|
99
|
+
"url": url_str,
|
|
100
|
+
"status_code": status_code,
|
|
101
|
+
"response_time_ms": resp_time,
|
|
102
|
+
"assertions_total": ex_assert_total,
|
|
103
|
+
"assertions_failed": ex_assert_failed,
|
|
104
|
+
"passed": (ex_assert_failed == 0 and status_code < 400 and status_code > 0)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# Triage Failures
|
|
108
|
+
triaged_failures: List[Dict[str, str]] = []
|
|
109
|
+
for f in failures:
|
|
110
|
+
err = f.get("error", {})
|
|
111
|
+
source = f.get("source", {})
|
|
112
|
+
cursor = f.get("cursor", {})
|
|
113
|
+
|
|
114
|
+
req_name = source.get("name", cursor.get("ref", "Request"))
|
|
115
|
+
assert_name = err.get("test", err.get("name", "Assertion Failure"))
|
|
116
|
+
message = err.get("message", "Unknown assertion error")
|
|
117
|
+
|
|
118
|
+
triaged_failures.append({
|
|
119
|
+
"request_name": req_name,
|
|
120
|
+
"assertion_name": assert_name,
|
|
121
|
+
"message": message
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
error_rate = (failed_requests / total_requests * 100.0) if total_requests > 0 else 0.0
|
|
125
|
+
pass_ratio = (passed_assertions / total_assertions * 100.0) if total_assertions > 0 else 100.0
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"collection_name": data.get("collection", {}).get("info", {}).get("name", "Postman Collection"),
|
|
129
|
+
"total_requests": total_requests,
|
|
130
|
+
"failed_requests": failed_requests,
|
|
131
|
+
"passed_requests": passed_requests,
|
|
132
|
+
"error_rate_pct": round(error_rate, 2),
|
|
133
|
+
"total_assertions": total_assertions,
|
|
134
|
+
"failed_assertions": failed_assertions,
|
|
135
|
+
"passed_assertions": passed_assertions,
|
|
136
|
+
"assertion_pass_ratio_pct": round(pass_ratio, 2),
|
|
137
|
+
"latency": {
|
|
138
|
+
"avg_ms": round(float(response_times), 2),
|
|
139
|
+
"min_ms": round(float(min_time), 2),
|
|
140
|
+
"max_ms": round(float(max_time), 2)
|
|
141
|
+
},
|
|
142
|
+
"requests": request_rows,
|
|
143
|
+
"failures": triaged_failures
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def evaluate_newman_sla(
|
|
148
|
+
summary: Dict[str, Any],
|
|
149
|
+
max_failures: int = 0,
|
|
150
|
+
sla_avg: Optional[float] = None
|
|
151
|
+
) -> Tuple[bool, List[str]]:
|
|
152
|
+
"""Evaluates computed Newman summary against CI/CD SLA gates."""
|
|
153
|
+
violations: List[str] = []
|
|
154
|
+
|
|
155
|
+
total_failed = summary["failed_requests"] + summary["failed_assertions"]
|
|
156
|
+
if total_failed > max_failures:
|
|
157
|
+
violations.append(f"Failed Tests SLA Breached: {total_failed} failures > allowable limit ({max_failures})")
|
|
158
|
+
|
|
159
|
+
avg_latency = summary["latency"]["avg_ms"]
|
|
160
|
+
if sla_avg is not None and avg_latency > sla_avg:
|
|
161
|
+
violations.append(f"Average Latency SLA Breached: {avg_latency}ms > {sla_avg}ms threshold")
|
|
162
|
+
|
|
163
|
+
passed = (len(violations) == 0)
|
|
164
|
+
return passed, violations
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def format_cli_table(summary: Dict[str, Any]) -> str:
|
|
168
|
+
"""Formats requests into a clean CLI table."""
|
|
169
|
+
headers = ["Method", "Request Name", "Status", "Latency", "Asserts (Pass/Fail)"]
|
|
170
|
+
col_w = [8, 36, 8, 12, 20]
|
|
171
|
+
|
|
172
|
+
lines = []
|
|
173
|
+
header_str = " | ".join(h.ljust(col_w[i]) for i, h in enumerate(headers))
|
|
174
|
+
sep_str = "-+-".join("-" * col_w[i] for i in range(len(headers)))
|
|
175
|
+
|
|
176
|
+
lines.append(header_str)
|
|
177
|
+
lines.append(sep_str)
|
|
178
|
+
|
|
179
|
+
for r in summary["requests"]:
|
|
180
|
+
req_name = r["name"]
|
|
181
|
+
if len(req_name) > 34:
|
|
182
|
+
req_name = req_name[:31] + "..."
|
|
183
|
+
assert_info = f"{r['assertions_total'] - r['assertions_failed']} / {r['assertions_total']}"
|
|
184
|
+
status_display = str(r["status_code"]) if r["status_code"] > 0 else "FAIL"
|
|
185
|
+
|
|
186
|
+
lines.append(" | ".join([
|
|
187
|
+
f"[{r['method']}]".ljust(col_w[0]),
|
|
188
|
+
req_name.ljust(col_w[1]),
|
|
189
|
+
status_display.center(col_w[2]),
|
|
190
|
+
f"{r['response_time_ms']} ms".rjust(col_w[3]),
|
|
191
|
+
assert_info.center(col_w[4])
|
|
192
|
+
]))
|
|
193
|
+
|
|
194
|
+
return "\n".join(lines)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def generate_markdown_report(summary: Dict[str, Any], passed: bool, violations: List[str]) -> str:
|
|
198
|
+
"""Generates Pull Request Markdown report."""
|
|
199
|
+
status_badge = "✅ **PASSED (100% QUALITY GATE MET)**" if passed else "❌ **FAILED (QUALITY GATE BREACHED)**"
|
|
200
|
+
|
|
201
|
+
lines = [
|
|
202
|
+
f"# 📬 Postman / Newman API Automated Test Report: `{summary['collection_name']}`",
|
|
203
|
+
"",
|
|
204
|
+
f"- **Quality Gate Status:** {status_badge}",
|
|
205
|
+
f"- **Total Requests Executed:** `{summary['total_requests']}` ({summary['failed_requests']} failed)",
|
|
206
|
+
f"- **Total Assertions Checked:** `{summary['total_assertions']}` ({summary['failed_assertions']} failed)",
|
|
207
|
+
f"- **Assertion Pass Ratio:** `{summary['assertion_pass_ratio_pct']}%`",
|
|
208
|
+
f"- **Average Response Latency:** `{summary['latency']['avg_ms']} ms` (Min: `{summary['latency']['min_ms']} ms`, Max: `{summary['latency']['max_ms']} ms`)",
|
|
209
|
+
"",
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
if violations:
|
|
213
|
+
lines.extend([
|
|
214
|
+
"### ⚠️ CI/CD Quality Gate Violations",
|
|
215
|
+
""
|
|
216
|
+
])
|
|
217
|
+
for v in violations:
|
|
218
|
+
lines.append(f"- 🔴 `{v}`")
|
|
219
|
+
lines.append("")
|
|
220
|
+
|
|
221
|
+
if summary["failures"]:
|
|
222
|
+
lines.extend([
|
|
223
|
+
"### 🔍 Failed Assertions Triage (Root Cause)",
|
|
224
|
+
"",
|
|
225
|
+
"| Request Name | Failed pm.test Assertion | Error Details |",
|
|
226
|
+
"| :--- | :--- | :--- |"
|
|
227
|
+
])
|
|
228
|
+
for f in summary["failures"]:
|
|
229
|
+
lines.append(f"| `{f['request_name']}` | **{f['assertion_name']}** | `{f['message']}` |")
|
|
230
|
+
lines.append("")
|
|
231
|
+
|
|
232
|
+
lines.extend([
|
|
233
|
+
"### 📊 Detailed Endpoint Execution Breakdown",
|
|
234
|
+
"",
|
|
235
|
+
"| Method | Request Name | Status Code | Latency | Assertions | Result |",
|
|
236
|
+
"| :---: | :--- | :---: | :---: | :---: | :---: |"
|
|
237
|
+
])
|
|
238
|
+
|
|
239
|
+
for r in summary["requests"]:
|
|
240
|
+
verdict = "✅ PASS" if r["passed"] else "❌ FAIL"
|
|
241
|
+
lines.append(f"| `{r['method']}` | {r['name']} | `{r['status_code']}` | `{r['response_time_ms']} ms` | `{r['assertions_total'] - r['assertions_failed']}/{r['assertions_total']}` | {verdict} |")
|
|
242
|
+
|
|
243
|
+
lines.extend([
|
|
244
|
+
"",
|
|
245
|
+
"---",
|
|
246
|
+
"*Report auto-generated by Synapse-Hub `newman_analyzer.py` - Enterprise API Quality Gate.*"
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main():
|
|
253
|
+
parser = argparse.ArgumentParser(
|
|
254
|
+
description="Postman / Newman Test Results Analyzer & CI/CD Quality Gatekeeper."
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument("report_file", nargs="?", help="Path to Newman JSON report file (--reporters json).")
|
|
257
|
+
parser.add_argument("--max-failures", type=int, default=0, help="Allowable failed requests / assertions threshold. Default: 0.")
|
|
258
|
+
parser.add_argument("--sla-avg", type=float, default=None, help="Average Latency threshold in ms (fails if exceeded).")
|
|
259
|
+
parser.add_argument("--markdown", "-m", help="Path to export Markdown report.")
|
|
260
|
+
parser.add_argument("--json", "-j", help="Path to export JSON summary.")
|
|
261
|
+
parser.add_argument("--sample", action="store_true", help="Generate and analyze a mock Newman test dataset.")
|
|
262
|
+
|
|
263
|
+
args = parser.parse_args()
|
|
264
|
+
|
|
265
|
+
if args.sample or not args.report_file:
|
|
266
|
+
import tempfile
|
|
267
|
+
temp_file = os.path.join(tempfile.gettempdir(), "sample_newman_report.json")
|
|
268
|
+
sample_data = {
|
|
269
|
+
"collection": {"info": {"name": "Enterprise Banking & Billing API"}},
|
|
270
|
+
"run": {
|
|
271
|
+
"stats": {
|
|
272
|
+
"requests": {"total": 4, "failed": 0},
|
|
273
|
+
"assertions": {"total": 12, "failed": 1}
|
|
274
|
+
},
|
|
275
|
+
"timings": {
|
|
276
|
+
"responseAverage": 185.5,
|
|
277
|
+
"responseMin": 120.0,
|
|
278
|
+
"responseMax": 340.0
|
|
279
|
+
},
|
|
280
|
+
"executions": [
|
|
281
|
+
{
|
|
282
|
+
"item": {"name": "1. User Authentication (Login)"},
|
|
283
|
+
"request": {"method": "POST", "url": {"raw": "https://api.staging.enterprise.local/api/v1/auth/login"}},
|
|
284
|
+
"response": {"code": 200, "responseTime": 145},
|
|
285
|
+
"assertions": [
|
|
286
|
+
{"assertion": "Status code is 200", "error": None},
|
|
287
|
+
{"assertion": "Response time is below 500ms", "error": None},
|
|
288
|
+
{"assertion": "Response matches OpenAPI JSON Schema", "error": None}
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"item": {"name": "2. Get Account Balance"},
|
|
293
|
+
"request": {"method": "GET", "url": {"raw": "https://api.staging.enterprise.local/api/v1/accounts/me"}},
|
|
294
|
+
"response": {"code": 200, "responseTime": 120},
|
|
295
|
+
"assertions": [
|
|
296
|
+
{"assertion": "Status code is 200", "error": None},
|
|
297
|
+
{"assertion": "Response matches OpenAPI JSON Schema", "error": None}
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"item": {"name": "3. Transfer Funds (Dynamic Idempotency)"},
|
|
302
|
+
"request": {"method": "POST", "url": {"raw": "https://api.staging.enterprise.local/api/v1/transfers"}},
|
|
303
|
+
"response": {"code": 201, "responseTime": 340},
|
|
304
|
+
"assertions": [
|
|
305
|
+
{"assertion": "Status code is 201", "error": None},
|
|
306
|
+
{"assertion": "Response matches OpenAPI JSON Schema", "error": None},
|
|
307
|
+
{"assertion": "Transfer status is COMPLETED", "error": {"message": "expected 'PENDING' to equal 'COMPLETED'"}}
|
|
308
|
+
]
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"item": {"name": "4. Audit Transaction Log"},
|
|
312
|
+
"request": {"method": "GET", "url": {"raw": "https://api.staging.enterprise.local/api/v1/transfers/last"}},
|
|
313
|
+
"response": {"code": 200, "responseTime": 137},
|
|
314
|
+
"assertions": [
|
|
315
|
+
{"assertion": "Status code is 200", "error": None},
|
|
316
|
+
{"assertion": "Response matches OpenAPI JSON Schema", "error": None}
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"failures": [
|
|
321
|
+
{
|
|
322
|
+
"source": {"name": "3. Transfer Funds (Dynamic Idempotency)"},
|
|
323
|
+
"error": {
|
|
324
|
+
"test": "Transfer status is COMPLETED",
|
|
325
|
+
"message": "expected 'PENDING' to equal 'COMPLETED'"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
332
|
+
json.dump(sample_data, f, indent=2)
|
|
333
|
+
args.report_file = temp_file
|
|
334
|
+
print(f"🔬 [SAMPLE MODE] Generated synthetic Newman report: {temp_file}")
|
|
335
|
+
|
|
336
|
+
summary = parse_newman_json(args.report_file)
|
|
337
|
+
passed, violations = evaluate_newman_sla(
|
|
338
|
+
summary,
|
|
339
|
+
max_failures=args.max_failures,
|
|
340
|
+
sla_avg=args.sla_avg
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
print("\n" + "=" * 90)
|
|
344
|
+
print(f" POSTMAN / NEWMAN API AUTOMATED TEST MATRIX: {summary['collection_name']}")
|
|
345
|
+
print("=" * 90)
|
|
346
|
+
print(format_cli_table(summary))
|
|
347
|
+
print("=" * 90)
|
|
348
|
+
print(f"Total Requests : {summary['total_requests']} (Passed: {summary['passed_requests']} | Failed: {summary['failed_requests']})")
|
|
349
|
+
print(f"Total Assertions : {summary['total_assertions']} (Passed: {summary['passed_assertions']} | Failed: {summary['failed_assertions']})")
|
|
350
|
+
print(f"Pass Ratio : {summary['assertion_pass_ratio_pct']}% | Avg Latency: {summary['latency']['avg_ms']} ms")
|
|
351
|
+
print("-" * 90)
|
|
352
|
+
|
|
353
|
+
# Print SLA Verdict
|
|
354
|
+
print(f"🎯 [QUALITY GATE SLA VERDICT]")
|
|
355
|
+
if passed:
|
|
356
|
+
print("✅ ALL API FUNCTIONAL & SLA GATES PASSED! Safe to merge/deploy.")
|
|
357
|
+
else:
|
|
358
|
+
print("❌ API QUALITY GATE BREACHED:")
|
|
359
|
+
for v in violations:
|
|
360
|
+
print(f" 🔴 {v}")
|
|
361
|
+
if summary["failures"]:
|
|
362
|
+
print("\n🔍 Failing Assertions Breakdown:")
|
|
363
|
+
for f in summary["failures"]:
|
|
364
|
+
print(f" • [{f['request_name']}] -> {f['assertion_name']}: {f['message']}")
|
|
365
|
+
|
|
366
|
+
# Export Markdown
|
|
367
|
+
if args.markdown:
|
|
368
|
+
md = generate_markdown_report(summary, passed, violations)
|
|
369
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.markdown)) or ".", exist_ok=True)
|
|
370
|
+
with open(args.markdown, "w", encoding="utf-8") as f:
|
|
371
|
+
f.write(md)
|
|
372
|
+
print(f"\n📄 Markdown Report exported: {args.markdown}")
|
|
373
|
+
|
|
374
|
+
# Export JSON
|
|
375
|
+
if args.json:
|
|
376
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.json)) or ".", exist_ok=True)
|
|
377
|
+
with open(args.json, "w", encoding="utf-8") as f:
|
|
378
|
+
json.dump(summary, f, indent=2)
|
|
379
|
+
print(f"📊 JSON Metrics exported: {args.json}")
|
|
380
|
+
|
|
381
|
+
sys.exit(0 if passed else 1)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
main()
|
package/tools/setup_project.py
CHANGED
|
@@ -8,8 +8,10 @@ Dispatched automatically by AI Agents (Zero-Command Protocol) or via 'agent-qa s
|
|
|
8
8
|
Automated actions:
|
|
9
9
|
1. Detects project tech stack (Katalon, Playwright, Cypress, Pytest).
|
|
10
10
|
2. Generates tailored 'PROJECT_AI_GUIDE.md' if missing.
|
|
11
|
-
3.
|
|
12
|
-
4.
|
|
11
|
+
3. Deploys Universal AI Bridges (.cursor/rules, CLAUDE.md, .github/copilot-instructions.md, .windsurfrules).
|
|
12
|
+
4. Injects Antigravity Global Plugin (~/.gemini/config/plugins/synapse-hub).
|
|
13
|
+
5. Installs automated Git Pre-Push Hook (.git/hooks/pre-push) for zero-leakage protection.
|
|
14
|
+
6. Triggers 5-in-1 Health Audit (tools/analyze_project.py):
|
|
13
15
|
- Maps AST method signatures (repo_map.txt).
|
|
14
16
|
- Scans fake passes (infer_linter).
|
|
15
17
|
- Audits database teardown hygiene (trace_data_flow).
|
|
@@ -54,17 +56,29 @@ def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
|
54
56
|
resolved = os.path.abspath(chosen)
|
|
55
57
|
if not os.path.exists(resolved):
|
|
56
58
|
raise FileNotFoundError(f"Target repository path not found: {resolved}")
|
|
59
|
+
if resolved.lower() == HUB_ROOT.lower():
|
|
60
|
+
print(f"{RED}{BOLD}X [CWD CONFUSION GUARD] Target repository cannot be Synapse-Hub itself!{RESET}")
|
|
61
|
+
print(" Synapse-Hub is the centralized skill hub, not an automation target project.")
|
|
62
|
+
print(" Please specify a target project: python tools/setup_project.py --target /path/to/target-project")
|
|
63
|
+
sys.exit(1)
|
|
57
64
|
return resolved
|
|
58
65
|
|
|
59
66
|
env_dir = os.environ.get("TARGET_PROJECT_DIR")
|
|
60
67
|
if env_dir and os.path.exists(env_dir):
|
|
61
|
-
|
|
68
|
+
resolved_env = os.path.abspath(env_dir)
|
|
69
|
+
if resolved_env.lower() == HUB_ROOT.lower():
|
|
70
|
+
print(f"{RED}{BOLD}X [CWD CONFUSION GUARD] TARGET_PROJECT_DIR points to Synapse-Hub itself!{RESET}")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
return resolved_env
|
|
62
73
|
|
|
63
74
|
cwd = os.path.abspath(os.getcwd())
|
|
64
75
|
if cwd.lower() != HUB_ROOT.lower():
|
|
65
76
|
return cwd
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
print(f"{RED}{BOLD}X [CWD CONFUSION GUARD] Current working directory is Synapse-Hub!{RESET}")
|
|
79
|
+
print(" You must run setup inside your target project folder or supply --target:")
|
|
80
|
+
print(" python tools/setup_project.py --target <path_to_target_project>")
|
|
81
|
+
sys.exit(1)
|
|
68
82
|
|
|
69
83
|
|
|
70
84
|
def detect_tech_stack(target_dir):
|
|
@@ -175,19 +189,27 @@ def install_git_pre_push_hook(target_dir):
|
|
|
175
189
|
analyze_script = os.path.join(SCRIPT_DIR, "analyze_project.py").replace("\\", "/")
|
|
176
190
|
|
|
177
191
|
hook_content = f"""#!/bin/sh
|
|
178
|
-
# Automated Zero-Leakage Pre-Push Hook (Installed by
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
echo "\\n[AI-GUARD] Running Pre-Push Verification (
|
|
182
|
-
|
|
192
|
+
# Automated Zero-Leakage Pre-Push Hook (Installed by Synapse-Hub)
|
|
193
|
+
# Enforces 8 Quality Gates DoD and syncs project intelligence before pushing.
|
|
194
|
+
|
|
195
|
+
echo "\\n[AI-GUARD] Running Pre-Push Verification (8 Quality Gates DoD)..."
|
|
196
|
+
if command -v npx >/dev/null 2>&1; then
|
|
197
|
+
npx synapse-hub verify
|
|
198
|
+
elif [ -f "{verify_all_script}" ]; then
|
|
199
|
+
python3 "{verify_all_script}" || python "{verify_all_script}"
|
|
200
|
+
fi
|
|
183
201
|
if [ $? -ne 0 ]; then
|
|
184
|
-
echo "\\n[AI-GUARD - REJECTED] Git Push aborted:
|
|
185
|
-
echo "Please fix defects or run '
|
|
202
|
+
echo "\\n[AI-GUARD - REJECTED] Git Push aborted: Quality Gates verification failed!"
|
|
203
|
+
echo "Please fix defects or run 'npx synapse-hub verify' locally before pushing.\\n"
|
|
186
204
|
exit 1
|
|
187
205
|
fi
|
|
188
206
|
|
|
189
|
-
echo "[AI-GUARD - PASSED]
|
|
190
|
-
|
|
207
|
+
echo "[AI-GUARD - PASSED] 8 Quality Gates satisfied. Syncing project health report..."
|
|
208
|
+
if command -v npx >/dev/null 2>&1; then
|
|
209
|
+
npx synapse-hub analyze
|
|
210
|
+
elif [ -f "{analyze_script}" ]; then
|
|
211
|
+
python3 "{analyze_script}" || python "{analyze_script}"
|
|
212
|
+
fi
|
|
191
213
|
echo "[AI-GUARD - READY] Pushing to remote...\\n"
|
|
192
214
|
exit 0
|
|
193
215
|
"""
|
|
@@ -208,6 +230,151 @@ exit 0
|
|
|
208
230
|
return False
|
|
209
231
|
|
|
210
232
|
|
|
233
|
+
def inject_antigravity_global_plugin():
|
|
234
|
+
"""
|
|
235
|
+
Automatically injects Synapse-Hub into Antigravity's Global Plugin Directory
|
|
236
|
+
(~/.gemini/config/plugins/synapse-hub).
|
|
237
|
+
Ensures that running `npx synapse-hub setup` activates the 27 skills and 6 subagents
|
|
238
|
+
globally across ALL projects in Google Antigravity.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
plugins_dir = os.path.expanduser("~/.gemini/config/plugins")
|
|
242
|
+
target_plugin_dir = os.path.join(plugins_dir, "synapse-hub")
|
|
243
|
+
os.makedirs(target_plugin_dir, exist_ok=True)
|
|
244
|
+
|
|
245
|
+
# 1. plugin.json
|
|
246
|
+
src_plugin_json = os.path.join(HUB_ROOT, "plugin.json")
|
|
247
|
+
dst_plugin_json = os.path.join(target_plugin_dir, "plugin.json")
|
|
248
|
+
if os.path.exists(src_plugin_json):
|
|
249
|
+
if not os.path.exists(dst_plugin_json) or not os.path.samefile(src_plugin_json, dst_plugin_json):
|
|
250
|
+
shutil.copy2(src_plugin_json, dst_plugin_json)
|
|
251
|
+
|
|
252
|
+
# 2. README.md
|
|
253
|
+
src_readme = os.path.join(HUB_ROOT, "README.md")
|
|
254
|
+
dst_readme = os.path.join(target_plugin_dir, "README.md")
|
|
255
|
+
if os.path.exists(src_readme):
|
|
256
|
+
if not os.path.exists(dst_readme) or not os.path.samefile(src_readme, dst_readme):
|
|
257
|
+
shutil.copy2(src_readme, dst_readme)
|
|
258
|
+
|
|
259
|
+
# Helper to sync directory (junction or copy)
|
|
260
|
+
def sync_dir(src, dst):
|
|
261
|
+
if not os.path.exists(src):
|
|
262
|
+
return
|
|
263
|
+
if os.path.exists(dst) and os.path.samefile(src, dst):
|
|
264
|
+
return
|
|
265
|
+
if os.path.islink(dst):
|
|
266
|
+
try:
|
|
267
|
+
os.unlink(dst)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
if sys.platform == "win32":
|
|
271
|
+
try:
|
|
272
|
+
import _winapi
|
|
273
|
+
if not os.path.exists(dst):
|
|
274
|
+
_winapi.CreateJunction(src, dst)
|
|
275
|
+
return
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
shutil.copytree(src, dst, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.tmp"))
|
|
279
|
+
|
|
280
|
+
sync_dir(os.path.join(HUB_ROOT, ".agents", "skills"), os.path.join(target_plugin_dir, "skills"))
|
|
281
|
+
sync_dir(os.path.join(HUB_ROOT, ".agents", "agents"), os.path.join(target_plugin_dir, "agents"))
|
|
282
|
+
sync_dir(os.path.join(HUB_ROOT, "tools"), os.path.join(target_plugin_dir, "tools"))
|
|
283
|
+
|
|
284
|
+
# 3. Rules (AGENTS.md)
|
|
285
|
+
dst_rules = os.path.join(target_plugin_dir, "rules")
|
|
286
|
+
os.makedirs(dst_rules, exist_ok=True)
|
|
287
|
+
src_agents_md = os.path.join(HUB_ROOT, "AGENTS.md")
|
|
288
|
+
dst_agents_md = os.path.join(dst_rules, "AGENTS.md")
|
|
289
|
+
if os.path.exists(src_agents_md):
|
|
290
|
+
if not os.path.exists(dst_agents_md) or not os.path.samefile(src_agents_md, dst_agents_md):
|
|
291
|
+
shutil.copy2(src_agents_md, dst_agents_md)
|
|
292
|
+
|
|
293
|
+
print(f" {GREEN}[OK] Injected Antigravity Global Plugin:{RESET} {target_plugin_dir}")
|
|
294
|
+
return True
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print(f" ⚠️ Antigravity Global Plugin notice: {e}")
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def install_universal_ai_bridges(target_dir):
|
|
301
|
+
"""
|
|
302
|
+
Deploys Universal AI Bridge configuration files into the target project workspace
|
|
303
|
+
for seamless integration with Cursor, Claude Code, GitHub Copilot, and Windsurf.
|
|
304
|
+
"""
|
|
305
|
+
bridges_dir = os.path.join(TEMPLATES_DIR, "ai_bridges")
|
|
306
|
+
if not os.path.exists(bridges_dir):
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
deployed = []
|
|
310
|
+
|
|
311
|
+
# 1. Cursor (.cursor/rules/synapse-qa.mdc)
|
|
312
|
+
cursor_dir = os.path.join(target_dir, ".cursor", "rules")
|
|
313
|
+
os.makedirs(cursor_dir, exist_ok=True)
|
|
314
|
+
src_cursor = os.path.join(bridges_dir, "cursor_synapse_qa.mdc")
|
|
315
|
+
dst_cursor = os.path.join(cursor_dir, "synapse-qa.mdc")
|
|
316
|
+
if os.path.exists(src_cursor):
|
|
317
|
+
shutil.copy2(src_cursor, dst_cursor)
|
|
318
|
+
deployed.append("Cursor (.cursor/rules/synapse-qa.mdc)")
|
|
319
|
+
|
|
320
|
+
# 2. Claude Code (CLAUDE.md)
|
|
321
|
+
dst_claude = os.path.join(target_dir, "CLAUDE.md")
|
|
322
|
+
src_claude = os.path.join(bridges_dir, "CLAUDE.md")
|
|
323
|
+
if os.path.exists(src_claude):
|
|
324
|
+
if not os.path.exists(dst_claude):
|
|
325
|
+
shutil.copy2(src_claude, dst_claude)
|
|
326
|
+
deployed.append("Claude Code (CLAUDE.md)")
|
|
327
|
+
else:
|
|
328
|
+
with open(dst_claude, "r", encoding="utf-8", errors="ignore") as f:
|
|
329
|
+
c_content = f.read()
|
|
330
|
+
if "Synapse-Hub" not in c_content:
|
|
331
|
+
with open(src_claude, "r", encoding="utf-8") as f:
|
|
332
|
+
append_data = f.read()
|
|
333
|
+
with open(dst_claude, "a", encoding="utf-8") as f:
|
|
334
|
+
f.write("\n\n" + append_data)
|
|
335
|
+
deployed.append("Claude Code (appended to CLAUDE.md)")
|
|
336
|
+
|
|
337
|
+
# 3. GitHub Copilot (.github/copilot-instructions.md)
|
|
338
|
+
github_dir = os.path.join(target_dir, ".github")
|
|
339
|
+
os.makedirs(github_dir, exist_ok=True)
|
|
340
|
+
dst_copilot = os.path.join(github_dir, "copilot-instructions.md")
|
|
341
|
+
src_copilot = os.path.join(bridges_dir, "copilot-instructions.md")
|
|
342
|
+
if os.path.exists(src_copilot):
|
|
343
|
+
if not os.path.exists(dst_copilot):
|
|
344
|
+
shutil.copy2(src_copilot, dst_copilot)
|
|
345
|
+
deployed.append("GitHub Copilot (.github/copilot-instructions.md)")
|
|
346
|
+
else:
|
|
347
|
+
with open(dst_copilot, "r", encoding="utf-8", errors="ignore") as f:
|
|
348
|
+
cp_content = f.read()
|
|
349
|
+
if "Synapse-Hub" not in cp_content:
|
|
350
|
+
with open(src_copilot, "r", encoding="utf-8") as f:
|
|
351
|
+
append_data = f.read()
|
|
352
|
+
with open(dst_copilot, "a", encoding="utf-8") as f:
|
|
353
|
+
f.write("\n\n" + append_data)
|
|
354
|
+
deployed.append("GitHub Copilot (appended to copilot-instructions.md)")
|
|
355
|
+
|
|
356
|
+
# 4. Windsurf (.windsurfrules)
|
|
357
|
+
dst_windsurf = os.path.join(target_dir, ".windsurfrules")
|
|
358
|
+
src_windsurf = os.path.join(bridges_dir, "windsurfrules")
|
|
359
|
+
if os.path.exists(src_windsurf):
|
|
360
|
+
if not os.path.exists(dst_windsurf):
|
|
361
|
+
shutil.copy2(src_windsurf, dst_windsurf)
|
|
362
|
+
deployed.append("Windsurf (.windsurfrules)")
|
|
363
|
+
else:
|
|
364
|
+
with open(dst_windsurf, "r", encoding="utf-8", errors="ignore") as f:
|
|
365
|
+
w_content = f.read()
|
|
366
|
+
if "Synapse-Hub" not in w_content:
|
|
367
|
+
with open(src_windsurf, "r", encoding="utf-8") as f:
|
|
368
|
+
append_data = f.read()
|
|
369
|
+
with open(dst_windsurf, "a", encoding="utf-8") as f:
|
|
370
|
+
f.write("\n\n" + append_data)
|
|
371
|
+
deployed.append("Windsurf (appended to .windsurfrules)")
|
|
372
|
+
|
|
373
|
+
if deployed:
|
|
374
|
+
print(f" {GREEN}[OK] Deployed Universal AI Bridges:{RESET} {', '.join(deployed)}")
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
211
378
|
def run_setup(target_repo):
|
|
212
379
|
"""Main execution entrypoint for automated project setup."""
|
|
213
380
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
@@ -221,16 +388,32 @@ def run_setup(target_repo):
|
|
|
221
388
|
print(f"📂 Structure Scope : Pages: `{stack_info['page_dir']}` | Tests: `{stack_info['test_dir']}`\n")
|
|
222
389
|
|
|
223
390
|
# 2. Ensure PROJECT_AI_GUIDE.md & PROJECT_STATE.md
|
|
224
|
-
print(f"{BOLD}[1/
|
|
391
|
+
print(f"{BOLD}[1/5] Checking Project AI Guide & State Anchor...{RESET}")
|
|
225
392
|
guide_created = ensure_project_ai_guide(target_repo, stack_info)
|
|
226
393
|
project_state.init_state(target_repo)
|
|
227
394
|
|
|
228
|
-
# 3.
|
|
229
|
-
print(f"\n{BOLD}[2/
|
|
395
|
+
# 3. Deploy Universal AI Bridges (Cursor, Claude Code, GitHub Copilot, Windsurf)
|
|
396
|
+
print(f"\n{BOLD}[2/5] Deploying Universal AI Bridges (Cursor, Claude, Copilot, Windsurf)...{RESET}")
|
|
397
|
+
bridges_deployed = install_universal_ai_bridges(target_repo)
|
|
398
|
+
|
|
399
|
+
# 4. Inject Antigravity Global Plugin
|
|
400
|
+
print(f"\n{BOLD}[3/5] Injecting Antigravity Global Plugin (~/.gemini/config/plugins)...{RESET}")
|
|
401
|
+
plugin_injected = inject_antigravity_global_plugin()
|
|
402
|
+
|
|
403
|
+
# 4.1 Initialize Central Agent Memory (SQLite Cache)
|
|
404
|
+
try:
|
|
405
|
+
import cache_resolver
|
|
406
|
+
cache_resolver.init_db(seed=True)
|
|
407
|
+
print(f" {GREEN}[OK] Initialized Central Agent Memory (SQLite):{RESET} ~/.agent_memory/global_cache.db")
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print(f" ⚠️ Could not initialize memory cache: {e}")
|
|
410
|
+
|
|
411
|
+
# 5. Install Pre-Push Git Hook
|
|
412
|
+
print(f"\n{BOLD}[4/5] Installing Zero-Leakage Git Pre-Push Hook...{RESET}")
|
|
230
413
|
hook_installed = install_git_pre_push_hook(target_repo)
|
|
231
414
|
|
|
232
|
-
#
|
|
233
|
-
print(f"\n{BOLD}[
|
|
415
|
+
# 6. Run 5-in-1 Comprehensive Health & AST Analysis
|
|
416
|
+
print(f"\n{BOLD}[5/5] Executing Initial Health Audit & AST Mapping...{RESET}")
|
|
234
417
|
analysis_code = analyze_project.run_full_analysis(target_repo)
|
|
235
418
|
|
|
236
419
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
@@ -238,6 +421,8 @@ def run_setup(target_repo):
|
|
|
238
421
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
239
422
|
print(f" Target Framework : {stack_info['framework']}")
|
|
240
423
|
print(f" AI Project Guide : {os.path.join(target_repo, 'PROJECT_AI_GUIDE.md')}")
|
|
424
|
+
print(f" Universal Bridges : Cursor (.cursor/rules), Claude Code, Copilot, Windsurf")
|
|
425
|
+
print(f" Antigravity Plugin : {'ACTIVE (~/.gemini/config/plugins/synapse-hub)' if plugin_injected else 'STANDALONE'}")
|
|
241
426
|
print(f" AST Method Map : {os.path.join(target_repo, 'repo_map.txt')}")
|
|
242
427
|
print(f" Health Report : {os.path.join(target_repo, 'PROJECT_HEALTH_REPORT.md')}")
|
|
243
428
|
print(f" Pre-Push Sentinel : {'ACTIVE (.git/hooks/pre-push)' if hook_installed else 'MANUAL (No .git folder)'}")
|