synapse-hub 2.7.0 → 3.0.1
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/live-explorer.md +2 -0
- package/.agents/agents/pom-crafter.md +2 -0
- package/.agents/agents/test-runner.md +3 -0
- package/.agents/rules/communication-style.md +4 -0
- package/.agents/rules/context-engineering.md +1 -0
- package/.agents/rules/git-push-protocol.md +17 -0
- package/.agents/skills/agentless-qa-repair/examples/three_phase_repair_workflow.md +40 -0
- package/.agents/skills/antigravity-test-sentinel/examples/sprint_dod_enforcement.md +47 -0
- package/.agents/skills/auth-session-and-cookie-manager/examples/multi_role_auth_matrix.md +70 -0
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_playwright_pom.ts +52 -0
- package/.agents/skills/failure-autopsy-and-healing/examples/closed_loop_healing_walkthrough.md +48 -0
- package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
- package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/examples/dspy_assertion_optimizer_case.md +46 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +1 -1
- package/.agents/skills/qa-aci-interface/examples/surgical_code_window_walkthrough.md +40 -0
- package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
- package/.agents/skills/review-spec/SKILL.md +1 -1
- package/.agents/skills/review-spec/examples/pocock_grilling_decision_rubric.md +40 -0
- package/.agents/skills/senior-code-review/SKILL.md +1 -1
- package/.agents/skills/skillspector-security-guard/examples/audit_threat_vectors_report.json +35 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +1 -1
- package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
- package/.agents/skills/tot-exploratory-testing/examples/payment_gateway_tot_tree.json +62 -0
- package/.agents/skills/verification-before-completion/SKILL.md +2 -2
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/examples/enterprise_tasks_suite.json +38 -0
- package/.agents/skills/web-vitals-and-performance-profiler/examples/playwright_cwv_profiler_fixture.ts +61 -0
- package/AGENTS.md +6 -4
- package/CLAUDE.md +2 -1
- package/README.en.md +71 -292
- package/README.md +69 -304
- package/action.yml +1 -1
- package/bin/agent-qa.js +2 -2
- package/docs/ARCHITECTURE.en.md +192 -0
- package/docs/ARCHITECTURE.md +135 -88
- package/docs/CI_CD_INTEGRATION.en.md +96 -0
- package/docs/CI_CD_INTEGRATION.md +24 -22
- package/docs/CLI_REFERENCE.en.md +174 -0
- package/docs/CLI_REFERENCE.md +120 -115
- package/docs/CONTRIBUTING.en.md +49 -0
- package/docs/CONTRIBUTING.md +26 -68
- package/docs/QUALITY_GATES.en.md +115 -0
- package/docs/QUALITY_GATES.md +115 -0
- package/docs/research/agent_skill_training_analysis.md +1 -1
- package/package.json +1 -1
- package/scripts/package_dist.py +2 -1
- package/skills_manifest.json +6 -6
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +2 -1
- package/templates/ai_bridges/copilot-instructions.md +2 -2
- package/tools/__init__.py +18 -1
- package/tools/_openapi_parser.py +1 -1
- package/tools/agent_canary.py +42 -30
- package/tools/agentless_patcher.py +2 -2
- package/tools/analyze_project.py +3 -5
- package/tools/api_drift_sentinel.py +4 -4
- package/tools/api_fuzzer.py +8 -8
- package/tools/auto_heal.py +4 -5
- package/tools/auto_heal_reflexion.py +10 -2
- package/tools/autogen_qa_broker.py +2 -2
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +2 -3
- package/tools/bug_reporter.py +11 -13
- package/tools/bump_version.py +90 -19
- package/tools/cache_resolver.py +78 -83
- package/tools/check_env.py +9 -1
- package/tools/check_version_sync.py +58 -4
- package/tools/ci_healing_hook.py +4 -6
- package/tools/cli.py +5 -7
- package/tools/config_decoupler.py +2 -3
- package/tools/contract_validator.py +141 -0
- package/tools/core/__init__.py +52 -0
- package/tools/count_tokens.py +22 -20
- package/tools/dashboard_generator.py +629 -1000
- package/tools/db_sandbox.py +87 -62
- package/tools/debt_harvester.py +3 -3
- package/tools/distill_log.py +3 -3
- package/tools/dspy_qa_signatures.py +3 -4
- package/tools/export_postman_collection.py +8 -8
- package/tools/flaky_detector.py +2 -2
- package/tools/fuzz_payloads.py +5 -5
- package/tools/generate_allure_report.py +1 -1
- package/tools/generate_jmx_plan.py +4 -4
- package/tools/gherkin_suite.py +1 -1
- package/tools/git_pre_commit_guard.py +2 -2
- package/tools/goldfish_review.py +2 -2
- package/tools/heal/__init__.py +28 -0
- package/tools/heal_pr_automator.py +6 -6
- package/tools/heuristic_locator_matcher.py +2 -2
- package/tools/infer_linter.py +143 -29
- package/tools/interview_story.py +0 -1
- package/tools/jtl_analyzer.py +11 -11
- package/tools/lint/__init__.py +30 -0
- package/tools/lint_ai_docs.py +42 -0
- package/tools/lookup_tc.py +1 -1
- package/tools/matrix_sharder.py +15 -13
- package/tools/mcp/__init__.py +16 -0
- package/tools/mcp_excel_server.py +2 -2
- package/tools/mcp_server.py +9 -7
- package/tools/md_to_xlsx.py +26 -26
- package/tools/micro_mutator.py +32 -15
- package/tools/network_mock_shield.py +27 -27
- package/tools/newman_analyzer.py +2 -3
- package/tools/omni_emitter.py +5 -8
- package/tools/omni_parser.py +5 -5
- package/tools/omni_transpiler.py +5 -5
- package/tools/output_formatter.py +19 -0
- package/tools/perf/__init__.py +24 -0
- package/tools/pinpoint_failure.py +1 -1
- package/tools/project_state.py +0 -2
- package/tools/qa_event_stream.py +1 -1
- package/tools/repo_map.py +8 -8
- package/tools/report/__init__.py +32 -0
- package/tools/reversible_sandbox.py +3 -4
- package/tools/safety_firewall.py +2 -2
- package/tools/senior_code_review.py +4 -3
- package/tools/setup_project.py +10 -12
- package/tools/skill_collision_guard.py +3 -3
- package/tools/skill_vault.py +94 -6
- package/tools/skillspector.py +10 -8
- package/tools/smart_visual_masker.py +4 -6
- package/tools/spec_drift_sentinel.py +1 -2
- package/tools/swe_qa_aci.py +2 -2
- package/tools/synapse_qa_bench.py +3 -3
- package/tools/sync_agent_rules.py +18 -3
- package/tools/telemetry_correlator.py +3 -2
- package/tools/test_impact.py +2 -1
- package/tools/test_memory_os.py +15 -5
- package/tools/textgrad_mutation_optimizer.py +3 -3
- package/tools/tms_sync_bridge.py +5 -5
- package/tools/tot_exploratory_tester.py +5 -6
- package/tools/traffic_session_converter.py +9 -9
- package/tools/universal_ir.py +2 -3
- package/tools/update_sentinel.py +6 -9
- package/tools/verify_all.py +12 -2
- package/tools/visual_diff_canvas.py +4 -4
- package/tools/visual_som_helper.py +15 -13
- package/tools/watch_mode.py +1 -1
- package/tools/wcag_audit.py +22 -1
- package/tools/welcome_page.py +5 -6
package/tools/mcp_server.py
CHANGED
|
@@ -11,9 +11,8 @@ import os
|
|
|
11
11
|
import sys
|
|
12
12
|
import json
|
|
13
13
|
import re
|
|
14
|
-
import traceback
|
|
15
14
|
from pathlib import Path
|
|
16
|
-
from typing import Dict, Any,
|
|
15
|
+
from typing import Dict, Any, Optional, Set
|
|
17
16
|
|
|
18
17
|
# Enforce UTF-8 stdio encoding
|
|
19
18
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -47,7 +46,7 @@ import gherkin_suite
|
|
|
47
46
|
import api_fuzzer
|
|
48
47
|
|
|
49
48
|
SERVER_NAME = "enterprise-agent-qa"
|
|
50
|
-
SERVER_VERSION = "
|
|
49
|
+
SERVER_VERSION = "3.0.1"
|
|
51
50
|
|
|
52
51
|
MCP_TOOLS = [
|
|
53
52
|
{
|
|
@@ -474,13 +473,15 @@ def _handle_pull_alm_story(arguments: Dict[str, Any]) -> str:
|
|
|
474
473
|
|
|
475
474
|
def _handle_report_alm_defect(arguments: Dict[str, Any]) -> str:
|
|
476
475
|
bridge = tms_sync_bridge.TMSBridge()
|
|
476
|
+
raw_screenshot = arguments.get("screenshot_path", "")
|
|
477
|
+
safe_screenshot = _validate_safe_path(raw_screenshot) if raw_screenshot else ""
|
|
477
478
|
ticket = bridge.report_defect(
|
|
478
479
|
project_key=arguments["project_key"],
|
|
479
480
|
test_case_id=arguments["test_case_id"],
|
|
480
481
|
summary=arguments["summary"],
|
|
481
482
|
error_message=arguments["error_message"],
|
|
482
483
|
stack_trace=arguments.get("stack_trace", ""),
|
|
483
|
-
screenshot_path=
|
|
484
|
+
screenshot_path=safe_screenshot
|
|
484
485
|
)
|
|
485
486
|
return json.dumps(ticket, indent=2)
|
|
486
487
|
|
|
@@ -518,7 +519,8 @@ def _handle_convert_traffic_session(arguments: Dict[str, Any]) -> str:
|
|
|
518
519
|
|
|
519
520
|
def _handle_resolve_visual_som_coordinates(arguments: Dict[str, Any]) -> str:
|
|
520
521
|
helper = visual_som_helper.VisualSoMHelper()
|
|
521
|
-
|
|
522
|
+
raw_img = arguments.get("image_path", "screenshot.png")
|
|
523
|
+
img = _validate_safe_path(raw_img)
|
|
522
524
|
lbl = arguments.get("target_label")
|
|
523
525
|
mid = arguments.get("mark_id")
|
|
524
526
|
marked_img, marks_map = helper.generate_som_overlay(img, [])
|
|
@@ -527,7 +529,7 @@ def _handle_resolve_visual_som_coordinates(arguments: Dict[str, Any]) -> str:
|
|
|
527
529
|
|
|
528
530
|
|
|
529
531
|
def _handle_compile_gherkin_suite(arguments: Dict[str, Any]) -> str:
|
|
530
|
-
target = arguments["target"]
|
|
532
|
+
target = _validate_safe_path(arguments["target"])
|
|
531
533
|
res = gherkin_suite.compile_feature(target) if os.path.isfile(target) else gherkin_suite.run(target)
|
|
532
534
|
if isinstance(res, int):
|
|
533
535
|
return json.dumps({"exit_code": res, "detail": "see reports/gherkin_suite_report.json"}, indent=2)
|
|
@@ -599,7 +601,7 @@ def handle_tool_call(name: str, arguments: Dict[str, Any]) -> str:
|
|
|
599
601
|
return handler(arguments)
|
|
600
602
|
|
|
601
603
|
|
|
602
|
-
def process_message(msg: Dict[str, Any]) -> Dict[str, Any]:
|
|
604
|
+
def process_message(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
603
605
|
"""Processes an incoming JSON-RPC 2.0 message."""
|
|
604
606
|
method = msg.get("method")
|
|
605
607
|
msg_id = msg.get("id")
|
package/tools/md_to_xlsx.py
CHANGED
|
@@ -16,7 +16,7 @@ import os
|
|
|
16
16
|
import sys
|
|
17
17
|
import re
|
|
18
18
|
import argparse
|
|
19
|
-
from typing import List,
|
|
19
|
+
from typing import List, Tuple
|
|
20
20
|
|
|
21
21
|
# UTF-8 terminal encoding
|
|
22
22
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -52,11 +52,11 @@ def parse_markdown_tables(content: str) -> List[Tuple[List[str], List[List[str]]
|
|
|
52
52
|
"""
|
|
53
53
|
lines = content.splitlines()
|
|
54
54
|
tables = []
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
current_header = None
|
|
57
57
|
current_rows = []
|
|
58
58
|
in_table = False
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
for line in lines:
|
|
61
61
|
stripped = line.strip()
|
|
62
62
|
if not stripped.startswith('|') or not stripped.endswith('|'):
|
|
@@ -66,15 +66,15 @@ def parse_markdown_tables(content: str) -> List[Tuple[List[str], List[List[str]]
|
|
|
66
66
|
current_rows = []
|
|
67
67
|
in_table = False
|
|
68
68
|
continue
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# Split cells
|
|
71
71
|
raw_cells = [c.strip() for c in stripped[1:-1].split('|')]
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
# Check if separator row (|---|---|)
|
|
74
74
|
if all(re.match(r'^\s*:?-+:?\s*$', c) for c in raw_cells if c):
|
|
75
75
|
in_table = True
|
|
76
76
|
continue
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
if not in_table:
|
|
79
79
|
# First line with pipes is potential header
|
|
80
80
|
current_header = [clean_cell_content(c) for c in raw_cells]
|
|
@@ -88,10 +88,10 @@ def parse_markdown_tables(content: str) -> List[Tuple[List[str], List[List[str]]
|
|
|
88
88
|
elif len(cleaned_row) > len(current_header):
|
|
89
89
|
cleaned_row = cleaned_row[:len(current_header)]
|
|
90
90
|
current_rows.append(cleaned_row)
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
if in_table and current_header and current_rows:
|
|
93
93
|
tables.append((current_header, current_rows))
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
return tables
|
|
96
96
|
|
|
97
97
|
|
|
@@ -108,32 +108,32 @@ def create_styled_excel(tables: List[Tuple[List[str], List[List[str]]]], output_
|
|
|
108
108
|
NAVY_BLUE = "1F4E78"
|
|
109
109
|
WHITE = "FFFFFF"
|
|
110
110
|
BORDER_COLOR = "D9D9D9"
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
# Styles
|
|
113
113
|
header_fill = PatternFill(start_color=NAVY_BLUE, end_color=NAVY_BLUE, fill_type="solid")
|
|
114
114
|
header_font = Font(name="Calibri", size=11, bold=True, color=WHITE)
|
|
115
115
|
header_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
cell_border = Border(
|
|
118
118
|
left=Side(style='thin', color=BORDER_COLOR),
|
|
119
119
|
right=Side(style='thin', color=BORDER_COLOR),
|
|
120
120
|
top=Side(style='thin', color=BORDER_COLOR),
|
|
121
121
|
bottom=Side(style='thin', color=BORDER_COLOR)
|
|
122
122
|
)
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
# Status badges
|
|
125
125
|
fill_high = PatternFill(start_color="FCE4D6", fill_type="solid") # Soft Red
|
|
126
126
|
font_high = Font(name="Calibri", size=10, bold=True, color="C00000")
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
fill_med = PatternFill(start_color="FFF2CC", fill_type="solid") # Soft Yellow
|
|
129
129
|
font_med = Font(name="Calibri", size=10, bold=True, color="B25900")
|
|
130
|
-
|
|
130
|
+
|
|
131
131
|
fill_low = PatternFill(start_color="E2EFDA", fill_type="solid") # Soft Green
|
|
132
132
|
font_low = Font(name="Calibri", size=10, bold=True, color="375623")
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
fill_yes = PatternFill(start_color="E2EFDA", fill_type="solid")
|
|
135
135
|
font_yes = Font(name="Calibri", size=10, color="276A3C", bold=True)
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
fill_no = PatternFill(start_color="F2F2F2", fill_type="solid")
|
|
138
138
|
font_no = Font(name="Calibri", size=10, color="595959")
|
|
139
139
|
|
|
@@ -191,16 +191,16 @@ def create_styled_excel(tables: List[Tuple[List[str], List[List[str]]]], output_
|
|
|
191
191
|
for row_data in all_rows:
|
|
192
192
|
ws.append(row_data)
|
|
193
193
|
ws.row_dimensions[current_row_idx].height = None # Auto height for wrapped text
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
for col_idx, val in enumerate(row_data, 1):
|
|
196
196
|
cell = ws.cell(row=current_row_idx, column=col_idx)
|
|
197
197
|
cell.border = cell_border
|
|
198
198
|
cell.font = Font(name="Calibri", size=10)
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
h_name = col_types.get(col_idx, "")
|
|
201
201
|
val_str = str(val).strip()
|
|
202
202
|
val_lower = val_str.lower()
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
# Alignments & Styling based on column semantic
|
|
205
205
|
if any(k in h_name for k in ["steps", "expected", "title", "scenario", "pre-condition"]):
|
|
206
206
|
cell.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
|
|
@@ -246,23 +246,23 @@ def create_styled_excel(tables: List[Tuple[List[str], List[List[str]]]], output_
|
|
|
246
246
|
for col_idx, header in enumerate(primary_headers, 1):
|
|
247
247
|
h_lower = header.lower().strip()
|
|
248
248
|
col_letter = get_column_letter(col_idx)
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
# Check presets
|
|
251
251
|
target_width = None
|
|
252
252
|
for key, width in width_map.items():
|
|
253
253
|
if key in h_lower:
|
|
254
254
|
target_width = width
|
|
255
255
|
break
|
|
256
|
-
|
|
256
|
+
|
|
257
257
|
if not target_width:
|
|
258
258
|
# Dynamic max calculation
|
|
259
259
|
max_len = len(header)
|
|
260
260
|
for r in all_rows:
|
|
261
261
|
if col_idx - 1 < len(r):
|
|
262
262
|
lines = str(r[col_idx - 1]).split('\n')
|
|
263
|
-
for
|
|
264
|
-
if len(
|
|
265
|
-
max_len = len(
|
|
263
|
+
for line_val in lines:
|
|
264
|
+
if len(line_val) > max_len:
|
|
265
|
+
max_len = len(line_val)
|
|
266
266
|
target_width = min(max(max_len + 3, 12), 45)
|
|
267
267
|
|
|
268
268
|
ws.column_dimensions[col_letter].width = target_width
|
|
@@ -278,13 +278,13 @@ def main():
|
|
|
278
278
|
parser.add_argument("input", help="Path to input Markdown file (.md)")
|
|
279
279
|
parser.add_argument("-o", "--output", help="Path to output Excel file (.xlsx). Defaults to same name as input.")
|
|
280
280
|
parser.add_argument("--sheet-name", default="Test Cases", help="Worksheet title (default: Test Cases)")
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
args = parser.parse_args()
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
if not os.path.exists(args.input):
|
|
285
285
|
print(f"❌ Error: Input file not found: {args.input}", file=sys.stderr)
|
|
286
286
|
sys.exit(1)
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
output_path = args.output
|
|
289
289
|
if not output_path:
|
|
290
290
|
base, _ = os.path.splitext(args.input)
|
package/tools/micro_mutator.py
CHANGED
|
@@ -22,11 +22,11 @@ import os
|
|
|
22
22
|
import re
|
|
23
23
|
import sys
|
|
24
24
|
import json
|
|
25
|
+
import ast
|
|
25
26
|
import shlex
|
|
26
27
|
import html as html_lib
|
|
27
28
|
import argparse
|
|
28
29
|
import subprocess
|
|
29
|
-
import tempfile
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
from typing import List, Dict, Any, Tuple
|
|
32
32
|
|
|
@@ -60,11 +60,12 @@ MUTATION_RULES = [
|
|
|
60
60
|
(r"\breturn\s+true\b", "return false", "Return Value Inversion (return true -> return false)"),
|
|
61
61
|
(r"\breturn\s+false\b", "return true", "Return Value Inversion (return false -> return true)"),
|
|
62
62
|
(r"\breturn\s+0\b", "return -1", "Return Value Shift (return 0 -> return -1)"),
|
|
63
|
+
(r"\s\?\?\s", " || ", "Nullish Coalescing Inversion (?? -> ||)"),
|
|
63
64
|
]
|
|
64
65
|
|
|
65
66
|
# Action statement pattern for Statement Deletion Mutants
|
|
66
67
|
ACTION_STATEMENT_PATTERN = re.compile(
|
|
67
|
-
r'\.(click|submit|save|update|delete|send|fill|press|type|execute)\s*\(',
|
|
68
|
+
r'\.(click|submit|save|update|delete|send|fill|press|type|execute|check|uncheck|hover|dragTo|selectOption|clear|focus|setInputFiles)\s*\(',
|
|
68
69
|
re.IGNORECASE
|
|
69
70
|
)
|
|
70
71
|
|
|
@@ -75,7 +76,12 @@ ASSERTION_PATTERN = re.compile(
|
|
|
75
76
|
|
|
76
77
|
# Tautological / fake assertions that do not test application logic
|
|
77
78
|
TAUTOLOGY_PATTERN = re.compile(
|
|
78
|
-
r'(?:assert\s+(?:true|1\s*==\s*1)\b
|
|
79
|
+
r'(?:assert\s+(?:true|1\s*==\s*1)\b'
|
|
80
|
+
r'|\bassert\s+([a-zA-Z0-9_\.]+(?:\([^)]*\))?)\s*==\s*\1\b'
|
|
81
|
+
r'|\bassert\s+(?:len\s*\([^)]*\)|[a-zA-Z0-9_\.]+\.(?:length|size\(\)))\s*(?:>=\s*0|>\s*-(?:1|0))\b'
|
|
82
|
+
r'|assertEquals\s*\(\s*([a-zA-Z_]\w*)\s*,\s*\1\s*\)'
|
|
83
|
+
r'|expect\s*\(\s*([a-zA-Z_]\w*)\s*\)\.toBe\s*\(\s*\1\s*\)'
|
|
84
|
+
r'|assertTrue\s*\(\s*true\s*\))',
|
|
79
85
|
re.IGNORECASE
|
|
80
86
|
)
|
|
81
87
|
|
|
@@ -260,9 +266,9 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
260
266
|
"""
|
|
261
267
|
if not mutants:
|
|
262
268
|
has_asserts = any(
|
|
263
|
-
ASSERTION_PATTERN.search(
|
|
264
|
-
for
|
|
265
|
-
if not
|
|
269
|
+
ASSERTION_PATTERN.search(line) and not TAUTOLOGY_PATTERN.search(line)
|
|
270
|
+
for line in code.splitlines()
|
|
271
|
+
if not line.strip().startswith(("//", "#", "/*"))
|
|
266
272
|
)
|
|
267
273
|
return mutants, (100.0 if has_asserts else 0.0)
|
|
268
274
|
|
|
@@ -271,8 +277,8 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
271
277
|
|
|
272
278
|
# Identify lines containing assertions
|
|
273
279
|
assertion_entries = []
|
|
274
|
-
for idx,
|
|
275
|
-
s =
|
|
280
|
+
for idx, line in enumerate(lines, start=1):
|
|
281
|
+
s = line.strip()
|
|
276
282
|
if s.startswith(("//", "#", "/*")):
|
|
277
283
|
continue
|
|
278
284
|
if ASSERTION_PATTERN.search(s) and not TAUTOLOGY_PATTERN.search(s):
|
|
@@ -302,6 +308,10 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
302
308
|
|
|
303
309
|
# Case 1: Mutation directly in an assertion statement
|
|
304
310
|
if ASSERTION_PATTERN.search(snippet):
|
|
311
|
+
if TAUTOLOGY_PATTERN.search(snippet):
|
|
312
|
+
m["status"] = "SURVIVED"
|
|
313
|
+
m["reason"] = "Mutant in tautological/dummy assertion - cannot kill mutant without genuine assertion logic"
|
|
314
|
+
continue
|
|
305
315
|
m["status"] = "KILLED"
|
|
306
316
|
m["reason"] = "Direct assertion mutation caught by verification condition"
|
|
307
317
|
killed_count += 1
|
|
@@ -325,6 +335,13 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
325
335
|
tgt = asgn.group(1)
|
|
326
336
|
if any(re.search(rf'\b{re.escape(t)}\b', l_code) for t in dataflow_tokens):
|
|
327
337
|
dataflow_tokens.add(tgt)
|
|
338
|
+
destruct = re.match(r'^\s*(?:(?:const|let|var|val)\s+)?[\{\(]([^\}\)]+)[\}\)]\s*=', l_code)
|
|
339
|
+
if destruct:
|
|
340
|
+
vars_in_destruct = [v.strip().split(':')[0].strip() for v in destruct.group(1).split(',') if v.strip()]
|
|
341
|
+
if any(re.search(rf'\b{re.escape(t)}\b', l_code) for t in dataflow_tokens):
|
|
342
|
+
for v in vars_in_destruct:
|
|
343
|
+
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', v):
|
|
344
|
+
dataflow_tokens.add(v)
|
|
328
345
|
|
|
329
346
|
# Check if any assertion references the variables mutated in this logic branch or downstream dataflow
|
|
330
347
|
has_variable_link = any(any(re.search(rf'\b{re.escape(tok)}\b', a_text) for tok in dataflow_tokens) for _, a_text in downstream)
|
|
@@ -489,9 +506,9 @@ def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "
|
|
|
489
506
|
valid_exts = {".groovy", ".py", ".ts", ".js"}
|
|
490
507
|
file_scores = {}
|
|
491
508
|
for root, _, files in os.walk(target):
|
|
492
|
-
for
|
|
493
|
-
if any(
|
|
494
|
-
file_full = os.path.join(root,
|
|
509
|
+
for filename in files:
|
|
510
|
+
if any(filename.endswith(ext) for ext in valid_exts):
|
|
511
|
+
file_full = os.path.join(root, filename)
|
|
495
512
|
try:
|
|
496
513
|
with open(file_full, "r", encoding="utf-8", errors="ignore") as fp:
|
|
497
514
|
code = fp.read()
|
|
@@ -634,17 +651,17 @@ def main():
|
|
|
634
651
|
print(format_junit(report))
|
|
635
652
|
else:
|
|
636
653
|
mode_tag = "LIVE DYNAMIC RUNNER" if report.get("mode") == "DYNAMIC" else "STATIC HEURISTIC"
|
|
637
|
-
mode_desc = f"LIVE DYNAMIC RUNNER (cmd: '{args.test_cmd}')" if report.get("mode") == "DYNAMIC" else "STATIC HEURISTIC (fast
|
|
638
|
-
print(
|
|
654
|
+
mode_desc = f"LIVE DYNAMIC RUNNER (cmd: '{args.test_cmd}')" if report.get("mode") == "DYNAMIC" else "STATIC HEURISTIC (fast heuristic assertion sensitivity scan; pass --test-cmd for dynamic run)"
|
|
655
|
+
print("\n======================================================================")
|
|
639
656
|
print(f" ASSERTION SENSITIVITY & SYNTACTIC MUTATION SENTINEL [{mode_tag}] ")
|
|
640
|
-
print(
|
|
657
|
+
print("======================================================================")
|
|
641
658
|
print(f" Target Scope : {report['target']}")
|
|
642
659
|
print(f" Evaluation Mode : {mode_desc}")
|
|
643
660
|
print(f" Mutants Injected : {report['total_mutants']}")
|
|
644
661
|
print(f" Mutants Caught/Killed : {report['killed']}")
|
|
645
662
|
print(f" Mutants Survived : {report['survived']}")
|
|
646
663
|
print(f" Assertion Sensitivity : {report['mutation_score']}% (Required: >={report['min_score']}%)")
|
|
647
|
-
print(
|
|
664
|
+
print("======================================================================\n")
|
|
648
665
|
|
|
649
666
|
for m in report['mutants'][:6]:
|
|
650
667
|
status_tag = "[KILLED] " if m["status"] == "KILLED" else "[SURVIVED]"
|
|
@@ -88,16 +88,16 @@ class NetworkMockShield:
|
|
|
88
88
|
code = [
|
|
89
89
|
f"# Mock API Interception for '{url_pattern}'",
|
|
90
90
|
"import time, json",
|
|
91
|
-
|
|
91
|
+
"def handle_route(route):",
|
|
92
92
|
]
|
|
93
93
|
if delay_ms > 0:
|
|
94
94
|
code.append(f" time.sleep({delay_ms / 1000.0})")
|
|
95
95
|
code.extend([
|
|
96
|
-
|
|
96
|
+
" route.fulfill(",
|
|
97
97
|
f" status={status},",
|
|
98
|
-
|
|
98
|
+
" content_type='application/json',",
|
|
99
99
|
f" body=json.dumps({body_json})",
|
|
100
|
-
|
|
100
|
+
" )",
|
|
101
101
|
f"page.route('{url_pattern}', handle_route)"
|
|
102
102
|
])
|
|
103
103
|
return "\n".join(code)
|
|
@@ -119,52 +119,52 @@ class NetworkMockShield:
|
|
|
119
119
|
if mode == "record":
|
|
120
120
|
if lang == "python":
|
|
121
121
|
return "\n".join([
|
|
122
|
-
|
|
122
|
+
"# --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright Python) ---",
|
|
123
123
|
f"# Dumps live network traffic to '{har_path}'",
|
|
124
|
-
|
|
124
|
+
"context = browser.new_context(",
|
|
125
125
|
f" record_har_path='{har_path}',",
|
|
126
|
-
|
|
126
|
+
" record_har_mode='minimal',",
|
|
127
127
|
f" record_har_url_filter='{url_glob}'",
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
")",
|
|
129
|
+
"page = context.new_page()",
|
|
130
130
|
f"# Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
|
|
131
131
|
])
|
|
132
132
|
else:
|
|
133
133
|
return "\n".join([
|
|
134
|
-
|
|
134
|
+
"// --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright TS) ---",
|
|
135
135
|
f"// Dumps live network traffic to '{har_path}'",
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
"const context = await browser.newContext({",
|
|
137
|
+
" recordHar: {",
|
|
138
138
|
f" path: '{har_path}',",
|
|
139
|
-
|
|
139
|
+
" mode: 'minimal',",
|
|
140
140
|
f" urlFilter: '{url_glob}'",
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
" }",
|
|
142
|
+
"});",
|
|
143
|
+
"const page = await context.newPage();",
|
|
144
144
|
f"// Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
|
|
145
145
|
])
|
|
146
146
|
else:
|
|
147
147
|
# Replay Mode
|
|
148
148
|
if lang == "python":
|
|
149
149
|
return "\n".join([
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
"# --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright Python) ---",
|
|
151
|
+
"# Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
|
|
152
|
+
"page.route_from_har(",
|
|
153
153
|
f" har_id='{har_path}',",
|
|
154
154
|
f" url='{url_glob}',",
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
" not_found='fallback',",
|
|
156
|
+
" update=False",
|
|
157
|
+
")"
|
|
158
158
|
])
|
|
159
159
|
else:
|
|
160
160
|
return "\n".join([
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
"// --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright TS) ---",
|
|
162
|
+
"// Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
|
|
163
163
|
f"await page.routeFromHAR('{har_path}', {{",
|
|
164
164
|
f" url: '{url_glob}',",
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
" notFound: 'fallback',",
|
|
166
|
+
" update: false",
|
|
167
|
+
"});"
|
|
168
168
|
])
|
|
169
169
|
|
|
170
170
|
@classmethod
|
package/tools/newman_analyzer.py
CHANGED
|
@@ -54,7 +54,6 @@ def parse_newman_json(report_path: str) -> Dict[str, Any]:
|
|
|
54
54
|
|
|
55
55
|
run = data.get("run", {})
|
|
56
56
|
stats = run.get("stats", {})
|
|
57
|
-
timings = run.get("timings", {})
|
|
58
57
|
executions = run.get("executions", [])
|
|
59
58
|
failures = run.get("failures", [])
|
|
60
59
|
|
|
@@ -110,7 +109,7 @@ def parse_newman_json(report_path: str) -> Dict[str, Any]:
|
|
|
110
109
|
err = f.get("error", {})
|
|
111
110
|
source = f.get("source", {})
|
|
112
111
|
cursor = f.get("cursor", {})
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
req_name = source.get("name", cursor.get("ref", "Request"))
|
|
115
114
|
assert_name = err.get("test", err.get("name", "Assertion Failure"))
|
|
116
115
|
message = err.get("message", "Unknown assertion error")
|
|
@@ -351,7 +350,7 @@ def main():
|
|
|
351
350
|
print("-" * 90)
|
|
352
351
|
|
|
353
352
|
# Print SLA Verdict
|
|
354
|
-
print(
|
|
353
|
+
print("🎯 [QUALITY GATE SLA VERDICT]")
|
|
355
354
|
if passed:
|
|
356
355
|
print("✅ ALL API FUNCTIONAL & SLA GATES PASSED! Safe to merge/deploy.")
|
|
357
356
|
else:
|
package/tools/omni_emitter.py
CHANGED
|
@@ -12,18 +12,15 @@ Supports:
|
|
|
12
12
|
5. Cypress JavaScript (cypress-js)
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
from typing import Dict, List, Optional
|
|
15
|
+
from typing import Dict
|
|
17
16
|
|
|
18
17
|
try:
|
|
19
18
|
from tools.universal_ir import (
|
|
20
|
-
ProjectIR,
|
|
21
|
-
ActionType, LocatorStrategy, AssertionType
|
|
19
|
+
ProjectIR, ActionType, LocatorStrategy
|
|
22
20
|
)
|
|
23
21
|
except ImportError:
|
|
24
22
|
from universal_ir import (
|
|
25
|
-
ProjectIR,
|
|
26
|
-
ActionType, LocatorStrategy, AssertionType
|
|
23
|
+
ProjectIR, ActionType, LocatorStrategy
|
|
27
24
|
)
|
|
28
25
|
|
|
29
26
|
|
|
@@ -243,7 +240,7 @@ export async function safeInteract(
|
|
|
243
240
|
for tc in project.test_cases:
|
|
244
241
|
t_code.extend([
|
|
245
242
|
f"test('{escape_js(tc.id)}: {escape_js(tc.title)}', async ({{ page }}) => {{",
|
|
246
|
-
|
|
243
|
+
" const uniqueCode = 'AUTO_' + Date.now();",
|
|
247
244
|
])
|
|
248
245
|
|
|
249
246
|
# Emit Hermetic Pre-flight Fixtures if present
|
|
@@ -445,7 +442,7 @@ export async function safeInteract(
|
|
|
445
442
|
"import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI",
|
|
446
443
|
"import pages.*",
|
|
447
444
|
"",
|
|
448
|
-
|
|
445
|
+
"String uniqueCode = BasePage.uniqueCode('AUTO_')",
|
|
449
446
|
"",
|
|
450
447
|
]
|
|
451
448
|
for s in tc.steps:
|
package/tools/omni_parser.py
CHANGED
|
@@ -10,7 +10,6 @@ the Universal Intermediate Representation (universal_ir.ProjectIR).
|
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
-
import sys
|
|
14
13
|
from typing import Dict, List, Optional, Tuple
|
|
15
14
|
|
|
16
15
|
try:
|
|
@@ -38,7 +37,7 @@ class OmniParser:
|
|
|
38
37
|
@staticmethod
|
|
39
38
|
def detect_language_and_framework(file_path: str, content: str) -> Tuple[str, str]:
|
|
40
39
|
ext = os.path.splitext(file_path)[1].lower()
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
# Groovy / Katalon
|
|
43
42
|
if ext == ".groovy":
|
|
44
43
|
return "groovy", "katalon"
|
|
@@ -369,8 +368,8 @@ class OmniParser:
|
|
|
369
368
|
# 0. Detect Hardcoded Sleep Antipatterns (Thread.sleep, WebUI.delay, cy.wait, time.sleep)
|
|
370
369
|
sleep_m = re.search(r"(?:Thread\.sleep|WebUI\.delay|time\.sleep|cy\.wait)\s*\(\s*(\d+)\s*\)", line_str, re.IGNORECASE)
|
|
371
370
|
if sleep_m:
|
|
372
|
-
|
|
373
|
-
ms =
|
|
371
|
+
sleep_val = int(sleep_m.group(1))
|
|
372
|
+
ms = sleep_val * 1000 if sleep_val <= 60 else sleep_val # WebUI.delay(3) vs Thread.sleep(3000)
|
|
374
373
|
steps.append(StepIR(
|
|
375
374
|
action=ActionType.WAIT,
|
|
376
375
|
wait_strategy=WaitStrategy.STATIC_SLEEP,
|
|
@@ -389,7 +388,8 @@ class OmniParser:
|
|
|
389
388
|
if cy_act == "click":
|
|
390
389
|
steps.append(StepIR(action=ActionType.CLICK, target=sel, description=f"Cypress click on {sel}"))
|
|
391
390
|
elif cy_act in ["type", "clear"]:
|
|
392
|
-
|
|
391
|
+
m_type = re.search(r"(?:\"([^\"]*)\"|'([^']*)')", cy_args)
|
|
392
|
+
type_val = extract_quoted_string((m_type.group(1), m_type.group(2))) if m_type else ""
|
|
393
393
|
steps.append(StepIR(action=ActionType.FILL, target=sel, value=type_val, description=f"Cypress type on {sel}"))
|
|
394
394
|
elif cy_act == "should":
|
|
395
395
|
steps.append(StepIR(action=ActionType.ASSERT, target=sel, assertion_type=AssertionType.VISIBLE, description=line_str))
|
package/tools/omni_transpiler.py
CHANGED
|
@@ -12,15 +12,15 @@ import os
|
|
|
12
12
|
import sys
|
|
13
13
|
import json
|
|
14
14
|
import argparse
|
|
15
|
-
from typing import Dict,
|
|
15
|
+
from typing import Dict, Any, Optional
|
|
16
16
|
|
|
17
17
|
# Dynamic imports
|
|
18
18
|
try:
|
|
19
|
-
from tools.universal_ir import
|
|
19
|
+
from tools.universal_ir import resolve_hermetic_fixtures
|
|
20
20
|
from tools.omni_parser import OmniParser
|
|
21
21
|
from tools.omni_emitter import OmniEmitter
|
|
22
22
|
except ImportError:
|
|
23
|
-
from universal_ir import
|
|
23
|
+
from universal_ir import resolve_hermetic_fixtures
|
|
24
24
|
from omni_parser import OmniParser
|
|
25
25
|
from omni_emitter import OmniEmitter
|
|
26
26
|
|
|
@@ -65,7 +65,7 @@ def scan_repository(repo_path: str) -> Dict[str, Any]:
|
|
|
65
65
|
if not os.path.exists(repo_path):
|
|
66
66
|
raise FileNotFoundError(f"Path does not exist: {repo_path}")
|
|
67
67
|
|
|
68
|
-
inventory = {
|
|
68
|
+
inventory: Dict[str, Any] = {
|
|
69
69
|
"repository": os.path.abspath(repo_path),
|
|
70
70
|
"total_files_scanned": 0,
|
|
71
71
|
"languages": {},
|
|
@@ -88,7 +88,7 @@ def scan_repository(repo_path: str) -> Dict[str, Any]:
|
|
|
88
88
|
try:
|
|
89
89
|
p_ir = OmniParser.parse_file(file_full)
|
|
90
90
|
inventory["total_files_scanned"] += 1
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
# Language counts
|
|
93
93
|
inventory["languages"][p_ir.source_language] = inventory["languages"].get(p_ir.source_language, 0) + 1
|
|
94
94
|
inventory["frameworks"][p_ir.source_framework] = inventory["frameworks"].get(p_ir.source_framework, 0) + 1
|