synapse-hub 2.2.2 → 2.2.4
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/author-integrity-guard.md +9 -10
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/lessons-learned.md +3 -3
- package/.agents/rules/skill-precedence.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +3 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
- package/.agents/skills/security-dast-sentinel/SKILL.md +3 -0
- package/.cursorrules +7 -8
- package/.windsurfrules +7 -8
- package/AGENTS.md +3 -3
- package/CLAUDE.md +4 -5
- package/README.md +32 -26
- package/package.json +1 -1
- package/skills_manifest.json +2 -2
- package/tools/author_integrity.py +10 -4
- package/tools/browser_helpers.js +2 -2
- package/tools/cache_resolver.py +16 -5
- package/tools/cli.py +1 -1
- package/tools/distill_log.py +19 -10
- package/tools/export_postman_collection.py +29 -7
- package/tools/generate_jmx_plan.py +42 -10
- package/tools/infer_linter.py +45 -15
- package/tools/jtl_analyzer.py +1 -1
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +78 -7
- package/tools/repo_map.py +90 -56
- package/tools/safety_firewall.py +15 -2
- package/tools/setup_project.py +43 -13
- package/tools/skillspector.py +8 -8
- package/tools/verify_all.py +55 -47
package/tools/distill_log.py
CHANGED
|
@@ -26,8 +26,10 @@ def distill(log_text, max_lines=40):
|
|
|
26
26
|
# Noise filters
|
|
27
27
|
noise_patterns = [
|
|
28
28
|
re.compile(r'^\s*$'),
|
|
29
|
-
re.compile(r'\[INFO\b'),
|
|
30
|
-
re.compile(r'\[DEBUG\b'),
|
|
29
|
+
re.compile(r'\[INFO\b|\bINFO:|\bINFO\s+', re.IGNORECASE),
|
|
30
|
+
re.compile(r'\[DEBUG\b|\bDEBUG:|\bDEBUG\s+', re.IGNORECASE),
|
|
31
|
+
re.compile(r'\[TRACE\b|\bTRACE:|\bTRACE\s+', re.IGNORECASE),
|
|
32
|
+
re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}.*\b(?:INFO|DEBUG|TRACE)\b', re.IGNORECASE),
|
|
31
33
|
re.compile(r'\bPASSED\b', re.IGNORECASE),
|
|
32
34
|
re.compile(r'> Task :'),
|
|
33
35
|
re.compile(r'SLF4J:'),
|
|
@@ -47,10 +49,11 @@ def distill(log_text, max_lines=40):
|
|
|
47
49
|
re.compile(r'Caused by:'),
|
|
48
50
|
re.compile(r'at (?:Enterprise\.|Script\d+|com\.kms)'),
|
|
49
51
|
re.compile(r'TC-\d+.*FAILED'),
|
|
52
|
+
re.compile(r'FAIL\s+.*(?:\.spec|\.test)'),
|
|
50
53
|
]
|
|
51
54
|
|
|
52
|
-
tc_pattern = re.compile(r'(TC-\d
|
|
53
|
-
line_num_pattern = re.compile(r'((?:Script\d+|\w+Page)
|
|
55
|
+
tc_pattern = re.compile(r'(TC-\d+|test_[A-Za-z0-9_]+|[A-Za-z0-9_-]+\.(?:spec|test)\.[jt]sx?)')
|
|
56
|
+
line_num_pattern = re.compile(r'((?:Script\d+|\w+Page|\w+\.(?:groovy|ts|js|py)):(\d+))|File "([^"]+)", line (\d+)')
|
|
54
57
|
exception_pattern = re.compile(r'([A-Za-z0-9_.]*(?:Exception|Error)):?\s*(.*)')
|
|
55
58
|
|
|
56
59
|
captured_failures = []
|
|
@@ -63,7 +66,7 @@ def distill(log_text, max_lines=40):
|
|
|
63
66
|
for line in lines:
|
|
64
67
|
# Check if line indicates failure
|
|
65
68
|
is_fail_line = any(p.search(line) for p in failure_indicators)
|
|
66
|
-
is_noise = any(p.search(line) for p in noise_patterns)
|
|
69
|
+
is_noise = any(p.search(line) for p in noise_patterns) and not is_fail_line
|
|
67
70
|
|
|
68
71
|
# Detect TC identifier
|
|
69
72
|
m_tc = tc_pattern.search(line)
|
|
@@ -73,7 +76,7 @@ def distill(log_text, max_lines=40):
|
|
|
73
76
|
# Detect script line number
|
|
74
77
|
m_loc = line_num_pattern.search(line)
|
|
75
78
|
if m_loc:
|
|
76
|
-
failing_locs.append(m_loc.group(1))
|
|
79
|
+
failing_locs.append(m_loc.group(1) or f"{m_loc.group(3)}:{m_loc.group(4)}")
|
|
77
80
|
|
|
78
81
|
if is_fail_line:
|
|
79
82
|
in_failure_block = True
|
|
@@ -102,9 +105,12 @@ def distill(log_text, max_lines=40):
|
|
|
102
105
|
output.append("======================================================================")
|
|
103
106
|
return "\n".join(output)
|
|
104
107
|
|
|
105
|
-
output.append(f"❌ FAILURES DETECTED: {len(failing_tcs)} Test Case(s) marked FAILED.")
|
|
106
108
|
if failing_tcs:
|
|
109
|
+
output.append(f"❌ FAILURES DETECTED: {len(failing_tcs)} Test Case(s) marked FAILED.")
|
|
107
110
|
output.append(f" Targeted TCs: {', '.join(sorted(failing_tcs))}")
|
|
111
|
+
else:
|
|
112
|
+
output.append(f"❌ FAILURES / ERRORS DETECTED: {len(captured_failures)} failure block(s) isolated.")
|
|
113
|
+
|
|
108
114
|
if failing_locs:
|
|
109
115
|
unique_locs = sorted(set(failing_locs))
|
|
110
116
|
output.append(f" Failing Script Location(s): {', '.join(unique_locs[:5])}")
|
|
@@ -116,10 +122,13 @@ def distill(log_text, max_lines=40):
|
|
|
116
122
|
output.append("======================================================================")
|
|
117
123
|
return "\n".join(output)
|
|
118
124
|
|
|
125
|
+
# Public alias for MCP server compatibility
|
|
126
|
+
distill_log_content = distill
|
|
127
|
+
|
|
119
128
|
def main():
|
|
120
129
|
parser = argparse.ArgumentParser(description="Distill terminal execution logs into high-signal failure blocks.")
|
|
121
130
|
parser.add_argument("--file", "-f", help="Path to raw log file (reads from stdin if omitted).")
|
|
122
|
-
parser.add_argument("--save-raw", "-s", default=
|
|
131
|
+
parser.add_argument("--save-raw", "-s", default=None, help="Path to preserve raw log (omitted by default to prevent hub pollution).")
|
|
123
132
|
args = parser.parse_args()
|
|
124
133
|
|
|
125
134
|
if args.file:
|
|
@@ -134,13 +143,13 @@ def main():
|
|
|
134
143
|
sys.exit(0)
|
|
135
144
|
log_text = sys.stdin.read()
|
|
136
145
|
|
|
137
|
-
# Preserve raw log on disk
|
|
146
|
+
# Preserve raw log on disk if explicitly requested
|
|
138
147
|
if args.save_raw:
|
|
139
148
|
try:
|
|
140
149
|
os.makedirs(os.path.dirname(os.path.abspath(args.save_raw)), exist_ok=True)
|
|
141
150
|
with open(args.save_raw, "w", encoding="utf-8") as fp:
|
|
142
151
|
fp.write(log_text)
|
|
143
|
-
except Exception
|
|
152
|
+
except Exception:
|
|
144
153
|
pass
|
|
145
154
|
|
|
146
155
|
summary = distill(log_text)
|
|
@@ -36,6 +36,7 @@ Usage:
|
|
|
36
36
|
import os
|
|
37
37
|
import sys
|
|
38
38
|
import json
|
|
39
|
+
import re
|
|
39
40
|
import uuid
|
|
40
41
|
import csv
|
|
41
42
|
import argparse
|
|
@@ -82,7 +83,7 @@ def build_test_script(
|
|
|
82
83
|
"// Auto-generated Test Assertions by Synapse-Hub",
|
|
83
84
|
f"// 1. Assert Status Code is {expected_status}",
|
|
84
85
|
f"pm.test('Status code is {expected_status}', function () {{",
|
|
85
|
-
f" pm.expect(pm.response.code).to.
|
|
86
|
+
f" pm.expect(pm.response.code).to.eql({expected_status});",
|
|
86
87
|
"});",
|
|
87
88
|
"",
|
|
88
89
|
f"// 2. Assert SLA Latency (< {max_time_ms}ms)",
|
|
@@ -280,13 +281,34 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
|
|
|
280
281
|
"options": {"raw": {"language": "json"}}
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
# URL decomposition
|
|
284
|
-
|
|
284
|
+
# URL decomposition and path parameterization
|
|
285
|
+
raw_path = path
|
|
286
|
+
path_variables = []
|
|
287
|
+
normalized_segments = []
|
|
288
|
+
for seg in path.strip("/").split("/"):
|
|
289
|
+
if not seg:
|
|
290
|
+
continue
|
|
291
|
+
match = re.match(r"^\{([a-zA-Z0-9_-]+)\}$", seg)
|
|
292
|
+
if match:
|
|
293
|
+
param_name = match.group(1)
|
|
294
|
+
raw_path = raw_path.replace(f"{{{param_name}}}", f":{param_name}")
|
|
295
|
+
normalized_segments.append(f":{param_name}")
|
|
296
|
+
default_val = "{{lastCreatedId}}" if param_name.lower() in ("id", "entityid", "orderid") else f"{{{{{param_name}}}}}"
|
|
297
|
+
path_variables.append({
|
|
298
|
+
"key": param_name,
|
|
299
|
+
"value": default_val,
|
|
300
|
+
"description": f"URL Path variable {param_name}"
|
|
301
|
+
})
|
|
302
|
+
else:
|
|
303
|
+
normalized_segments.append(seg)
|
|
304
|
+
|
|
285
305
|
url_obj = {
|
|
286
|
-
"raw": f"{{{{baseUrl}}}}{
|
|
306
|
+
"raw": f"{{{{baseUrl}}}}{raw_path}",
|
|
287
307
|
"host": ["{{baseUrl}}"],
|
|
288
|
-
"path":
|
|
308
|
+
"path": normalized_segments
|
|
289
309
|
}
|
|
310
|
+
if path_variables:
|
|
311
|
+
url_obj["variable"] = path_variables
|
|
290
312
|
if query_params:
|
|
291
313
|
url_obj["query"] = query_params
|
|
292
314
|
|
|
@@ -451,10 +473,10 @@ def generate_postman_environment(name: str = "Enterprise-Staging-Env", base_url:
|
|
|
451
473
|
"name": name,
|
|
452
474
|
"values": [
|
|
453
475
|
{"key": "baseUrl", "value": base_url, "enabled": True},
|
|
454
|
-
{"key": "bearerToken", "value": "", "enabled": True},
|
|
476
|
+
{"key": "bearerToken", "value": "", "type": "secret", "enabled": True},
|
|
455
477
|
{"key": "lastCreatedId", "value": "1", "enabled": True},
|
|
456
478
|
{"key": "adminUser", "value": "qa_admin@enterprise.local", "enabled": True},
|
|
457
|
-
{"key": "adminPass", "value": "SecretVault#2026", "enabled": True}
|
|
479
|
+
{"key": "adminPass", "value": "SecretVault#2026", "type": "secret", "enabled": True}
|
|
458
480
|
],
|
|
459
481
|
"_postman_variable_scope": "environment"
|
|
460
482
|
}
|
|
@@ -122,6 +122,16 @@ def build_jmx_xml(
|
|
|
122
122
|
f' <stringProp name="Argument.value">{sla_p95}</stringProp>',
|
|
123
123
|
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
124
124
|
' </elementProp>',
|
|
125
|
+
' <elementProp name="MAX_LATENCY" elementType="Argument">',
|
|
126
|
+
' <stringProp name="Argument.name">MAX_LATENCY</stringProp>',
|
|
127
|
+
f' <stringProp name="Argument.value">${{__P(max_latency, {max(3000, sla_p95 * 2)})}}</stringProp>',
|
|
128
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
129
|
+
' </elementProp>',
|
|
130
|
+
' <elementProp name="LAST_CREATED_ID" elementType="Argument">',
|
|
131
|
+
' <stringProp name="Argument.name">LAST_CREATED_ID</stringProp>',
|
|
132
|
+
' <stringProp name="Argument.value">1</stringProp>',
|
|
133
|
+
' <stringProp name="Argument.metadata">=</stringProp>',
|
|
134
|
+
' </elementProp>',
|
|
125
135
|
' <elementProp name="BEARER_TOKEN" elementType="Argument">',
|
|
126
136
|
' <stringProp name="Argument.name">BEARER_TOKEN</stringProp>',
|
|
127
137
|
' <stringProp name="Argument.value"></stringProp>',
|
|
@@ -469,9 +479,9 @@ def build_jmx_xml(
|
|
|
469
479
|
f'{indent} <intProp name="Assertion.test_type">40</intProp>',
|
|
470
480
|
f'{indent} </ResponseAssertion>',
|
|
471
481
|
f'{indent} <hashTree/>',
|
|
472
|
-
f'{indent} <!-- SLA
|
|
473
|
-
f'{indent} <DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Assert
|
|
474
|
-
f'{indent} <stringProp name="DurationAssertion.duration">${{
|
|
482
|
+
f'{indent} <!-- SLA Ceiling Assertion (Percentile P95 is validated post-run by jtl_analyzer.py) -->',
|
|
483
|
+
f'{indent} <DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Assert Max Latency Ceiling (< ${{MAX_LATENCY}}ms)" enabled="true">',
|
|
484
|
+
f'{indent} <stringProp name="DurationAssertion.duration">${{MAX_LATENCY}}</stringProp>',
|
|
475
485
|
f'{indent} </DurationAssertion>',
|
|
476
486
|
f'{indent} <hashTree/>'
|
|
477
487
|
])
|
|
@@ -492,7 +502,7 @@ def build_jmx_xml(
|
|
|
492
502
|
f'{indent} <!-- Deep Correlation: Random Array Item Selection (Match No: 0) -->',
|
|
493
503
|
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Random Item Selector" enabled="true">',
|
|
494
504
|
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">RANDOM_ITEM_ID</stringProp>',
|
|
495
|
-
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..
|
|
505
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..id</stringProp>',
|
|
496
506
|
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">0</stringProp>',
|
|
497
507
|
f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{RANDOM_ITEM_ID}}</stringProp>',
|
|
498
508
|
f'{indent} </JSONPostProcessor>',
|
|
@@ -505,7 +515,7 @@ def build_jmx_xml(
|
|
|
505
515
|
f'{indent} <!-- Auto-Extract Dynamic Bearer Token -->',
|
|
506
516
|
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Bearer Token" enabled="true">',
|
|
507
517
|
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">BEARER_TOKEN</stringProp>',
|
|
508
|
-
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs"
|
|
518
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..[\'token\',\'access_token\']</stringProp>',
|
|
509
519
|
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
|
|
510
520
|
f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{BEARER_TOKEN}}</stringProp>',
|
|
511
521
|
f'{indent} </JSONPostProcessor>',
|
|
@@ -516,9 +526,9 @@ def build_jmx_xml(
|
|
|
516
526
|
f'{indent} <!-- Auto-Extract Created Entity ID -->',
|
|
517
527
|
f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Entity ID" enabled="true">',
|
|
518
528
|
f'{indent} <stringProp name="JSONPostProcessor.referenceNames">LAST_CREATED_ID</stringProp>',
|
|
519
|
-
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs"
|
|
529
|
+
f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..[\'id\',\'code\']</stringProp>',
|
|
520
530
|
f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
|
|
521
|
-
f'{indent} <stringProp name="JSONPostProcessor.defaultValues"
|
|
531
|
+
f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{LAST_CREATED_ID}}</stringProp>',
|
|
522
532
|
f'{indent} </JSONPostProcessor>',
|
|
523
533
|
f'{indent} <hashTree/>'
|
|
524
534
|
])
|
|
@@ -752,7 +762,10 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
|
|
|
752
762
|
|
|
753
763
|
def replace_path_param(match):
|
|
754
764
|
pname = match.group(1)
|
|
755
|
-
|
|
765
|
+
plow = pname.lower()
|
|
766
|
+
if plow in ("itemid", "productid", "item_id", "product_id"):
|
|
767
|
+
return "${RANDOM_ITEM_ID}"
|
|
768
|
+
if plow in ("id", "categoryid", "entityid", "userid", "orderid", "order_id"):
|
|
756
769
|
return "${LAST_CREATED_ID}"
|
|
757
770
|
return f"${{{pname}}}"
|
|
758
771
|
|
|
@@ -794,6 +807,25 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
|
|
|
794
807
|
"expected_status": expected_status
|
|
795
808
|
})
|
|
796
809
|
|
|
810
|
+
def sampler_sort_key(s: Dict[str, Any]) -> int:
|
|
811
|
+
p = s.get("path", "").lower()
|
|
812
|
+
m = s.get("method", "GET")
|
|
813
|
+
if "auth" in p or "login" in p or "token" in p:
|
|
814
|
+
return 1
|
|
815
|
+
if m == "POST" and "{" not in s.get("path", "") and "${" not in s.get("path", ""):
|
|
816
|
+
return 2
|
|
817
|
+
if m == "GET" and "{" not in s.get("path", "") and "${" not in s.get("path", ""):
|
|
818
|
+
return 3
|
|
819
|
+
if m == "GET":
|
|
820
|
+
return 4
|
|
821
|
+
if m in ("PUT", "PATCH"):
|
|
822
|
+
return 5
|
|
823
|
+
if m == "DELETE":
|
|
824
|
+
return 6
|
|
825
|
+
return 7
|
|
826
|
+
|
|
827
|
+
samplers.sort(key=sampler_sort_key)
|
|
828
|
+
|
|
797
829
|
return plan_name, base_host, protocol, port, samplers
|
|
798
830
|
|
|
799
831
|
|
|
@@ -993,8 +1025,8 @@ def main():
|
|
|
993
1025
|
plan_name, base_host, protocol, port, samplers = parse_openapi_for_jmeter(spec_data)
|
|
994
1026
|
else:
|
|
995
1027
|
# Fallback to built-in sample spec
|
|
996
|
-
from export_postman_collection import
|
|
997
|
-
spec_data =
|
|
1028
|
+
from export_postman_collection import get_domain_spec
|
|
1029
|
+
spec_data = get_domain_spec("ecommerce")
|
|
998
1030
|
plan_name, base_host, protocol, port, samplers = parse_openapi_for_jmeter(spec_data)
|
|
999
1031
|
|
|
1000
1032
|
# Base URL override if provided
|
package/tools/infer_linter.py
CHANGED
|
@@ -154,21 +154,40 @@ def analyze_script(file_path):
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def run_infer_linter(target_dir=None):
|
|
157
|
-
"""Scans
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
"""Scans test scripts across Groovy, TypeScript, JavaScript, Python, and Java."""
|
|
158
|
+
EXTENSIONS = ("*.groovy", "*.ts", "*.js", "*.py", "*.java")
|
|
159
|
+
|
|
160
|
+
if target_dir:
|
|
161
|
+
if os.path.isfile(target_dir):
|
|
162
|
+
script_files = [target_dir]
|
|
163
|
+
scan_dir = os.path.dirname(target_dir)
|
|
164
|
+
elif os.path.isdir(target_dir):
|
|
165
|
+
scan_dir = target_dir
|
|
166
|
+
script_files = []
|
|
167
|
+
for ext in EXTENSIONS:
|
|
168
|
+
script_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
|
|
169
|
+
else:
|
|
170
|
+
scan_dir = target_dir
|
|
171
|
+
script_files = []
|
|
163
172
|
else:
|
|
164
|
-
|
|
173
|
+
# Search candidate directories across test frameworks (Katalon, Playwright, Pytest, Cypress)
|
|
174
|
+
script_files = []
|
|
175
|
+
scan_dir = SCRIPTS_DIR
|
|
176
|
+
for cand in ["Scripts", "tests", "test", "e2e", "specs", "cypress"]:
|
|
177
|
+
cand_path = os.path.join(REPO_ROOT, cand)
|
|
178
|
+
if os.path.isdir(cand_path):
|
|
179
|
+
for ext in EXTENSIONS:
|
|
180
|
+
script_files.extend(glob.glob(os.path.join(cand_path, "**", ext), recursive=True))
|
|
181
|
+
if script_files:
|
|
182
|
+
scan_dir = cand_path
|
|
183
|
+
break
|
|
165
184
|
|
|
166
185
|
results = []
|
|
167
186
|
critical_count = 0
|
|
168
187
|
warning_count = 0
|
|
169
188
|
clean_count = 0
|
|
170
189
|
|
|
171
|
-
for s in sorted(script_files):
|
|
190
|
+
for s in sorted(set(script_files)):
|
|
172
191
|
res = analyze_script(s)
|
|
173
192
|
results.append(res)
|
|
174
193
|
crit = sum(1 for d in res["defects"] if d["severity"] == "CRITICAL")
|
|
@@ -179,17 +198,28 @@ def run_infer_linter(target_dir=None):
|
|
|
179
198
|
clean_count += 1
|
|
180
199
|
|
|
181
200
|
total_scripts = len(script_files)
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
if total_scripts == 0:
|
|
202
|
+
health_score = 0.0
|
|
203
|
+
clean_pct = 0.0
|
|
204
|
+
else:
|
|
205
|
+
health_score = round(((total_scripts - critical_count) / total_scripts) * 100, 1)
|
|
206
|
+
clean_pct = round((clean_count / total_scripts) * 100, 1)
|
|
184
207
|
|
|
185
208
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
186
209
|
print(f"{BOLD}{CYAN} ENTERPRISE STATIC DEFECT & FAKE-PASS SENTINEL (Meta Infer-Style) {RESET}")
|
|
187
210
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
if total_scripts == 0:
|
|
212
|
+
print(f" Target Scope : 0 Test Scripts discovered in {os.path.basename(scan_dir)}/ (No tests found)")
|
|
213
|
+
print(f" Critical Defects : {GREEN}0{RESET}")
|
|
214
|
+
print(f" Warnings / Suggestions : {GREEN}0{RESET}")
|
|
215
|
+
print(f" Clean Scripts : 0/0 (N/A)")
|
|
216
|
+
print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
|
|
217
|
+
else:
|
|
218
|
+
print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
|
|
219
|
+
print(f" Critical Defects : {RED if critical_count > 0 else GREEN}{critical_count}{RESET} (Swallowed exceptions, empty scripts)")
|
|
220
|
+
print(f" Warnings / Suggestions : {YELLOW if warning_count > 0 else GREEN}{warning_count}{RESET} (Missing explicit asserts, excessive delays)")
|
|
221
|
+
print(f" Clean Scripts : {GREEN}{clean_count}/{total_scripts} ({clean_pct:.1f}%){RESET}")
|
|
222
|
+
print(f" Codebase Health Score : {GREEN if health_score == 100 else YELLOW}{health_score:.1f}%{RESET}")
|
|
193
223
|
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
194
224
|
|
|
195
225
|
# Print defect breakdown if any
|
package/tools/jtl_analyzer.py
CHANGED
|
@@ -362,7 +362,7 @@ def main():
|
|
|
362
362
|
parser.add_argument("--sla-p95", type=float, default=None, help="P95 Latency threshold in ms (fails if exceeded).")
|
|
363
363
|
parser.add_argument("--sla-p99", type=float, default=None, help="P99 Latency threshold in ms (fails if exceeded).")
|
|
364
364
|
parser.add_argument("--sla-avg", type=float, default=None, help="Average Latency threshold in ms (fails if exceeded).")
|
|
365
|
-
parser.add_argument("--max-error-rate", type=float, default=None, help="Maximum allowed error rate percentage (e.g. 1.0 = 1
|
|
365
|
+
parser.add_argument("--max-error-rate", type=float, default=None, help="Maximum allowed error rate percentage (e.g. 1.0 = 1%%).")
|
|
366
366
|
parser.add_argument("--markdown", "-m", help="Output path for Markdown summary report.")
|
|
367
367
|
parser.add_argument("--json", "-j", help="Output path for JSON summary metrics.")
|
|
368
368
|
parser.add_argument("--telemetry", help="Path to server telemetry CSV (timestamp, cpu_pct, memory_pct, db_pool_pct) for Full-Stack APM correlation.")
|
|
@@ -160,7 +160,7 @@ while (($line = $inputReader.ReadLine()) -ne $null) {
|
|
|
160
160
|
result = @{
|
|
161
161
|
protocolVersion = "2024-11-05"
|
|
162
162
|
capabilities = @{ tools = @{} }
|
|
163
|
-
serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.2.
|
|
163
|
+
serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.2.4" }
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
|
package/tools/mcp_server.py
CHANGED
package/tools/micro_mutator.py
CHANGED
|
@@ -68,7 +68,7 @@ ASSERTION_PATTERN = re.compile(
|
|
|
68
68
|
|
|
69
69
|
# Tautological / fake assertions that do not test application logic
|
|
70
70
|
TAUTOLOGY_PATTERN = re.compile(
|
|
71
|
-
r'(?:assert\s+(?:true|1\s*==\s*1)\b|assertEquals\s*\(\s*
|
|
71
|
+
r'(?:assert\s+(?:true|1\s*==\s*1)\b|assert\s+([a-zA-Z_]\w*)\s*==\s*\1\b|assertEquals\s*\(\s*([a-zA-Z_]\w*)\s*,\s*\2\s*\)|expect\s*\(\s*([a-zA-Z_]\w*)\s*\)\.toBe\s*\(\s*\3\s*\)|assertTrue\s*\(\s*true\s*\))',
|
|
72
72
|
re.IGNORECASE
|
|
73
73
|
)
|
|
74
74
|
|
|
@@ -195,7 +195,12 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
195
195
|
- If mutant is inside an unasserted block or swallowed exception: mutant SURVIVES.
|
|
196
196
|
"""
|
|
197
197
|
if not mutants:
|
|
198
|
-
|
|
198
|
+
has_asserts = any(
|
|
199
|
+
ASSERTION_PATTERN.search(l) and not TAUTOLOGY_PATTERN.search(l)
|
|
200
|
+
for l in code.splitlines()
|
|
201
|
+
if not l.strip().startswith(("//", "#", "/*"))
|
|
202
|
+
)
|
|
203
|
+
return mutants, (100.0 if has_asserts else 0.0)
|
|
199
204
|
|
|
200
205
|
lines = code.splitlines()
|
|
201
206
|
has_empty_catch = bool(EMPTY_CATCH_PATTERN.search(code))
|
|
@@ -245,6 +250,55 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
245
250
|
return mutants, score
|
|
246
251
|
|
|
247
252
|
|
|
253
|
+
def evaluate_mutants_dynamically(file_path: str, code: str, mutants: List[Dict[str, Any]], test_cmd: str) -> Tuple[List[Dict[str, Any]], float]:
|
|
254
|
+
"""
|
|
255
|
+
Executes live mutation testing by injecting mutants into disk file and executing test_cmd.
|
|
256
|
+
If the test command fails (exit code != 0), the mutant is KILLED by the test suite.
|
|
257
|
+
If the test command passes (exit code == 0), the mutant SURVIVED (unasserted logic branch).
|
|
258
|
+
Safely restores original file content in all cases.
|
|
259
|
+
"""
|
|
260
|
+
if not mutants:
|
|
261
|
+
return mutants, 0.0
|
|
262
|
+
|
|
263
|
+
killed_count = 0
|
|
264
|
+
abs_path = os.path.abspath(file_path)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
for m in mutants:
|
|
268
|
+
mutated_code = m["mutated_code"]
|
|
269
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
270
|
+
f.write(mutated_code)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
proc = subprocess.run(
|
|
274
|
+
test_cmd,
|
|
275
|
+
shell=True,
|
|
276
|
+
capture_output=True,
|
|
277
|
+
timeout=30,
|
|
278
|
+
text=True
|
|
279
|
+
)
|
|
280
|
+
if proc.returncode != 0:
|
|
281
|
+
m["status"] = "KILLED"
|
|
282
|
+
m["reason"] = f"Dynamic test caught mutant: command failed (exit code {proc.returncode})"
|
|
283
|
+
killed_count += 1
|
|
284
|
+
else:
|
|
285
|
+
m["status"] = "SURVIVED"
|
|
286
|
+
m["reason"] = "Dynamic test suite passed unconditionally: missing assertion for this mutant."
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
m["status"] = "KILLED"
|
|
289
|
+
m["reason"] = "Mutant triggered execution timeout (> 30s): caught by runtime guard"
|
|
290
|
+
killed_count += 1
|
|
291
|
+
except Exception as e:
|
|
292
|
+
m["status"] = "SURVIVED"
|
|
293
|
+
m["reason"] = f"Test execution error: {e}"
|
|
294
|
+
finally:
|
|
295
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
296
|
+
f.write(code)
|
|
297
|
+
|
|
298
|
+
score = evaluate_mutation_score(killed_count, len(mutants))
|
|
299
|
+
return mutants, score
|
|
300
|
+
|
|
301
|
+
|
|
248
302
|
def scan_directory_for_mutants(target_dir: str) -> List[Dict[str, Any]]:
|
|
249
303
|
"""Scans target test/page scripts and generates a collection of mutants."""
|
|
250
304
|
all_mutants = []
|
|
@@ -269,21 +323,23 @@ def scan_directory_for_mutants(target_dir: str) -> List[Dict[str, Any]]:
|
|
|
269
323
|
def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "") -> Dict[str, Any]:
|
|
270
324
|
"""
|
|
271
325
|
Evaluates target file or directory for mutation robustness.
|
|
272
|
-
|
|
326
|
+
Dispatches to live test execution if test_cmd is provided, or static heuristic otherwise.
|
|
273
327
|
"""
|
|
274
328
|
target = os.path.abspath(target_path)
|
|
275
329
|
all_mutants = []
|
|
330
|
+
exec_mode = "DYNAMIC" if test_cmd else "STATIC"
|
|
276
331
|
|
|
277
332
|
if os.path.isfile(target):
|
|
278
333
|
with open(target, "r", encoding="utf-8", errors="ignore") as f:
|
|
279
334
|
code = f.read()
|
|
280
335
|
mutants = generate_mutants_for_code(code, file_path=os.path.basename(target))
|
|
281
|
-
|
|
336
|
+
if test_cmd:
|
|
337
|
+
evaluated_mutants, score = evaluate_mutants_dynamically(target, code, mutants, test_cmd)
|
|
338
|
+
else:
|
|
339
|
+
evaluated_mutants, score = evaluate_mutants_statically(code, mutants)
|
|
282
340
|
all_mutants.extend(evaluated_mutants)
|
|
283
341
|
else:
|
|
284
|
-
# Directory evaluation
|
|
285
342
|
valid_exts = {".groovy", ".py", ".ts", ".js"}
|
|
286
|
-
total_killed = 0
|
|
287
343
|
for root, _, files in os.walk(target):
|
|
288
344
|
for f in files:
|
|
289
345
|
if any(f.endswith(ext) for ext in valid_exts):
|
|
@@ -293,7 +349,10 @@ def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "
|
|
|
293
349
|
code = fp.read()
|
|
294
350
|
rel_path = os.path.relpath(file_full, target)
|
|
295
351
|
f_mutants = generate_mutants_for_code(code, file_path=rel_path)
|
|
296
|
-
|
|
352
|
+
if test_cmd:
|
|
353
|
+
eval_m, _ = evaluate_mutants_dynamically(file_full, code, f_mutants, test_cmd)
|
|
354
|
+
else:
|
|
355
|
+
eval_m, _ = evaluate_mutants_statically(code, f_mutants)
|
|
297
356
|
all_mutants.extend(eval_m)
|
|
298
357
|
except Exception:
|
|
299
358
|
pass
|
|
@@ -305,6 +364,18 @@ def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "
|
|
|
305
364
|
survived = sum(1 for m in all_mutants if m["status"] == "SURVIVED")
|
|
306
365
|
passed = score >= min_score
|
|
307
366
|
|
|
367
|
+
return {
|
|
368
|
+
"target": target,
|
|
369
|
+
"mode": exec_mode,
|
|
370
|
+
"total_mutants": len(all_mutants),
|
|
371
|
+
"killed": killed,
|
|
372
|
+
"survived": survived,
|
|
373
|
+
"mutation_score": score,
|
|
374
|
+
"min_score": min_score,
|
|
375
|
+
"passed": passed,
|
|
376
|
+
"mutants": all_mutants
|
|
377
|
+
}
|
|
378
|
+
|
|
308
379
|
return {
|
|
309
380
|
"target": target,
|
|
310
381
|
"total_mutants": len(all_mutants),
|