synapse-hub 2.0.0 → 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/git-push-protocol.md +1 -1
- 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 +172 -29
- package/package.json +9 -5
- 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 +128 -9
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
tools/bump_version.py — Atomic Universal Version Bumper for Synapse Hub
|
|
5
|
+
|
|
6
|
+
Synchronizes version numbers across all polyglot targets in a single command:
|
|
7
|
+
1. package.json (NPM)
|
|
8
|
+
2. pyproject.toml (Python/Poetry/PEP 621)
|
|
9
|
+
3. skills_manifest.json (Skill Catalog Index)
|
|
10
|
+
4. tools/cli.py (Agent-QA CLI)
|
|
11
|
+
5. tools/mcp_server.py (MCP Protocol Server)
|
|
12
|
+
6. tools/browser_helpers.js (In-Browser CDP Helper)
|
|
13
|
+
7. tools/mcp_excel_server.ps1 (PowerShell MCP Service)
|
|
14
|
+
8. docs/ARCHITECTURE.md (Architecture Specifications)
|
|
15
|
+
9. README.md (Bilingual Documentation & Badges)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python3 tools/bump_version.py patch # 2.0.1 -> 2.0.2
|
|
19
|
+
python3 tools/bump_version.py minor # 2.0.1 -> 2.1.0
|
|
20
|
+
python3 tools/bump_version.py major # 2.0.1 -> 3.0.0
|
|
21
|
+
python3 tools/bump_version.py 2.0.5 # Explicit version
|
|
22
|
+
python3 tools/bump_version.py patch --commit # Bump + git commit
|
|
23
|
+
python3 tools/bump_version.py patch --commit --push # Bump + commit + git push + tag
|
|
24
|
+
python3 tools/bump_version.py patch --dry-run # Preview only
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import json
|
|
31
|
+
import argparse
|
|
32
|
+
import subprocess
|
|
33
|
+
|
|
34
|
+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
35
|
+
|
|
36
|
+
def parse_semver(v_str: str):
|
|
37
|
+
m = re.match(r"^(\d+)\.(\d+)\.(\d+)(.*)$", v_str.strip())
|
|
38
|
+
if not m:
|
|
39
|
+
raise ValueError(f"Invalid semver: '{v_str}'. Must follow major.minor.patch[-suffix]")
|
|
40
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4)
|
|
41
|
+
|
|
42
|
+
def calculate_next_version(current: str, action: str) -> str:
|
|
43
|
+
major, minor, patch, suffix = parse_semver(current)
|
|
44
|
+
action = action.lower()
|
|
45
|
+
if action == "patch":
|
|
46
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
47
|
+
elif action == "minor":
|
|
48
|
+
return f"{major}.{minor + 1}.0"
|
|
49
|
+
elif action == "major":
|
|
50
|
+
return f"{major + 1}.0.0"
|
|
51
|
+
else:
|
|
52
|
+
# Validate that action itself is a valid semver
|
|
53
|
+
parse_semver(action)
|
|
54
|
+
return action
|
|
55
|
+
|
|
56
|
+
def get_current_version() -> str:
|
|
57
|
+
pkg_file = os.path.join(REPO_ROOT, "package.json")
|
|
58
|
+
with open(pkg_file, "r", encoding="utf-8") as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
return data.get("version", "2.0.0")
|
|
61
|
+
|
|
62
|
+
def update_json_root_version(rel_path: str, new_ver: str, dry_run: bool = False) -> bool:
|
|
63
|
+
full_path = os.path.join(REPO_ROOT, rel_path)
|
|
64
|
+
if not os.path.exists(full_path):
|
|
65
|
+
print(f" [WARN] File not found: {rel_path}")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
|
|
71
|
+
old_ver = data.get("version")
|
|
72
|
+
if old_ver == new_ver:
|
|
73
|
+
print(f" [SKIP] {rel_path} already at v{new_ver}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
data["version"] = new_ver
|
|
77
|
+
if not dry_run:
|
|
78
|
+
with open(full_path, "w", encoding="utf-8", newline="\n") as f:
|
|
79
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
80
|
+
f.write("\n")
|
|
81
|
+
print(f" [UPDATED] {rel_path} (root version {old_ver} -> {new_ver})")
|
|
82
|
+
else:
|
|
83
|
+
print(f" [DRY-RUN] Would update {rel_path} (root version {old_ver} -> {new_ver})")
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def update_file(rel_path: str, pattern: str, replacement: str, dry_run: bool = False) -> bool:
|
|
87
|
+
full_path = os.path.join(REPO_ROOT, rel_path)
|
|
88
|
+
if not os.path.exists(full_path):
|
|
89
|
+
print(f" [WARN] File not found: {rel_path}")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
|
|
95
|
+
new_content, count = re.subn(pattern, replacement, content)
|
|
96
|
+
if count == 0:
|
|
97
|
+
print(f" [SKIP] No pattern match in {rel_path}")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
if not dry_run:
|
|
101
|
+
with open(full_path, "w", encoding="utf-8", newline="\n") as f:
|
|
102
|
+
f.write(new_content)
|
|
103
|
+
print(f" [UPDATED] {rel_path} ({count} occurrence{'s' if count > 1 else ''})")
|
|
104
|
+
else:
|
|
105
|
+
print(f" [DRY-RUN] Would update {rel_path} ({count} occurrence{'s' if count > 1 else ''})")
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def run_tests() -> bool:
|
|
109
|
+
print("\n🔍 Running verification suite...")
|
|
110
|
+
cmd = [sys.executable, "-m", "unittest", "discover", "tests", "-p", "test_*.py"]
|
|
111
|
+
res = subprocess.run(cmd, cwd=REPO_ROOT)
|
|
112
|
+
if res.returncode != 0:
|
|
113
|
+
print("❌ Unit tests failed! Aborting release.")
|
|
114
|
+
return False
|
|
115
|
+
print("✅ All unit tests PASSED.")
|
|
116
|
+
|
|
117
|
+
verify_script = os.path.join(REPO_ROOT, "tools", "verify_all.py")
|
|
118
|
+
res_verify = subprocess.run([sys.executable, verify_script], cwd=REPO_ROOT)
|
|
119
|
+
if res_verify.returncode != 0:
|
|
120
|
+
print("❌ Quality Gates failed! Aborting release.")
|
|
121
|
+
return False
|
|
122
|
+
print("✅ 8 Quality Gates DoD PASSED.")
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
def git_commit_and_tag(version: str, push: bool = False):
|
|
126
|
+
print(f"\n📦 Creating Git commit and tag v{version}...")
|
|
127
|
+
subprocess.run(["git", "add", "-A"], cwd=REPO_ROOT, check=True)
|
|
128
|
+
subprocess.run(["git", "commit", "-m", f"chore(release): bump version to v{version}"], cwd=REPO_ROOT, check=True)
|
|
129
|
+
subprocess.run(["git", "tag", f"v{version}"], cwd=REPO_ROOT, check=True)
|
|
130
|
+
print(f"✅ Git commit and tag v{version} created.")
|
|
131
|
+
|
|
132
|
+
if push:
|
|
133
|
+
print(f"🚀 Pushing commit and tags to remote origin...")
|
|
134
|
+
subprocess.run(["git", "push", "origin", "HEAD", "--tags"], cwd=REPO_ROOT, check=True)
|
|
135
|
+
print(f"✅ Remote origin synchronized.")
|
|
136
|
+
|
|
137
|
+
def main():
|
|
138
|
+
parser = argparse.ArgumentParser(description="Atomic Universal Version Bumper for Synapse Hub")
|
|
139
|
+
parser.add_argument("action", nargs="?", default="patch", help="patch | minor | major | <explicit_version> (default: patch)")
|
|
140
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview version changes without writing files")
|
|
141
|
+
parser.add_argument("--no-test", action="store_true", help="Skip running test suite after bumping")
|
|
142
|
+
parser.add_argument("--commit", action="store_true", help="Auto-commit changes with conventional message")
|
|
143
|
+
parser.add_argument("--tag", action="store_true", help="Create Git tag v<version>")
|
|
144
|
+
parser.add_argument("--push", action="store_true", help="Push commit and tags to Git remote")
|
|
145
|
+
|
|
146
|
+
args = parser.parse_args()
|
|
147
|
+
|
|
148
|
+
current_ver = get_current_version()
|
|
149
|
+
new_ver = calculate_next_version(current_ver, args.action)
|
|
150
|
+
|
|
151
|
+
print("======================================================================")
|
|
152
|
+
print(" SYNAPSE HUB — ATOMIC UNIVERSAL VERSION BUMPER ")
|
|
153
|
+
print("======================================================================")
|
|
154
|
+
print(f" Current Version : {current_ver}")
|
|
155
|
+
print(f" Target Version : {new_ver} ({args.action})")
|
|
156
|
+
print(f" Mode : {'DRY RUN (Preview)' if args.dry_run else 'APPLY CHANGES'}")
|
|
157
|
+
print("======================================================================")
|
|
158
|
+
|
|
159
|
+
# 1. package.json
|
|
160
|
+
update_json_root_version("package.json", new_ver, args.dry_run)
|
|
161
|
+
|
|
162
|
+
# 2. pyproject.toml
|
|
163
|
+
update_file("pyproject.toml", r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
164
|
+
|
|
165
|
+
# 3. skills_manifest.json
|
|
166
|
+
update_json_root_version("skills_manifest.json", new_ver, args.dry_run)
|
|
167
|
+
|
|
168
|
+
# 4. tools/cli.py
|
|
169
|
+
update_file("tools/cli.py", r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
170
|
+
|
|
171
|
+
# 5. tools/mcp_server.py
|
|
172
|
+
update_file("tools/mcp_server.py", r'(SERVER_VERSION\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
173
|
+
|
|
174
|
+
# 6. tools/browser_helpers.js
|
|
175
|
+
update_file("tools/browser_helpers.js", r"(__version === ')[^']+(')", rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
176
|
+
update_file("tools/browser_helpers.js", r"(__version:\s*')[^']+(')", rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
177
|
+
|
|
178
|
+
# 7. tools/mcp_excel_server.ps1
|
|
179
|
+
update_file("tools/mcp_excel_server.ps1", r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
180
|
+
|
|
181
|
+
# 8. docs/ARCHITECTURE.md
|
|
182
|
+
update_file("docs/ARCHITECTURE.md", r'(>\s*\*\*Version:\*\*\s*`)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
183
|
+
|
|
184
|
+
# 9. README.md (Documentation headings and mermaid badges)
|
|
185
|
+
update_file("README.md", r'(Phiên bản Hub:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
186
|
+
update_file("README.md", r'(Hub Version:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
187
|
+
update_file("README.md", r'(Intelligence Core v)[^")\]]+', rf"\g<1>{new_ver}", args.dry_run)
|
|
188
|
+
|
|
189
|
+
if args.dry_run:
|
|
190
|
+
print("\n[INFO] Dry-run completed. No files modified on disk.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Tests verification
|
|
194
|
+
if not args.no_test:
|
|
195
|
+
if not run_tests():
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
# Git operations
|
|
199
|
+
if args.commit or args.push:
|
|
200
|
+
git_commit_and_tag(new_ver, push=args.push)
|
|
201
|
+
|
|
202
|
+
print("\n======================================================================")
|
|
203
|
+
print(f"🎉 VERSION BUMP COMPLETE: v{current_ver} ➔ v{new_ver}")
|
|
204
|
+
print("======================================================================")
|
|
205
|
+
print("Next steps:")
|
|
206
|
+
print(f" • Publish to NPM : npm publish")
|
|
207
|
+
print(f" • Or Git Push : git push origin HEAD --tags")
|
|
208
|
+
print("======================================================================")
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|
package/tools/cache_resolver.py
CHANGED
|
@@ -225,6 +225,22 @@ def init_db(seed=True, target_db=None):
|
|
|
225
225
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
226
226
|
)
|
|
227
227
|
""")
|
|
228
|
+
|
|
229
|
+
# 4. Flaky Test Historical Telemetry & Quarantine Matrix
|
|
230
|
+
cursor.execute("""
|
|
231
|
+
CREATE TABLE IF NOT EXISTS test_telemetry (
|
|
232
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
233
|
+
test_id TEXT NOT NULL,
|
|
234
|
+
run_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
235
|
+
status TEXT NOT NULL,
|
|
236
|
+
is_retry INTEGER DEFAULT 0,
|
|
237
|
+
duration_ms REAL DEFAULT 0.0,
|
|
238
|
+
error_signature TEXT DEFAULT '',
|
|
239
|
+
context TEXT DEFAULT ''
|
|
240
|
+
)
|
|
241
|
+
""")
|
|
242
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_telemetry_test_id ON test_telemetry (test_id)")
|
|
243
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_telemetry_timestamp ON test_telemetry (run_timestamp)")
|
|
228
244
|
conn.commit()
|
|
229
245
|
|
|
230
246
|
# Migrate existing solutions from local DB if initializing global DB for the first time
|
|
@@ -667,6 +683,72 @@ def query_lessons(context_or_keyword, limit=2):
|
|
|
667
683
|
return top_results
|
|
668
684
|
|
|
669
685
|
|
|
686
|
+
@retry_on_locked()
|
|
687
|
+
def record_test_telemetry(test_id, status, is_retry=0, duration_ms=0.0, error_signature="", context="", target_db=None):
|
|
688
|
+
"""
|
|
689
|
+
Records an individual test execution into persistent SQLite telemetry.
|
|
690
|
+
Used for computing 30-day historical flakiness index and auto-quarantine decisions.
|
|
691
|
+
"""
|
|
692
|
+
init_db(seed=True, target_db=target_db)
|
|
693
|
+
current_db = target_db or DB_FILE
|
|
694
|
+
with get_db(current_db) as conn:
|
|
695
|
+
cursor = conn.cursor()
|
|
696
|
+
cursor.execute("""
|
|
697
|
+
INSERT INTO test_telemetry (test_id, status, is_retry, duration_ms, error_signature, context)
|
|
698
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
699
|
+
""", (test_id, status.upper(), int(is_retry), float(duration_ms), error_signature, context))
|
|
700
|
+
conn.commit()
|
|
701
|
+
return True
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def get_flaky_telemetry(days=30, min_runs=2, threshold=0.15, target_db=None):
|
|
705
|
+
"""
|
|
706
|
+
Queries historical test telemetry and calculates the Flaky Ratio across the given window.
|
|
707
|
+
Flaky Ratio = (retry_pass_count + fail_then_pass_transitions) / total_runs.
|
|
708
|
+
"""
|
|
709
|
+
init_db(seed=True, target_db=target_db)
|
|
710
|
+
current_db = target_db or DB_FILE
|
|
711
|
+
with get_db(current_db) as conn:
|
|
712
|
+
cursor = conn.cursor()
|
|
713
|
+
cursor.execute("""
|
|
714
|
+
SELECT
|
|
715
|
+
test_id,
|
|
716
|
+
COUNT(*) as total_runs,
|
|
717
|
+
SUM(CASE WHEN status = 'PASS' THEN 1 ELSE 0 END) as pass_count,
|
|
718
|
+
SUM(CASE WHEN status = 'FAIL' THEN 1 ELSE 0 END) as fail_count,
|
|
719
|
+
SUM(CASE WHEN status = 'PASS' AND is_retry = 1 THEN 1 ELSE 0 END) as retry_pass_count,
|
|
720
|
+
AVG(duration_ms) as avg_duration_ms
|
|
721
|
+
FROM test_telemetry
|
|
722
|
+
WHERE run_timestamp >= datetime('now', '-' || ? || ' days')
|
|
723
|
+
GROUP BY test_id
|
|
724
|
+
HAVING total_runs >= ?
|
|
725
|
+
""", (days, min_runs))
|
|
726
|
+
rows = cursor.fetchall()
|
|
727
|
+
candidates = []
|
|
728
|
+
for r in rows:
|
|
729
|
+
total = r["total_runs"]
|
|
730
|
+
retry_passes = r["retry_pass_count"] or 0
|
|
731
|
+
fails = r["fail_count"] or 0
|
|
732
|
+
passes = r["pass_count"] or 0
|
|
733
|
+
# If test both passes and fails across runs, or passes on retry
|
|
734
|
+
flaky_events = retry_passes + (1 if fails > 0 and passes > 0 else 0)
|
|
735
|
+
flaky_ratio = round(min(1.0, flaky_events / max(1, total)), 3)
|
|
736
|
+
quarantine = flaky_ratio >= threshold and passes > 0
|
|
737
|
+
|
|
738
|
+
candidates.append({
|
|
739
|
+
"test_id": r["test_id"],
|
|
740
|
+
"total_runs": total,
|
|
741
|
+
"pass_count": passes,
|
|
742
|
+
"fail_count": fails,
|
|
743
|
+
"retry_pass_count": retry_passes,
|
|
744
|
+
"flaky_ratio": flaky_ratio,
|
|
745
|
+
"avg_duration_ms": round(r["avg_duration_ms"] or 0.0, 1),
|
|
746
|
+
"quarantine_recommended": quarantine
|
|
747
|
+
})
|
|
748
|
+
candidates.sort(key=lambda x: x["flaky_ratio"], reverse=True)
|
|
749
|
+
return candidates
|
|
750
|
+
|
|
751
|
+
|
|
670
752
|
def get_stats():
|
|
671
753
|
init_db(seed=True)
|
|
672
754
|
with get_db() as conn:
|
|
@@ -683,6 +765,9 @@ def get_stats():
|
|
|
683
765
|
cursor.execute("SELECT COUNT(*) as total_lessons FROM lessons_learned")
|
|
684
766
|
total_lessons = cursor.fetchone()["total_lessons"] or 0
|
|
685
767
|
|
|
768
|
+
cursor.execute("SELECT COUNT(*) as total_telemetry FROM test_telemetry")
|
|
769
|
+
total_telemetry = cursor.fetchone()["total_telemetry"] or 0
|
|
770
|
+
|
|
686
771
|
cursor.execute("SELECT error_signature, module_name, hit_count FROM healing_cache ORDER BY hit_count DESC")
|
|
687
772
|
top_solutions = [dict(r) for r in cursor.fetchall()]
|
|
688
773
|
|
|
@@ -691,6 +776,7 @@ def get_stats():
|
|
|
691
776
|
"total_hits": total_hits,
|
|
692
777
|
"total_failed_fixes": total_failed,
|
|
693
778
|
"total_lessons_learned": total_lessons,
|
|
779
|
+
"total_telemetry_records": total_telemetry,
|
|
694
780
|
"estimated_tokens_saved": saved_tokens,
|
|
695
781
|
"solutions": top_solutions
|
|
696
782
|
}
|
|
@@ -736,6 +822,21 @@ def main():
|
|
|
736
822
|
lesson_get_parser.add_argument("--context", required=True, help="Page, flow, or error keyword to match")
|
|
737
823
|
lesson_get_parser.add_argument("--limit", type=int, default=2, help="Number of lessons to retrieve (default: 2)")
|
|
738
824
|
|
|
825
|
+
# Record-Telemetry command (Historical Telemetry)
|
|
826
|
+
telem_record_parser = subparsers.add_parser("record-telemetry", help="Record individual test run outcome into telemetry")
|
|
827
|
+
telem_record_parser.add_argument("--test-id", required=True, help="Test case identifier or class#method")
|
|
828
|
+
telem_record_parser.add_argument("--status", required=True, choices=["PASS", "FAIL"], help="Execution status")
|
|
829
|
+
telem_record_parser.add_argument("--retry", type=int, default=0, help="Whether this run was an automated retry (1 or 0)")
|
|
830
|
+
telem_record_parser.add_argument("--duration", type=float, default=0.0, help="Run duration in ms")
|
|
831
|
+
telem_record_parser.add_argument("--error", default="", help="Error signature if failed")
|
|
832
|
+
telem_record_parser.add_argument("--context", default="", help="Environment or workflow context")
|
|
833
|
+
|
|
834
|
+
# Get-Flaky command (Historical Flakiness Query)
|
|
835
|
+
flaky_get_parser = subparsers.add_parser("get-flaky", help="Query historical flaky test candidates from telemetry")
|
|
836
|
+
flaky_get_parser.add_argument("--days", type=int, default=30, help="Lookback window in days (default: 30)")
|
|
837
|
+
flaky_get_parser.add_argument("--min-runs", type=int, default=2, help="Minimum runs required (default: 2)")
|
|
838
|
+
flaky_get_parser.add_argument("--threshold", type=float, default=0.15, help="Flaky ratio threshold for quarantine (default: 0.15)")
|
|
839
|
+
|
|
739
840
|
# Stats command
|
|
740
841
|
subparsers.add_parser("stats", help="View memory cache statistics (Positive & Negative)")
|
|
741
842
|
|
|
@@ -817,6 +918,33 @@ def main():
|
|
|
817
918
|
print(f" ✅ Proven Fix : {l.get('successful_fix') or 'N/A'}")
|
|
818
919
|
print("==================================================")
|
|
819
920
|
|
|
921
|
+
elif args.command == "record-telemetry":
|
|
922
|
+
ok = record_test_telemetry(
|
|
923
|
+
args.test_id,
|
|
924
|
+
args.status,
|
|
925
|
+
is_retry=args.retry,
|
|
926
|
+
duration_ms=args.duration,
|
|
927
|
+
error_signature=args.error,
|
|
928
|
+
context=args.context
|
|
929
|
+
)
|
|
930
|
+
if ok:
|
|
931
|
+
print(f"📊 [TELEMETRY RECORDED] {args.test_id} -> {args.status} (Retry={args.retry}, {args.duration}ms)")
|
|
932
|
+
|
|
933
|
+
elif args.command == "get-flaky":
|
|
934
|
+
flaky_list = get_flaky_telemetry(days=args.days, min_runs=args.min_runs, threshold=args.threshold)
|
|
935
|
+
print("==================================================")
|
|
936
|
+
print(f"📈 [HISTORICAL FLAKINESS TELEMETRY ({args.days} Days)]")
|
|
937
|
+
print("==================================================")
|
|
938
|
+
if not flaky_list:
|
|
939
|
+
print(" (Zero flaky tests detected above threshold. Suite is stable.)")
|
|
940
|
+
else:
|
|
941
|
+
for item in flaky_list:
|
|
942
|
+
status_icon = "🚨 [QUARANTINE]" if item["quarantine_recommended"] else "ℹ️ [OBSERVE]"
|
|
943
|
+
print(f"{status_icon} {item['test_id']}")
|
|
944
|
+
print(f" Runs: {item['total_runs']} | Passes: {item['pass_count']} | Fails: {item['fail_count']} | Retry Passes: {item['retry_pass_count']}")
|
|
945
|
+
print(f" Flaky Ratio: {item['flaky_ratio']*100:.1f}% | Avg Duration: {item['avg_duration_ms']}ms")
|
|
946
|
+
print("==================================================")
|
|
947
|
+
|
|
820
948
|
elif args.command == "stats":
|
|
821
949
|
stats = get_stats()
|
|
822
950
|
print("==================================================")
|
package/tools/cli.py
CHANGED
|
@@ -25,7 +25,7 @@ sys.path.insert(0, SCRIPT_DIR)
|
|
|
25
25
|
|
|
26
26
|
def cmd_version(args):
|
|
27
27
|
manifest_path = os.path.join(REPO_ROOT, "skills_manifest.json")
|
|
28
|
-
version = "2.
|
|
28
|
+
version = "2.1.0"
|
|
29
29
|
if os.path.exists(manifest_path):
|
|
30
30
|
try:
|
|
31
31
|
import json
|
|
@@ -257,6 +257,30 @@ def cmd_decouple_config(args):
|
|
|
257
257
|
sys.exit(decouple_main())
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
def cmd_fuzz_api(args):
|
|
261
|
+
from api_fuzzer import main as fuzz_main
|
|
262
|
+
sys.argv = [sys.argv[0]] + (sys.argv[2:] if len(sys.argv) > 2 else [])
|
|
263
|
+
sys.exit(fuzz_main())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cmd_som(args):
|
|
267
|
+
from visual_som_helper import main as som_main
|
|
268
|
+
sys.argv = [sys.argv[0]] + (sys.argv[2:] if len(sys.argv) > 2 else [])
|
|
269
|
+
sys.exit(som_main())
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def cmd_export_postman(args):
|
|
273
|
+
from export_postman_collection import main as postman_main
|
|
274
|
+
sys.argv = [sys.argv[0]] + (sys.argv[2:] if len(sys.argv) > 2 else [])
|
|
275
|
+
sys.exit(postman_main())
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def cmd_generate_jmx(args):
|
|
279
|
+
from generate_jmx_plan import main as jmx_main
|
|
280
|
+
sys.argv = [sys.argv[0]] + (sys.argv[2:] if len(sys.argv) > 2 else [])
|
|
281
|
+
sys.exit(jmx_main())
|
|
282
|
+
|
|
283
|
+
|
|
260
284
|
def main():
|
|
261
285
|
parser = argparse.ArgumentParser(
|
|
262
286
|
prog="agent-qa",
|
|
@@ -385,6 +409,22 @@ def main():
|
|
|
385
409
|
p_decouple.add_argument("--out-dir", default=None, help="Output directory for config profiles")
|
|
386
410
|
p_decouple.set_defaults(func=cmd_decouple_config)
|
|
387
411
|
|
|
412
|
+
# fuzz-api
|
|
413
|
+
p_fuzz = subparsers.add_parser("fuzz-api", help="Autonomous API Fuzzing & Chaos Breaker (bắn mutation payloads, bắt 500, DB leak, SLA breach)")
|
|
414
|
+
p_fuzz.set_defaults(func=cmd_fuzz_api)
|
|
415
|
+
|
|
416
|
+
# som (Visual Grounding for Canvas / GIS)
|
|
417
|
+
p_som = subparsers.add_parser("som", help="Multimodal Vision Grounding & Set-of-Marks (SoM) for Canvas/GIS without DOM")
|
|
418
|
+
p_som.set_defaults(func=cmd_som)
|
|
419
|
+
|
|
420
|
+
# export-postman
|
|
421
|
+
p_postman = subparsers.add_parser("export-postman", help="Export OpenAPI / Swagger / HAR to Postman Collection v2.1.0 with auto-asserts")
|
|
422
|
+
p_postman.set_defaults(func=cmd_export_postman)
|
|
423
|
+
|
|
424
|
+
# generate-jmx
|
|
425
|
+
p_jmx = subparsers.add_parser("generate-jmx", help="Generate Apache JMeter 5.x Test Plan XML (.jmx) from OpenAPI / Postman / HAR")
|
|
426
|
+
p_jmx.set_defaults(func=cmd_generate_jmx)
|
|
427
|
+
|
|
388
428
|
args, unknown = parser.parse_known_args()
|
|
389
429
|
if getattr(args, "offline", False):
|
|
390
430
|
os.environ["AGENT_QA_OFFLINE"] = "1"
|