synapse-hub 2.0.1 → 2.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/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +8 -5
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +13 -0
- package/README.md +4 -4
- package/package.json +6 -2
- package/skills_manifest.json +260 -260
- package/tools/TOOLS_RUNBOOK.md +54 -3
- package/tools/api_fuzzer.py +468 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +211 -0
- package/tools/cache_resolver.py +128 -0
- package/tools/cli.py +41 -1
- package/tools/export_postman_collection.py +606 -0
- package/tools/flaky_detector.py +180 -14
- package/tools/generate_jmx_plan.py +486 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +56 -0
- package/tools/network_mock_shield.py +141 -0
- package/tools/repo_map.json +1 -1
- package/tools/repo_map.txt +1 -1
- package/tools/visual_som_helper.py +121 -9
package/tools/flaky_detector.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
2
3
|
"""
|
|
3
4
|
Enterprise Flaky Test Detector & Quarantine Sentinel
|
|
4
5
|
---------------------------------------------------
|
|
5
6
|
Executes a target test scenario N times (default: 5) or parses historical JUnit XML
|
|
6
|
-
reports to calculate the Flakiness Index, detect state flips,
|
|
7
|
+
reports to calculate the Flakiness Index, detect state flips, query persistent SQLite telemetry,
|
|
8
|
+
and automatically isolate unstable tests via source-code @quarantine annotation.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
Features:
|
|
11
|
+
1. Multi-Run Repeat Execution & Live State-Flip Detection
|
|
12
|
+
2. Multi-Day Historical Telemetry Query from memory_cache.db
|
|
13
|
+
3. Automatic Source Code Isolation (--isolate) across Groovy, TS, Python test scripts
|
|
14
|
+
4. Quarantine Board JSON & Allure/CI metadata export
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
import os
|
|
18
|
+
import re
|
|
14
19
|
import sys
|
|
15
20
|
import glob
|
|
16
21
|
import json
|
|
@@ -19,6 +24,7 @@ import argparse
|
|
|
19
24
|
import subprocess
|
|
20
25
|
import xml.etree.ElementTree as ET
|
|
21
26
|
from datetime import datetime
|
|
27
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
22
28
|
|
|
23
29
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
24
30
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -28,9 +34,19 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
28
34
|
except Exception:
|
|
29
35
|
pass
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
32
39
|
REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
|
|
33
40
|
|
|
41
|
+
if SCRIPT_DIR not in sys.path:
|
|
42
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from cache_resolver import record_test_telemetry, get_flaky_telemetry
|
|
46
|
+
except ImportError:
|
|
47
|
+
record_test_telemetry = None
|
|
48
|
+
get_flaky_telemetry = None
|
|
49
|
+
|
|
34
50
|
GREEN = "\033[92m"
|
|
35
51
|
RED = "\033[91m"
|
|
36
52
|
YELLOW = "\033[93m"
|
|
@@ -39,7 +55,7 @@ BOLD = "\033[1m"
|
|
|
39
55
|
RESET = "\033[0m"
|
|
40
56
|
|
|
41
57
|
|
|
42
|
-
def run_command(cmd, timeout=300):
|
|
58
|
+
def run_command(cmd: str, timeout: int = 300) -> Tuple[int, str, str, float]:
|
|
43
59
|
"""Executes a shell command and returns (returncode, stdout, stderr, elapsed_ms)."""
|
|
44
60
|
start = time.time()
|
|
45
61
|
try:
|
|
@@ -58,7 +74,7 @@ def run_command(cmd, timeout=300):
|
|
|
58
74
|
return -1, "", "Execution timed out", elapsed_ms
|
|
59
75
|
|
|
60
76
|
|
|
61
|
-
def calculate_flakiness(runs):
|
|
77
|
+
def calculate_flakiness(runs: List[bool]) -> Dict[str, Any]:
|
|
62
78
|
"""
|
|
63
79
|
Given a list of boolean statuses [True, False, True, ...],
|
|
64
80
|
calculates pass rate, flip count, and flakiness tier.
|
|
@@ -71,6 +87,7 @@ def calculate_flakiness(runs):
|
|
|
71
87
|
"failures": 0,
|
|
72
88
|
"pass_rate": 0.0,
|
|
73
89
|
"flips": 0,
|
|
90
|
+
"flaky_ratio": 0.0,
|
|
74
91
|
"status": "NO_DATA",
|
|
75
92
|
"quarantine": False
|
|
76
93
|
}
|
|
@@ -85,6 +102,8 @@ def calculate_flakiness(runs):
|
|
|
85
102
|
if runs[i] != runs[i + 1]:
|
|
86
103
|
flips += 1
|
|
87
104
|
|
|
105
|
+
flaky_ratio = round((flips / max(1, total - 1)), 3) if total > 1 else (0.0 if failures == 0 else 1.0)
|
|
106
|
+
|
|
88
107
|
if pass_rate == 100.0:
|
|
89
108
|
status = "STABLE"
|
|
90
109
|
quarantine = False
|
|
@@ -93,7 +112,7 @@ def calculate_flakiness(runs):
|
|
|
93
112
|
quarantine = False
|
|
94
113
|
elif pass_rate >= 80.0:
|
|
95
114
|
status = "MILD_FLAKE"
|
|
96
|
-
quarantine = (flips >= 2)
|
|
115
|
+
quarantine = (flips >= 2 or flaky_ratio >= 0.15)
|
|
97
116
|
else:
|
|
98
117
|
status = "SEVERE_FLAKE"
|
|
99
118
|
quarantine = True
|
|
@@ -104,14 +123,15 @@ def calculate_flakiness(runs):
|
|
|
104
123
|
"failures": failures,
|
|
105
124
|
"pass_rate": pass_rate,
|
|
106
125
|
"flips": flips,
|
|
126
|
+
"flaky_ratio": flaky_ratio,
|
|
107
127
|
"status": status,
|
|
108
128
|
"quarantine": quarantine
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
|
|
112
|
-
def parse_junit_reports(glob_pattern):
|
|
132
|
+
def parse_junit_reports(glob_pattern: str) -> Dict[str, List[bool]]:
|
|
113
133
|
"""Parses JUnit XML report files and aggregates pass/fail counts by test case."""
|
|
114
|
-
test_results = {}
|
|
134
|
+
test_results: Dict[str, List[bool]] = {}
|
|
115
135
|
files = glob.glob(glob_pattern, recursive=True)
|
|
116
136
|
if not files:
|
|
117
137
|
print(f"⚠️ No JUnit XML reports found matching: {glob_pattern}")
|
|
@@ -134,12 +154,14 @@ def parse_junit_reports(glob_pattern):
|
|
|
134
154
|
return test_results
|
|
135
155
|
|
|
136
156
|
|
|
137
|
-
def benchmark_live_command(cmd, iterations=5):
|
|
157
|
+
def benchmark_live_command(cmd: str, iterations: int = 5, test_id: Optional[str] = None) -> Dict[str, Any]:
|
|
138
158
|
"""Executes a live test command N times to observe flakiness in real time."""
|
|
159
|
+
effective_id = test_id or f"CMD_{abs(hash(cmd)) % 100000}"
|
|
139
160
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
140
161
|
print(f"{BOLD}{CYAN} ENTERPRISE FLAKY TEST SENTINEL (Multi-Run Verification) {RESET}")
|
|
141
162
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
142
163
|
print(f" Target Command : {cmd}")
|
|
164
|
+
print(f" Test Identifier : {effective_id}")
|
|
143
165
|
print(f" Repetition Count : {iterations} consecutive iterations")
|
|
144
166
|
print(f" Timestamp : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
145
167
|
|
|
@@ -152,10 +174,26 @@ def benchmark_live_command(cmd, iterations=5):
|
|
|
152
174
|
is_pass = (code == 0)
|
|
153
175
|
runs.append(is_pass)
|
|
154
176
|
latencies.append(ms)
|
|
177
|
+
|
|
178
|
+
# Record to SQLite historical telemetry
|
|
179
|
+
if record_test_telemetry:
|
|
180
|
+
try:
|
|
181
|
+
record_test_telemetry(
|
|
182
|
+
test_id=effective_id,
|
|
183
|
+
status="PASS" if is_pass else "FAIL",
|
|
184
|
+
is_retry=1 if i > 1 else 0,
|
|
185
|
+
duration_ms=ms,
|
|
186
|
+
error_signature="NON_ZERO_EXIT" if not is_pass else "",
|
|
187
|
+
context=cmd
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
155
192
|
status_str = f"{GREEN}PASS{RESET}" if is_pass else f"{RED}FAIL (Exit {code}){RESET}"
|
|
156
193
|
print(f"{status_str} ({ms} ms)")
|
|
157
194
|
|
|
158
195
|
stats = calculate_flakiness(runs)
|
|
196
|
+
stats["test_id"] = effective_id
|
|
159
197
|
stats["latencies_ms"] = latencies
|
|
160
198
|
stats["avg_latency_ms"] = round(sum(latencies) / max(1, len(latencies)), 2)
|
|
161
199
|
stats["command"] = cmd
|
|
@@ -166,9 +204,10 @@ def benchmark_live_command(cmd, iterations=5):
|
|
|
166
204
|
print(f"{BOLD}======================================================================{RESET}")
|
|
167
205
|
print(f" Pass Rate : {stats['pass_rate']}% ({stats['passes']}/{stats['total']} passes)")
|
|
168
206
|
print(f" Status Flips : {stats['flips']} transitions")
|
|
207
|
+
print(f" Flaky Ratio : {stats['flaky_ratio'] * 100:.1f}%")
|
|
169
208
|
print(f" Average Runtime : {stats['avg_latency_ms']} ms")
|
|
170
209
|
print(f" Stability Status : {stats['status']}")
|
|
171
|
-
|
|
210
|
+
|
|
172
211
|
if stats["quarantine"]:
|
|
173
212
|
print(f" Recommendation : {RED}{BOLD}[QUARANTINE REQUIRED]{RESET} Tag with @quarantine immediately!")
|
|
174
213
|
else:
|
|
@@ -185,22 +224,146 @@ def benchmark_live_command(cmd, iterations=5):
|
|
|
185
224
|
return stats
|
|
186
225
|
|
|
187
226
|
|
|
227
|
+
def auto_quarantine_test_file(test_id: str, target_dir: str = ".") -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Searches repository test files and automatically annotates the flaky test method
|
|
230
|
+
with @quarantine or @tag("quarantine") or test.fixme().
|
|
231
|
+
"""
|
|
232
|
+
clean_id = test_id.split("#")[-1].strip()
|
|
233
|
+
annotated = False
|
|
234
|
+
|
|
235
|
+
for root, _, files in os.walk(target_dir):
|
|
236
|
+
# Skip node_modules and dot folders
|
|
237
|
+
if any(p in root for p in ("node_modules", ".git", "__pycache__", "reports")):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
for f in files:
|
|
241
|
+
ext = os.path.splitext(f)[1].lower()
|
|
242
|
+
if ext not in (".groovy", ".ts", ".js", ".py"):
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
file_path = os.path.join(root, f)
|
|
246
|
+
try:
|
|
247
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as fp:
|
|
248
|
+
content = fp.read()
|
|
249
|
+
|
|
250
|
+
# If test_id matches file or method inside file
|
|
251
|
+
if clean_id in content or f in test_id:
|
|
252
|
+
lines = content.splitlines(keepends=True)
|
|
253
|
+
new_lines = []
|
|
254
|
+
file_modified = False
|
|
255
|
+
|
|
256
|
+
for idx, line in enumerate(lines):
|
|
257
|
+
# Detect test declaration
|
|
258
|
+
if clean_id in line and not line.strip().startswith("//"):
|
|
259
|
+
# Check if already quarantined
|
|
260
|
+
if idx > 0 and "@quarantine" in lines[idx - 1]:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Inject quarantine tag based on language
|
|
264
|
+
indent = line[:len(line) - len(line.lstrip())]
|
|
265
|
+
if ext == ".groovy":
|
|
266
|
+
tag = f"{indent}// @quarantine - Auto-quarantined due to flakiness > 15%\n{indent}@Tag('quarantine')\n"
|
|
267
|
+
elif ext in (".ts", ".js"):
|
|
268
|
+
tag = f"{indent}// @quarantine - Auto-quarantined due to flakiness > 15%\n"
|
|
269
|
+
if "test(" in line:
|
|
270
|
+
line = line.replace("test(", "test.fixme(")
|
|
271
|
+
elif ext == ".py":
|
|
272
|
+
tag = f"{indent}@pytest.mark.quarantine\n"
|
|
273
|
+
else:
|
|
274
|
+
tag = f"{indent}// @quarantine\n"
|
|
275
|
+
|
|
276
|
+
new_lines.append(tag)
|
|
277
|
+
new_lines.append(line)
|
|
278
|
+
file_modified = True
|
|
279
|
+
annotated = True
|
|
280
|
+
else:
|
|
281
|
+
new_lines.append(line)
|
|
282
|
+
|
|
283
|
+
if file_modified:
|
|
284
|
+
with open(file_path, "w", encoding="utf-8") as fp:
|
|
285
|
+
fp.writelines(new_lines)
|
|
286
|
+
print(f"🔒 [QUARANTINE APPLIED] Injected quarantine annotation into: {file_path}")
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print(f"⚠️ Could not patch {file_path}: {e}")
|
|
289
|
+
|
|
290
|
+
return annotated
|
|
291
|
+
|
|
292
|
+
|
|
188
293
|
def main():
|
|
189
294
|
parser = argparse.ArgumentParser(description="Enterprise Flaky Test Detector & Quarantine Sentinel")
|
|
190
295
|
parser.add_argument("--cmd", "-c", default=None, help="Test command to execute repeatedly")
|
|
191
296
|
parser.add_argument("--repeat", "-r", type=int, default=5, help="Number of repetitions (default: 5)")
|
|
297
|
+
parser.add_argument("--test-id", default=None, help="Custom identifier for the test scenario")
|
|
192
298
|
parser.add_argument("--reports", default=None, help="Glob pattern for JUnit XML reports to analyze")
|
|
299
|
+
parser.add_argument("--history", action="store_true", help="Query 30-day historical flakiness from SQLite telemetry")
|
|
300
|
+
parser.add_argument("--days", type=int, default=30, help="Lookback window in days (default: 30)")
|
|
301
|
+
parser.add_argument("--threshold", type=float, default=0.15, help="Flaky ratio quarantine threshold (default: 0.15)")
|
|
302
|
+
parser.add_argument("--isolate", action="store_true", help="Automatically inject @quarantine tag into source files")
|
|
303
|
+
parser.add_argument("--target-dir", default=".", help="Root directory of test files to patch")
|
|
304
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
305
|
+
|
|
193
306
|
args = parser.parse_args()
|
|
194
307
|
|
|
195
|
-
if args.
|
|
196
|
-
|
|
308
|
+
if args.history:
|
|
309
|
+
if not get_flaky_telemetry:
|
|
310
|
+
print("❌ SQLite cache resolver not available.")
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
candidates = get_flaky_telemetry(days=args.days, threshold=args.threshold)
|
|
314
|
+
if args.json:
|
|
315
|
+
print(json.dumps(candidates, indent=2))
|
|
316
|
+
else:
|
|
317
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
318
|
+
print(f"{BOLD}{CYAN} HISTORICAL FLAKINESS TELEMETRY ({args.days}-DAY WINDOW) {RESET}")
|
|
319
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
320
|
+
print(f" Quarantine Threshold : {args.threshold * 100:.1f}% flaky ratio")
|
|
321
|
+
print(f" Total Candidates : {len(candidates)}\n")
|
|
322
|
+
|
|
323
|
+
quarantined_count = 0
|
|
324
|
+
for c in candidates:
|
|
325
|
+
is_q = c["quarantine_recommended"]
|
|
326
|
+
tag = f"{RED}[QUARANTINE]{RESET}" if is_q else f"{GREEN}[STABLE]{RESET}"
|
|
327
|
+
print(f" • {tag} {BOLD}{c['test_id']}{RESET}")
|
|
328
|
+
print(f" Runs: {c['total_runs']} | Passes: {c['pass_count']} | Fails: {c['fail_count']} | Retry Passes: {c['retry_pass_count']}")
|
|
329
|
+
print(f" Flaky Ratio: {c['flaky_ratio'] * 100:.1f}% | Avg Latency: {c['avg_duration_ms']} ms\n")
|
|
330
|
+
|
|
331
|
+
if is_q:
|
|
332
|
+
quarantined_count += 1
|
|
333
|
+
if args.isolate:
|
|
334
|
+
auto_quarantine_test_file(c["test_id"], target_dir=args.target_dir)
|
|
335
|
+
|
|
336
|
+
print(f"======================================================================")
|
|
337
|
+
print(f"Quarantine Recommendations: {quarantined_count}/{len(candidates)}")
|
|
338
|
+
sys.exit(0)
|
|
339
|
+
|
|
340
|
+
elif args.cmd:
|
|
341
|
+
stats = benchmark_live_command(args.cmd, iterations=args.repeat, test_id=args.test_id)
|
|
342
|
+
if stats["quarantine"] and args.isolate:
|
|
343
|
+
auto_quarantine_test_file(stats["test_id"], target_dir=args.target_dir)
|
|
197
344
|
sys.exit(1 if stats["quarantine"] else 0)
|
|
345
|
+
|
|
198
346
|
elif args.reports:
|
|
199
347
|
results = parse_junit_reports(args.reports)
|
|
200
348
|
quarantine_candidates = []
|
|
201
349
|
for test_id, runs in results.items():
|
|
202
350
|
st = calculate_flakiness(runs)
|
|
203
351
|
st["test_id"] = test_id
|
|
352
|
+
|
|
353
|
+
# Ingest into SQLite telemetry
|
|
354
|
+
if record_test_telemetry:
|
|
355
|
+
for idx, is_p in enumerate(runs):
|
|
356
|
+
try:
|
|
357
|
+
record_test_telemetry(
|
|
358
|
+
test_id=test_id,
|
|
359
|
+
status="PASS" if is_p else "FAIL",
|
|
360
|
+
is_retry=1 if idx > 0 else 0,
|
|
361
|
+
duration_ms=0.0,
|
|
362
|
+
context=args.reports
|
|
363
|
+
)
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
204
367
|
if st["quarantine"]:
|
|
205
368
|
quarantine_candidates.append(st)
|
|
206
369
|
|
|
@@ -208,11 +371,14 @@ def main():
|
|
|
208
371
|
print(f"Quarantine Candidates Detected: {len(quarantine_candidates)}")
|
|
209
372
|
for qc in quarantine_candidates:
|
|
210
373
|
print(f" - {RED}{qc['test_id']}{RESET}: Pass rate {qc['pass_rate']}%, Flips: {qc['flips']}")
|
|
374
|
+
if args.isolate:
|
|
375
|
+
auto_quarantine_test_file(qc["test_id"], target_dir=args.target_dir)
|
|
211
376
|
|
|
212
377
|
report_file = os.path.join(REPORTS_DIR, "quarantine_board.json")
|
|
213
378
|
with open(report_file, "w", encoding="utf-8") as fp:
|
|
214
379
|
json.dump(quarantine_candidates, fp, indent=2)
|
|
215
380
|
sys.exit(1 if quarantine_candidates else 0)
|
|
381
|
+
|
|
216
382
|
else:
|
|
217
383
|
parser.print_help()
|
|
218
384
|
sys.exit(0)
|