synapse-hub 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/failure-analyst.md +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +394 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +418 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Universal Data Flow & Teardown Tracer (Taint Analysis)
|
|
4
|
+
----------------------------------------------------
|
|
5
|
+
Performs AST-level Taint Analysis tracking test data lifecycle across test scripts:
|
|
6
|
+
1. SOURCE: Generation of dynamic unique identifiers (uniqueCode, DataGenerator).
|
|
7
|
+
2. PROPAGATE: Insertion / persistence into database or UI state.
|
|
8
|
+
3. SINK: Automated Teardown / Deletion / Cleanup.
|
|
9
|
+
|
|
10
|
+
Detects orphan test junk that pollutes test environments.
|
|
11
|
+
Supports dynamic target repository resolution (CLI flag, CWD auto-detect, ENV var).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
import glob
|
|
19
|
+
import argparse
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
23
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
24
|
+
try:
|
|
25
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
26
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
32
|
+
REPORTS_DIR = os.path.join(HUB_ROOT, "reports")
|
|
33
|
+
OUTPUT_JSON = os.path.join(REPORTS_DIR, "data_flow_trace_report.json")
|
|
34
|
+
|
|
35
|
+
# ANSI Color codes
|
|
36
|
+
GREEN = "\033[92m"
|
|
37
|
+
RED = "\033[91m"
|
|
38
|
+
YELLOW = "\033[93m"
|
|
39
|
+
CYAN = "\033[96m"
|
|
40
|
+
BOLD = "\033[1m"
|
|
41
|
+
RESET = "\033[0m"
|
|
42
|
+
|
|
43
|
+
# Taint patterns
|
|
44
|
+
SOURCE_PATTERNS = [
|
|
45
|
+
re.compile(r'uniqueCode', re.IGNORECASE),
|
|
46
|
+
re.compile(r'DataGenerator\.', re.IGNORECASE),
|
|
47
|
+
re.compile(r'System\.currentTimeMillis', re.IGNORECASE),
|
|
48
|
+
re.compile(r'random[A-Z]\w+', re.IGNORECASE),
|
|
49
|
+
re.compile(r'ensure\w+Exists', re.IGNORECASE),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
PROPAGATE_PATTERNS = [
|
|
53
|
+
re.compile(r'\.save\w*\(', re.IGNORECASE),
|
|
54
|
+
re.compile(r'\.themMoi\w*\(', re.IGNORECASE),
|
|
55
|
+
re.compile(r'\.fill\w*\(', re.IGNORECASE),
|
|
56
|
+
re.compile(r'\.create\w*\(', re.IGNORECASE),
|
|
57
|
+
re.compile(r'\.insert\w*\(', re.IGNORECASE),
|
|
58
|
+
re.compile(r'\b(?:save|persist|insert|themMoi)[A-Z]\w*\(', re.IGNORECASE),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
SINK_PATTERNS = [
|
|
62
|
+
re.compile(r'\.delete\w*\(', re.IGNORECASE),
|
|
63
|
+
re.compile(r'\.xoa\w*\(', re.IGNORECASE),
|
|
64
|
+
re.compile(r'\.teardown\w*\(', re.IGNORECASE),
|
|
65
|
+
re.compile(r'\.cleanUp\w*\(', re.IGNORECASE),
|
|
66
|
+
re.compile(r'confirmDialog', re.IGNORECASE),
|
|
67
|
+
re.compile(r'ICON_DELETE', re.IGNORECASE),
|
|
68
|
+
re.compile(r'Xoá|Xoa|delete', re.IGNORECASE),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
73
|
+
"""
|
|
74
|
+
Resolves the target repository path:
|
|
75
|
+
1. Explicit CLI flag: --repo / --path or positional target.
|
|
76
|
+
2. Environment variable: TARGET_PROJECT_DIR.
|
|
77
|
+
3. Current Working Directory (if different from Hub and has Scripts/ or tests/).
|
|
78
|
+
4. Default fallback: QA Hub root (parent of tools/).
|
|
79
|
+
"""
|
|
80
|
+
chosen = cli_repo or cli_target
|
|
81
|
+
if chosen:
|
|
82
|
+
resolved = os.path.abspath(chosen)
|
|
83
|
+
if not os.path.exists(resolved):
|
|
84
|
+
raise FileNotFoundError(f"Target repository path not found: {resolved}")
|
|
85
|
+
return resolved
|
|
86
|
+
|
|
87
|
+
env_dir = os.environ.get("TARGET_PROJECT_DIR")
|
|
88
|
+
if env_dir and os.path.exists(env_dir):
|
|
89
|
+
return os.path.abspath(env_dir)
|
|
90
|
+
|
|
91
|
+
cwd = os.path.abspath(os.getcwd())
|
|
92
|
+
if cwd.lower() != HUB_ROOT.lower():
|
|
93
|
+
if os.path.exists(os.path.join(cwd, "Scripts")) or os.path.exists(os.path.join(cwd, "tests")):
|
|
94
|
+
return cwd
|
|
95
|
+
|
|
96
|
+
return HUB_ROOT
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def trace_script_dataflow(file_path, repo_root=None):
|
|
100
|
+
"""Traces source, propagation, and sink in a test script."""
|
|
101
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
102
|
+
content = f.read()
|
|
103
|
+
|
|
104
|
+
base_root = repo_root or HUB_ROOT
|
|
105
|
+
try:
|
|
106
|
+
rel_path = os.path.relpath(file_path, base_root)
|
|
107
|
+
except ValueError:
|
|
108
|
+
rel_path = os.path.abspath(file_path)
|
|
109
|
+
|
|
110
|
+
tc_name = os.path.basename(os.path.dirname(file_path))
|
|
111
|
+
|
|
112
|
+
has_source = any(p.search(content) for p in SOURCE_PATTERNS)
|
|
113
|
+
has_propagate = any(p.search(content) for p in PROPAGATE_PATTERNS)
|
|
114
|
+
has_sink = any(p.search(content) for p in SINK_PATTERNS)
|
|
115
|
+
|
|
116
|
+
# Classification
|
|
117
|
+
tc_lower = tc_name.lower()
|
|
118
|
+
is_delete = any(k in tc_lower for k in ["xoa", "xóa", "xoá", "delete", "cleanup", "remove"])
|
|
119
|
+
is_edit = any(k in tc_lower for k in ["sua", "sửa", "chỉnh sửa", "chinh sua", "edit", "update", "modify"])
|
|
120
|
+
|
|
121
|
+
if is_delete or (has_sink and not has_propagate):
|
|
122
|
+
lifecycle = "CLEANUP_SUITE"
|
|
123
|
+
description = "Dedicated deletion / teardown test suite."
|
|
124
|
+
elif has_propagate and has_sink:
|
|
125
|
+
lifecycle = "IDEMPOTENT_CLEAN"
|
|
126
|
+
description = "Complete lifecycle: Dynamic creation + verified teardown."
|
|
127
|
+
elif has_propagate and not has_sink:
|
|
128
|
+
if is_edit:
|
|
129
|
+
lifecycle = "MUTATION_UPDATE"
|
|
130
|
+
description = "Modifies existing record in-place without creating new entities."
|
|
131
|
+
else:
|
|
132
|
+
lifecycle = "ORPHAN_DATA_RISK"
|
|
133
|
+
description = "Creates persistent entity on AUT without local teardown."
|
|
134
|
+
else:
|
|
135
|
+
lifecycle = "READ_ONLY"
|
|
136
|
+
description = "Read-only interaction flow."
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"file": rel_path,
|
|
140
|
+
"tc_name": tc_name,
|
|
141
|
+
"has_source": has_source,
|
|
142
|
+
"has_propagate": has_propagate,
|
|
143
|
+
"has_sink": has_sink,
|
|
144
|
+
"lifecycle": lifecycle,
|
|
145
|
+
"description": description
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def run_data_flow_tracer(target_dir=None, repo_root=None):
|
|
150
|
+
"""Scans all test cases in target directory and traces data flow."""
|
|
151
|
+
root = repo_root or HUB_ROOT
|
|
152
|
+
scan_dir = target_dir or os.path.join(root, "Scripts")
|
|
153
|
+
script_files = []
|
|
154
|
+
if os.path.isdir(scan_dir):
|
|
155
|
+
for ext in ("*.groovy", "*.ts", "*.js", "*.py"):
|
|
156
|
+
script_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
|
|
157
|
+
|
|
158
|
+
results = []
|
|
159
|
+
counts = {
|
|
160
|
+
"IDEMPOTENT_CLEAN": 0,
|
|
161
|
+
"READ_ONLY": 0,
|
|
162
|
+
"CLEANUP_SUITE": 0,
|
|
163
|
+
"MUTATION_UPDATE": 0,
|
|
164
|
+
"ORPHAN_DATA_RISK": 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for s in sorted(script_files):
|
|
168
|
+
res = trace_script_dataflow(s, repo_root=root)
|
|
169
|
+
results.append(res)
|
|
170
|
+
counts[res["lifecycle"]] = counts.get(res["lifecycle"], 0) + 1
|
|
171
|
+
|
|
172
|
+
total = len(script_files)
|
|
173
|
+
safe_count = counts["IDEMPOTENT_CLEAN"] + counts["READ_ONLY"] + counts["CLEANUP_SUITE"] + counts["MUTATION_UPDATE"]
|
|
174
|
+
purity_rate = (safe_count / max(1, total)) * 100 if total > 0 else 100.0
|
|
175
|
+
|
|
176
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
177
|
+
print(f"{BOLD}{CYAN} Universal Data Flow & TEARDOWN TRACER (Taint Audit) {RESET}")
|
|
178
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
179
|
+
print(f" Target Scope : {scan_dir}")
|
|
180
|
+
print(f" Total Audited Scripts : {total} Test Cases")
|
|
181
|
+
print(f" Idempotent Clean : {GREEN}{counts['IDEMPOTENT_CLEAN']}{RESET} (Dynamic data + verified teardown)")
|
|
182
|
+
print(f" Read-Only Invariant : {CYAN}{counts['READ_ONLY']}{RESET} (Search, filter, navigation, export)")
|
|
183
|
+
print(f" Dedicated Cleanup Suites: {GREEN}{counts['CLEANUP_SUITE']}{RESET} (Explicit deletion test cases)")
|
|
184
|
+
print(f" In-Place Mutations : {YELLOW}{counts['MUTATION_UPDATE']}{RESET} (Edit/update existing records)")
|
|
185
|
+
print(f" Orphan Data Risk : {RED if counts['ORPHAN_DATA_RISK'] > 0 else GREEN}{counts['ORPHAN_DATA_RISK']}{RESET} (Persistent creation without teardown)")
|
|
186
|
+
print(f" Database Hygiene Purity : {GREEN if purity_rate >= 90 else YELLOW}{purity_rate:.1f}%{RESET}")
|
|
187
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
188
|
+
|
|
189
|
+
# Save artifact
|
|
190
|
+
target_reports = os.path.join(root, "reports")
|
|
191
|
+
os.makedirs(target_reports, exist_ok=True)
|
|
192
|
+
report_file = os.path.join(target_reports, "data_flow_trace_report.json")
|
|
193
|
+
|
|
194
|
+
report_data = {
|
|
195
|
+
"timestamp": datetime.now().isoformat(),
|
|
196
|
+
"scan_dir": scan_dir,
|
|
197
|
+
"total_scripts": total,
|
|
198
|
+
"hygiene_purity_pct": round(purity_rate, 2),
|
|
199
|
+
"lifecycle_counts": counts,
|
|
200
|
+
"details": results
|
|
201
|
+
}
|
|
202
|
+
with open(report_file, "w", encoding="utf-8") as f:
|
|
203
|
+
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
|
204
|
+
print(f"Detailed data-flow artifact saved to: {report_file}\n")
|
|
205
|
+
|
|
206
|
+
if root != HUB_ROOT:
|
|
207
|
+
# Also mirror to HUB_ROOT reports for central auditing
|
|
208
|
+
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
209
|
+
with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
|
|
210
|
+
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
|
211
|
+
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def main():
|
|
216
|
+
parser = argparse.ArgumentParser(description="Universal Data Flow & Teardown Tracer")
|
|
217
|
+
parser.add_argument("target", nargs="?", default=None, help="Target repository or directory containing test scripts")
|
|
218
|
+
parser.add_argument("--repo", default=None, help="Target repository root")
|
|
219
|
+
parser.add_argument("--dir", "-d", default=None, help="Directory containing test scripts to trace")
|
|
220
|
+
args = parser.parse_args()
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
repo_root = resolve_target_repo(cli_target=args.target, cli_repo=args.repo)
|
|
224
|
+
except FileNotFoundError as e:
|
|
225
|
+
print(f"❌ Error: {e}")
|
|
226
|
+
return 1
|
|
227
|
+
|
|
228
|
+
target_dir = args.dir
|
|
229
|
+
if not target_dir:
|
|
230
|
+
# Check if target is explicitly a directory with scripts
|
|
231
|
+
if args.target and os.path.isdir(args.target) and not os.path.exists(os.path.join(args.target, "Scripts")):
|
|
232
|
+
target_dir = args.target
|
|
233
|
+
else:
|
|
234
|
+
target_dir = os.path.join(repo_root, "Scripts")
|
|
235
|
+
|
|
236
|
+
return run_data_flow_tracer(target_dir=target_dir, repo_root=repo_root)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
sys.exit(main())
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Shift-Right Traffic & User Session Replay Engine (HAR / RUM to Test)
|
|
4
|
+
--------------------------------------------------------------------------------
|
|
5
|
+
Transforms production HTTP Archives (.har), RUM logs, and network telemetry into
|
|
6
|
+
executable, 100% PII-sanitized automated E2E test suites (Playwright / Clean POM).
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
1. Ingestion of standard HTTP Archive (.har) and JSON network traces.
|
|
10
|
+
2. Autonomous PII & Credential Sanitizer:
|
|
11
|
+
- Redacts Bearer tokens, Cookies, API keys, passwords, credit cards, CVVs, SSNs.
|
|
12
|
+
- Parameterizes dynamic test data with idempotent prefixes (AUTO_*, DataGenerator).
|
|
13
|
+
3. Noise & Static Asset Elimination:
|
|
14
|
+
- Filters out images (.png, .jpg), styles (.css), fonts (.woff), and tracking pings.
|
|
15
|
+
- Preserves critical XHR/Fetch API endpoints and page navigations.
|
|
16
|
+
4. Test Code Generator:
|
|
17
|
+
- Emits production-grade Playwright (TypeScript/Python) test cases with route mocks
|
|
18
|
+
and state assertions replicating authentic production user journeys.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
import argparse
|
|
26
|
+
from typing import Dict, Any, List, Optional
|
|
27
|
+
from urllib.parse import urlparse
|
|
28
|
+
|
|
29
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
30
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
31
|
+
try:
|
|
32
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
33
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
QA_HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
39
|
+
|
|
40
|
+
STATIC_EXTENSIONS = (
|
|
41
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico",
|
|
42
|
+
".css", ".woff", ".woff2", ".ttf", ".eot", ".map", ".mp4"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
TRACKING_DOMAINS = (
|
|
46
|
+
"google-analytics.com", "googletagmanager.com", "hotjar.com",
|
|
47
|
+
"segment.io", "datadoghq.com", "sentry.io", "mixpanel.com",
|
|
48
|
+
"facebook.net", "doubleclick.net", "clarity.ms"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
PII_FIELD_PATTERNS = [
|
|
52
|
+
(re.compile(r'"(password|pass|secret|token|api_?key|access_?token)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "REDACTED_SECRET"'),
|
|
53
|
+
(re.compile(r'"(credit_?card|card_?number|pan)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "411111******1111"'),
|
|
54
|
+
(re.compile(r'"(cvv|cvc)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "999"'),
|
|
55
|
+
(re.compile(r'"(ssn|tax_?id)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "000-00-0000"'),
|
|
56
|
+
(re.compile(r'"(email)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "auto_test_user@enterprise.local"'),
|
|
57
|
+
(re.compile(r'"(phone|mobile)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "+15550199999"'),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
AUTH_HEADER_REGEX = re.compile(r'^(bearer|token|basic)\s+.*$', re.IGNORECASE)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TrafficConverter:
|
|
64
|
+
"""Converts recorded network traffic into sanitized E2E test scripts."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, sanitize_pii: bool = True):
|
|
67
|
+
self.sanitize_pii = sanitize_pii
|
|
68
|
+
|
|
69
|
+
def is_static_or_tracking(self, url: str) -> bool:
|
|
70
|
+
"""Determines whether a URL is a static asset or analytics beacon."""
|
|
71
|
+
parsed = urlparse(url)
|
|
72
|
+
path = parsed.path.lower()
|
|
73
|
+
|
|
74
|
+
# Check static extensions
|
|
75
|
+
if any(path.endswith(ext) for ext in STATIC_EXTENSIONS):
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
# Check tracking domains
|
|
79
|
+
netloc = parsed.netloc.lower()
|
|
80
|
+
if any(t_dom in netloc for t_dom in TRACKING_DOMAINS):
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def sanitize_payload(self, text: str) -> str:
|
|
86
|
+
"""Applies regex masking across sensitive PII fields."""
|
|
87
|
+
if not self.sanitize_pii or not text:
|
|
88
|
+
return text
|
|
89
|
+
|
|
90
|
+
sanitized = text
|
|
91
|
+
for pattern, replacement in PII_FIELD_PATTERNS:
|
|
92
|
+
sanitized = pattern.sub(replacement, sanitized)
|
|
93
|
+
|
|
94
|
+
return sanitized
|
|
95
|
+
|
|
96
|
+
def parse_har(self, har_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
97
|
+
"""Extracts and sanitizes actionable HTTP requests from HAR entries."""
|
|
98
|
+
entries = har_data.get("log", {}).get("entries", [])
|
|
99
|
+
actionable_requests = []
|
|
100
|
+
|
|
101
|
+
for entry in entries:
|
|
102
|
+
req = entry.get("request", {})
|
|
103
|
+
resp = entry.get("response", {})
|
|
104
|
+
url = req.get("url", "")
|
|
105
|
+
method = req.get("method", "GET").upper()
|
|
106
|
+
|
|
107
|
+
# Filter noise and static assets
|
|
108
|
+
if self.is_static_or_tracking(url):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Headers sanitization
|
|
112
|
+
headers = {}
|
|
113
|
+
for h in req.get("headers", []):
|
|
114
|
+
name = h.get("name", "")
|
|
115
|
+
val = h.get("value", "")
|
|
116
|
+
if name.lower() in ("authorization", "x-api-key", "cookie", "set-cookie"):
|
|
117
|
+
headers[name] = "Bearer REDACTED_TOKEN" if "bearer" in val.lower() else "REDACTED_AUTH"
|
|
118
|
+
else:
|
|
119
|
+
headers[name] = val
|
|
120
|
+
|
|
121
|
+
# Request Body sanitization
|
|
122
|
+
post_data = req.get("postData", {}).get("text", "")
|
|
123
|
+
if post_data:
|
|
124
|
+
post_data = self.sanitize_payload(post_data)
|
|
125
|
+
|
|
126
|
+
# Response Body sanitization
|
|
127
|
+
resp_content = resp.get("content", {}).get("text", "")
|
|
128
|
+
if resp_content:
|
|
129
|
+
resp_content = self.sanitize_payload(resp_content)
|
|
130
|
+
|
|
131
|
+
actionable_requests.append({
|
|
132
|
+
"method": method,
|
|
133
|
+
"url": url,
|
|
134
|
+
"status_code": resp.get("status", 200),
|
|
135
|
+
"headers": headers,
|
|
136
|
+
"request_body": post_data,
|
|
137
|
+
"response_body": resp_content,
|
|
138
|
+
"time_ms": entry.get("time", 50)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return actionable_requests
|
|
142
|
+
|
|
143
|
+
def generate_playwright_test(self, requests: List[Dict[str, Any]], test_name: str = "ProductionTrafficReplay") -> str:
|
|
144
|
+
"""Generates a complete, executable Playwright TypeScript E2E test."""
|
|
145
|
+
if not requests:
|
|
146
|
+
return "// No actionable HTTP requests found in traffic session."
|
|
147
|
+
|
|
148
|
+
base_url = "https://app.enterprise.local"
|
|
149
|
+
if requests:
|
|
150
|
+
parsed = urlparse(requests[0]["url"])
|
|
151
|
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
152
|
+
|
|
153
|
+
lines = [
|
|
154
|
+
"import { test, expect } from '@playwright/test';",
|
|
155
|
+
"",
|
|
156
|
+
f"/**",
|
|
157
|
+
f" * Automated Replay of Production Traffic Session",
|
|
158
|
+
f" * Generated by traffic_session_converter.py (agent-skills Shift-Right Engine)",
|
|
159
|
+
f" * Total Actions: {len(requests)} verified network interactions",
|
|
160
|
+
f" */",
|
|
161
|
+
f"test.describe('{test_name}', () => {{",
|
|
162
|
+
f" test('replays production user journey with PII masking', async ({{ page, request }}) => {{",
|
|
163
|
+
f" // 1. Base navigation",
|
|
164
|
+
f" await page.goto('{base_url}');",
|
|
165
|
+
""
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
step_num = 1
|
|
169
|
+
for req in requests:
|
|
170
|
+
method = req["method"]
|
|
171
|
+
url = req["url"]
|
|
172
|
+
status = req["status_code"]
|
|
173
|
+
parsed = urlparse(url)
|
|
174
|
+
endpoint = parsed.path + (f"?{parsed.query}" if parsed.query else "")
|
|
175
|
+
|
|
176
|
+
lines.append(f" // Step {step_num}: {method} {endpoint} (Expected HTTP {status})")
|
|
177
|
+
|
|
178
|
+
if method in ("POST", "PUT", "PATCH"):
|
|
179
|
+
body_str = req["request_body"]
|
|
180
|
+
# Try parsing JSON body for cleaner indentation
|
|
181
|
+
try:
|
|
182
|
+
parsed_json = json.loads(body_str)
|
|
183
|
+
formatted_body = json.dumps(parsed_json, indent=6)
|
|
184
|
+
except Exception:
|
|
185
|
+
formatted_body = json.dumps(body_str)
|
|
186
|
+
|
|
187
|
+
lines.append(f" const resp{step_num} = await request.{method.lower()}('{url}', {{")
|
|
188
|
+
lines.append(f" headers: {{ 'Content-Type': 'application/json' }},")
|
|
189
|
+
lines.append(f" data: {formatted_body}")
|
|
190
|
+
lines.append(f" }});")
|
|
191
|
+
lines.append(f" expect(resp{step_num}.status()).toBe({status});")
|
|
192
|
+
else:
|
|
193
|
+
lines.append(f" const resp{step_num} = await request.get('{url}');")
|
|
194
|
+
lines.append(f" expect(resp{step_num}.status()).toBe({status});")
|
|
195
|
+
|
|
196
|
+
lines.append("")
|
|
197
|
+
step_num += 1
|
|
198
|
+
|
|
199
|
+
lines.append(" });")
|
|
200
|
+
lines.append("});")
|
|
201
|
+
lines.append("")
|
|
202
|
+
|
|
203
|
+
return "\n".join(lines)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
parser = argparse.ArgumentParser(description="Shift-Right Traffic & User Session Replay Engine")
|
|
208
|
+
parser.add_argument("input_file", help="Path to input .har or network session JSON file")
|
|
209
|
+
parser.add_argument("--output", "-o", default="", help="Path to save generated test file")
|
|
210
|
+
parser.add_argument("--name", default="ProductionTrafficReplay", help="Test suite name")
|
|
211
|
+
parser.add_argument("--no-sanitize", action="store_true", help="Disable PII sanitization")
|
|
212
|
+
parser.add_argument("--json", action="store_true", help="Output parsed requests in JSON format")
|
|
213
|
+
args = parser.parse_args()
|
|
214
|
+
|
|
215
|
+
if not os.path.exists(args.input_file):
|
|
216
|
+
print(f"[ERROR] Input file does not exist: {args.input_file}")
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
with open(args.input_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
220
|
+
try:
|
|
221
|
+
data = json.load(f)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"[ERROR] Failed to parse input JSON: {e}")
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
converter = TrafficConverter(sanitize_pii=not args.no_sanitize)
|
|
227
|
+
actionable_requests = converter.parse_har(data)
|
|
228
|
+
|
|
229
|
+
if args.json:
|
|
230
|
+
print(json.dumps({
|
|
231
|
+
"total_requests": len(actionable_requests),
|
|
232
|
+
"requests": actionable_requests
|
|
233
|
+
}, indent=2))
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
test_code = converter.generate_playwright_test(actionable_requests, test_name=args.name)
|
|
237
|
+
|
|
238
|
+
if args.output:
|
|
239
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
|
240
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
241
|
+
f.write(test_code)
|
|
242
|
+
print(f"\n✅ [SHIFT-RIGHT CONVERTED] Generated Playwright test saved to: {args.output}")
|
|
243
|
+
print(f" Actionable Endpoints : {len(actionable_requests)}")
|
|
244
|
+
print(f" PII Sanitization : {'ENABLED (Tokens & Passwords Redacted)' if not args.no_sanitize else 'DISABLED'}\n")
|
|
245
|
+
else:
|
|
246
|
+
print("\n" + "=" * 70)
|
|
247
|
+
print(" GENERATED PLAYWRIGHT TEST SUITE (SHIFT-RIGHT REPLAY)")
|
|
248
|
+
print("=" * 70)
|
|
249
|
+
print(test_code)
|
|
250
|
+
print("=" * 70 + "\n")
|
|
251
|
+
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
sys.exit(main())
|