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/infer_linter.py
CHANGED
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
import re
|
|
17
17
|
import json
|
|
18
18
|
import glob
|
|
19
|
+
import ast
|
|
19
20
|
import html as html_lib
|
|
20
21
|
import argparse
|
|
21
22
|
from datetime import datetime
|
|
@@ -44,7 +45,7 @@ RESET = "\033[0m"
|
|
|
44
45
|
|
|
45
46
|
# Optional Tree-Sitter Integration for compiler-grade AST parsing across TS/Java/C#
|
|
46
47
|
try:
|
|
47
|
-
import tree_sitter # type: ignore
|
|
48
|
+
import tree_sitter # type: ignore # noqa: F401
|
|
48
49
|
HAS_TREE_SITTER = True
|
|
49
50
|
except ImportError:
|
|
50
51
|
HAS_TREE_SITTER = False
|
|
@@ -76,15 +77,19 @@ def parse_with_tree_sitter(file_path, content):
|
|
|
76
77
|
return None
|
|
77
78
|
|
|
78
79
|
# Generic Detection regexes (including Cypress .should and .and assertions)
|
|
80
|
+
# Note: Standalone expect(...) without chained matcher is caught as DANGLING_ASSERTION_NO_MATCHER.
|
|
79
81
|
ASSERTION_PATTERN = re.compile(
|
|
80
|
-
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b
|
|
82
|
+
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b'
|
|
83
|
+
r'|\.should\s*\('
|
|
84
|
+
r'|\.and\s*\(',
|
|
85
|
+
re.MULTILINE
|
|
81
86
|
)
|
|
82
87
|
|
|
83
88
|
# Rule L1 & L5: Empty catch / swallowed exceptions across JS/TS/Groovy/Java and Python
|
|
84
|
-
# Also catches try/catch that only console.log or promise .catch(() => {})
|
|
89
|
+
# Also catches try/catch that only console.log/logger.error or promise .catch(() => {})
|
|
85
90
|
EMPTY_CATCH_PATTERN = re.compile(
|
|
86
|
-
r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}'
|
|
87
|
-
r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}\s*\)'
|
|
91
|
+
r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}'
|
|
92
|
+
r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}\s*\)'
|
|
88
93
|
r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
|
|
89
94
|
re.MULTILINE
|
|
90
95
|
)
|
|
@@ -96,16 +101,48 @@ BENIGN_TRY_CALL_PATTERN = re.compile(
|
|
|
96
101
|
r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
|
|
97
102
|
)
|
|
98
103
|
|
|
99
|
-
# Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r,
|
|
104
|
+
# Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, assert f() == f(), assert len(x) >= 0)
|
|
100
105
|
TAUTOLOGY_PATTERN = re.compile(
|
|
101
|
-
r'(?:expect
|
|
106
|
+
r'(?:expect(?:\.soft|\.poll)?\s*\(\s*(?:true|1|null|undefined)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy|toBeNull|toBeUndefined)\s*\(\s*(?:true|1|null|undefined)?\s*\)'
|
|
107
|
+
r'|expect(?:\.soft|\.poll)?\s*\(\s*([a-zA-Z0-9_]+)\s*\)\s*\.\s*(?:toBe|toEqual)\s*\(\s*\1\s*\)'
|
|
102
108
|
r'|\bassert\s+(?:True|true|1\s*==\s*1)\b'
|
|
103
|
-
r'|\bassert\s+([a-zA-Z0-9_]+)\s*==\s*\1\b'
|
|
104
|
-
r'
|
|
109
|
+
r'|\bassert\s+([a-zA-Z0-9_\.]+(?:\([^)]*\))?)\s*==\s*\1\b'
|
|
110
|
+
r'|\bassert\s+(?:len\s*\([^)]*\)|[a-zA-Z0-9_\.]+\.(?:length|size\(\)))\s*(?:>=\s*0|>\s*-(?:1|0))\b'
|
|
111
|
+
r'|assertEquals\s*\(\s*(?:true|1|null)\s*,\s*(?:true|1|null)\s*\)'
|
|
105
112
|
r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
|
|
106
113
|
re.IGNORECASE
|
|
107
114
|
)
|
|
108
115
|
|
|
116
|
+
|
|
117
|
+
def find_dangling_expects(clean_code, raw_lines):
|
|
118
|
+
"""
|
|
119
|
+
Detects dangling expect(...) calls lacking chained matchers (e.g. expect(page) without .toBeVisible()).
|
|
120
|
+
Correctly handles nested parentheses like expect(page.locator('#id')).
|
|
121
|
+
"""
|
|
122
|
+
dangling = []
|
|
123
|
+
pattern = re.compile(r'(?:await\s+)?\bexpect(?:\.soft|\.poll)?\s*\(')
|
|
124
|
+
for m in pattern.finditer(clean_code):
|
|
125
|
+
start_idx = m.end()
|
|
126
|
+
depth = 1
|
|
127
|
+
pos = start_idx
|
|
128
|
+
while pos < len(clean_code) and depth > 0:
|
|
129
|
+
ch = clean_code[pos]
|
|
130
|
+
if ch == '(':
|
|
131
|
+
depth += 1
|
|
132
|
+
elif ch == ')':
|
|
133
|
+
depth -= 1
|
|
134
|
+
elif ch in (';', '\n') and depth > 0:
|
|
135
|
+
# Malformed unclosed expect
|
|
136
|
+
break
|
|
137
|
+
pos += 1
|
|
138
|
+
if depth == 0:
|
|
139
|
+
rest = clean_code[pos:].lstrip()
|
|
140
|
+
if not rest.startswith('.'):
|
|
141
|
+
line_no = clean_code[:m.start()].count("\n") + 1
|
|
142
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
143
|
+
dangling.append((line_no, raw_line))
|
|
144
|
+
return dangling
|
|
145
|
+
|
|
109
146
|
# Rule L4: Unawaited Playwright assertions in JS/TS
|
|
110
147
|
# Expanded with all standard Playwright async matchers and optional .not modifier
|
|
111
148
|
PLAYWRIGHT_ASYNC_MATCHERS = (
|
|
@@ -135,7 +172,7 @@ THREAD_SLEEP_PATTERN = MS_DELAY_PATTERN
|
|
|
135
172
|
|
|
136
173
|
def strip_comments_and_strings(source_code, preserve_newlines=True):
|
|
137
174
|
"""
|
|
138
|
-
Lexically strips block comments, line comments, multiline strings,
|
|
175
|
+
Lexically strips block comments, line comments, multiline strings,
|
|
139
176
|
and quoted literals while preserving line numbering.
|
|
140
177
|
Prevents false-positive / false-negative matches in AST linter.
|
|
141
178
|
"""
|
|
@@ -173,7 +210,7 @@ def _is_benign_empty_catch(clean_code, match_start):
|
|
|
173
210
|
if not stripped:
|
|
174
211
|
continue
|
|
175
212
|
if re.match(r'try\s*(?:[:{]|$)', stripped):
|
|
176
|
-
return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(
|
|
213
|
+
return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(bl) for bl in body_lines)
|
|
177
214
|
body_lines.append(stripped)
|
|
178
215
|
return False
|
|
179
216
|
|
|
@@ -185,7 +222,7 @@ def load_config(repo_root=None, config_path=None):
|
|
|
185
222
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
186
223
|
return json.load(f)
|
|
187
224
|
except (OSError, json.JSONDecodeError):
|
|
188
|
-
|
|
225
|
+
_ = False
|
|
189
226
|
|
|
190
227
|
root = repo_root or REPO_ROOT
|
|
191
228
|
candidates = [
|
|
@@ -300,7 +337,7 @@ def format_junit(report_data):
|
|
|
300
337
|
return "\n".join(lines)
|
|
301
338
|
|
|
302
339
|
|
|
303
|
-
def classify_file_role(file_path: str) -> str:
|
|
340
|
+
def classify_file_role(file_path: str, content: str = "") -> str:
|
|
304
341
|
"""
|
|
305
342
|
Classifies a source file into:
|
|
306
343
|
- 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
|
|
@@ -324,6 +361,8 @@ def classify_file_role(file_path: str) -> str:
|
|
|
324
361
|
return "PAGE_OBJECT"
|
|
325
362
|
if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
|
|
326
363
|
return "PAGE_OBJECT"
|
|
364
|
+
if content and (re.search(r'\bclass\s+[A-Za-z0-9_]*Page\b', content) or re.search(r'\bextends\s+BasePage\b', content)):
|
|
365
|
+
return "PAGE_OBJECT"
|
|
327
366
|
|
|
328
367
|
# 3. Helpers & Fixtures / Tools & Utilities / Benchmarks
|
|
329
368
|
if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin", "benchmarks", "benchmark") for p in parts):
|
|
@@ -348,13 +387,16 @@ def classify_file_role(file_path: str) -> str:
|
|
|
348
387
|
|
|
349
388
|
def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
350
389
|
"""Performs static rule scanning on a single test script or automation asset."""
|
|
351
|
-
role = classify_file_role(file_path)
|
|
352
|
-
|
|
353
390
|
try:
|
|
354
391
|
rel_path = os.path.relpath(file_path, REPO_ROOT)
|
|
355
392
|
except ValueError:
|
|
356
393
|
rel_path = os.path.abspath(file_path)
|
|
357
394
|
|
|
395
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
396
|
+
content = f.read()
|
|
397
|
+
|
|
398
|
+
role = classify_file_role(file_path, content=content)
|
|
399
|
+
|
|
358
400
|
# Config files have zero test assertions by definition
|
|
359
401
|
if role == "CONFIG":
|
|
360
402
|
return {
|
|
@@ -364,14 +406,11 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
364
406
|
"is_clean": True
|
|
365
407
|
}
|
|
366
408
|
|
|
367
|
-
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
368
|
-
content = f.read()
|
|
369
|
-
|
|
370
409
|
defects = []
|
|
371
410
|
|
|
372
411
|
# Lexically strip comments and string literals for accurate analysis
|
|
373
412
|
clean_code = strip_comments_and_strings(content, preserve_newlines=True)
|
|
374
|
-
clean_lines = [
|
|
413
|
+
clean_lines = [line.strip() for line in clean_code.splitlines()]
|
|
375
414
|
|
|
376
415
|
raw_lines = content.splitlines()
|
|
377
416
|
|
|
@@ -402,12 +441,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
402
441
|
elif arg.isdigit():
|
|
403
442
|
delay_val = int(arg)
|
|
404
443
|
if delay_val > 0:
|
|
405
|
-
|
|
444
|
+
is_excessive = delay_val >= 3000
|
|
445
|
+
d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
|
|
446
|
+
sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
|
|
406
447
|
defects.append({
|
|
407
448
|
"type": d_type,
|
|
408
|
-
"severity":
|
|
449
|
+
"severity": sev,
|
|
409
450
|
"line": idx,
|
|
410
|
-
"message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
451
|
+
"message": f"Static sleep of {delay_val}ms found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
411
452
|
})
|
|
412
453
|
continue
|
|
413
454
|
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
|
|
@@ -428,12 +469,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
428
469
|
if delay_val > 0:
|
|
429
470
|
delay_ms = int(delay_val * 1000)
|
|
430
471
|
delay_str = f"{delay_val:g}"
|
|
431
|
-
|
|
472
|
+
is_excessive = delay_ms >= 3000
|
|
473
|
+
d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
|
|
474
|
+
sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
|
|
432
475
|
defects.append({
|
|
433
476
|
"type": d_type,
|
|
434
|
-
"severity":
|
|
477
|
+
"severity": sev,
|
|
435
478
|
"line": idx,
|
|
436
|
-
"message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
479
|
+
"message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
437
480
|
})
|
|
438
481
|
except ValueError:
|
|
439
482
|
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
|
|
@@ -447,8 +490,8 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
447
490
|
# Test Script Specific Invariants (Do NOT apply to Page Objects or Helpers)
|
|
448
491
|
if role == "TEST_SCRIPT":
|
|
449
492
|
executable_lines = [
|
|
450
|
-
|
|
451
|
-
if
|
|
493
|
+
line for line in clean_lines
|
|
494
|
+
if line and not line.startswith("import ") and not line.startswith("package ") and not line.startswith("from ")
|
|
452
495
|
]
|
|
453
496
|
# Rule 1: Empty Test Script
|
|
454
497
|
if len(executable_lines) == 0:
|
|
@@ -470,6 +513,68 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
470
513
|
"message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
|
|
471
514
|
})
|
|
472
515
|
|
|
516
|
+
# Rule 4.1: Dangling expect(...) calls without chained matcher
|
|
517
|
+
for d_line, d_raw in find_dangling_expects(clean_code, raw_lines):
|
|
518
|
+
defects.append({
|
|
519
|
+
"type": "DANGLING_ASSERTION_NO_MATCHER",
|
|
520
|
+
"severity": "CRITICAL",
|
|
521
|
+
"line": d_line,
|
|
522
|
+
"message": f"Dangling expect() call without matcher detected ('{d_raw}'). expect() returns an expectation object and does not perform any assertion without a matcher (e.g. .toBeVisible(), .toHaveText())."
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
# Rule 4.2: Python AST Compiler-Grade Verification for .py Test Scripts
|
|
526
|
+
if file_path.endswith(".py"):
|
|
527
|
+
try:
|
|
528
|
+
tree = ast.parse(content, filename=file_path)
|
|
529
|
+
for node in ast.walk(tree):
|
|
530
|
+
if isinstance(node, ast.Assert):
|
|
531
|
+
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1:
|
|
532
|
+
op = node.test.ops[0]
|
|
533
|
+
right = node.test.comparators[0]
|
|
534
|
+
if isinstance(op, ast.Eq):
|
|
535
|
+
if ast.dump(node.test.left) == ast.dump(right):
|
|
536
|
+
line_no = getattr(node, "lineno", 1)
|
|
537
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
538
|
+
defects.append({
|
|
539
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
540
|
+
"severity": "CRITICAL",
|
|
541
|
+
"line": line_no,
|
|
542
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). Both sides of comparison evaluate to identical AST expressions."
|
|
543
|
+
})
|
|
544
|
+
elif isinstance(op, (ast.GtE, ast.Gt)):
|
|
545
|
+
if isinstance(node.test.left, ast.Call) and getattr(node.test.left.func, 'id', None) == 'len':
|
|
546
|
+
if isinstance(right, ast.Constant):
|
|
547
|
+
if (isinstance(op, ast.GtE) and right.value == 0) or (isinstance(op, ast.Gt) and right.value < 0):
|
|
548
|
+
line_no = getattr(node, "lineno", 1)
|
|
549
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
550
|
+
defects.append({
|
|
551
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
552
|
+
"severity": "CRITICAL",
|
|
553
|
+
"line": line_no,
|
|
554
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). len() is non-negative by definition."
|
|
555
|
+
})
|
|
556
|
+
elif isinstance(node.test, ast.Constant) and node.test.value is True:
|
|
557
|
+
line_no = getattr(node, "lineno", 1)
|
|
558
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
559
|
+
defects.append({
|
|
560
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
561
|
+
"severity": "CRITICAL",
|
|
562
|
+
"line": line_no,
|
|
563
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). Constant True assertion."
|
|
564
|
+
})
|
|
565
|
+
elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
566
|
+
if getattr(node.value.func, 'id', None) == 'expect':
|
|
567
|
+
line_no = getattr(node, "lineno", 1)
|
|
568
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
569
|
+
defects.append({
|
|
570
|
+
"type": "DANGLING_ASSERTION_NO_MATCHER",
|
|
571
|
+
"severity": "CRITICAL",
|
|
572
|
+
"line": line_no,
|
|
573
|
+
"message": f"Dangling expect() call without matcher detected via AST ('{raw_line}')."
|
|
574
|
+
})
|
|
575
|
+
except SyntaxError:
|
|
576
|
+
_ = False # synapse-ignore
|
|
577
|
+
|
|
473
578
|
# Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
|
|
474
579
|
if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
|
|
475
580
|
for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
|
|
@@ -544,6 +649,15 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
544
649
|
"message": "Test case file contains executable logic but has no verification or assertion statements."
|
|
545
650
|
})
|
|
546
651
|
|
|
652
|
+
seen_keys = set()
|
|
653
|
+
deduped = []
|
|
654
|
+
for d in defects:
|
|
655
|
+
k = (d.get("type"), d.get("line"))
|
|
656
|
+
if k not in seen_keys:
|
|
657
|
+
seen_keys.add(k)
|
|
658
|
+
deduped.append(d)
|
|
659
|
+
defects = deduped
|
|
660
|
+
|
|
547
661
|
active_defects = []
|
|
548
662
|
disabled = {r.upper() for r in (disabled_rules or [])}
|
|
549
663
|
norm_rel = rel_path.replace("\\", "/")
|
|
@@ -740,7 +854,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
|
|
|
740
854
|
print(f" Target Scope : 0 Test Scripts discovered in {os.path.basename(scan_dir)}/ (No tests found)")
|
|
741
855
|
print(f" Critical Defects : {GREEN}0{RESET}")
|
|
742
856
|
print(f" Warnings / Suggestions : {GREEN}0{RESET}")
|
|
743
|
-
print(
|
|
857
|
+
print(" Clean Scripts : 0/0 (N/A)")
|
|
744
858
|
print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
|
|
745
859
|
else:
|
|
746
860
|
print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
|
|
@@ -777,7 +891,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
|
|
|
777
891
|
sys.stderr.write(f"Notice: Target report write failed: {err}\n")
|
|
778
892
|
|
|
779
893
|
if strict:
|
|
780
|
-
fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST"))
|
|
894
|
+
fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST", "DANGLING_ASSERTION_NO_MATCHER"))
|
|
781
895
|
return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
|
|
782
896
|
return 0 if critical_count == 0 else 1
|
|
783
897
|
|
package/tools/interview_story.py
CHANGED
package/tools/jtl_analyzer.py
CHANGED
|
@@ -163,16 +163,16 @@ def parse_csv_jtl(filepath: str) -> Tuple[Dict[str, SamplerStats], SamplerStats]
|
|
|
163
163
|
# Column names can vary slightly across JMeter versions
|
|
164
164
|
elapsed = float(row.get("elapsed") or row.get("Elapsed") or row.get("time") or 0)
|
|
165
165
|
label = row.get("label") or row.get("Label") or "Unknown"
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
success_val = row.get("success") or row.get("Success") or "true"
|
|
168
168
|
success = str(success_val).strip().lower() in ("true", "1")
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
ts_val = row.get("timeStamp") or row.get("Timestamp") or row.get("t") or 0
|
|
171
171
|
timestamp = int(float(ts_val))
|
|
172
|
-
|
|
172
|
+
|
|
173
173
|
bytes_val = row.get("bytes") or row.get("Bytes") or 0
|
|
174
174
|
bytes_recv = int(float(bytes_val))
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
resp_code = row.get("responseCode") or row.get("ResponseCode") or row.get("rc") or "200"
|
|
177
177
|
|
|
178
178
|
if label not in per_sampler:
|
|
@@ -236,7 +236,7 @@ def evaluate_sla(
|
|
|
236
236
|
) -> Tuple[bool, List[str]]:
|
|
237
237
|
"""Evaluates computed aggregate metrics against SLA limits."""
|
|
238
238
|
violations: List[str] = []
|
|
239
|
-
|
|
239
|
+
|
|
240
240
|
if sla_p95 is not None and agg_stats["p95_ms"] > sla_p95:
|
|
241
241
|
violations.append(f"P95 Latency SLA Breached: {agg_stats['p95_ms']}ms > {sla_p95}ms")
|
|
242
242
|
|
|
@@ -261,7 +261,7 @@ def format_table(per_sampler_data: List[Dict[str, Any]], agg_data: Dict[str, Any
|
|
|
261
261
|
lines = []
|
|
262
262
|
header_str = " | ".join(h.ljust(col_w[i]) for i, h in enumerate(headers))
|
|
263
263
|
sep_str = "-+-".join("-" * col_w[i] for i in range(len(headers)))
|
|
264
|
-
|
|
264
|
+
|
|
265
265
|
lines.append(header_str)
|
|
266
266
|
lines.append(sep_str)
|
|
267
267
|
|
|
@@ -271,7 +271,7 @@ def format_table(per_sampler_data: List[Dict[str, Any]], agg_data: Dict[str, Any
|
|
|
271
271
|
if len(label) > 28:
|
|
272
272
|
label = label[:25] + "..."
|
|
273
273
|
err_str = f"{row['errors']} ({row['error_rate_pct']}%)"
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
row_str = " | ".join([
|
|
276
276
|
label.ljust(col_w[0]),
|
|
277
277
|
str(row["samples"]).rjust(col_w[1]),
|
|
@@ -301,7 +301,7 @@ def generate_markdown_report(
|
|
|
301
301
|
) -> str:
|
|
302
302
|
"""Generates GitHub / GitLab Markdown report for Pull Request annotations."""
|
|
303
303
|
status_badge = "✅ **PASSED**" if passed else "❌ **FAILED (SLA BREACHED)**"
|
|
304
|
-
|
|
304
|
+
|
|
305
305
|
lines = [
|
|
306
306
|
"# 🚀 Apache JMeter Performance & Load Test Report",
|
|
307
307
|
"",
|
|
@@ -377,7 +377,7 @@ def main():
|
|
|
377
377
|
with open(temp_jtl, "w", encoding="utf-8", newline="") as f:
|
|
378
378
|
writer = csv.writer(f)
|
|
379
379
|
writer.writerow(["timeStamp", "elapsed", "label", "responseCode", "responseMessage", "threadName", "dataType", "success", "failureMessage", "bytes", "sentBytes", "grpThreads", "allThreads", "URL", "Latency", "IdleTime", "Connect"])
|
|
380
|
-
|
|
380
|
+
|
|
381
381
|
# Generate 200 synthetic rows
|
|
382
382
|
import time
|
|
383
383
|
base_time = int(time.time() * 1000)
|
|
@@ -387,7 +387,7 @@ def main():
|
|
|
387
387
|
writer.writerow([base_time + (i * 100), 280 + (i % 80), "[POST] /api/v1/orders", "201", "Created", "Thread 1-2", "text", "true", "", 2048, 512, 10, 10, "http://api.local/orders", 270, 0, 20])
|
|
388
388
|
for i in range(5):
|
|
389
389
|
writer.writerow([base_time + (i * 100), 1500, "[POST] /api/v1/orders", "500", "Internal Error", "Thread 1-2", "text", "false", "DB Timeout", 512, 512, 10, 10, "http://api.local/orders", 1490, 0, 25])
|
|
390
|
-
|
|
390
|
+
|
|
391
391
|
args.jtl_file = temp_jtl
|
|
392
392
|
print(f"🔬 [SAMPLE MODE] Generated synthetic benchmark JTL: {temp_jtl}")
|
|
393
393
|
|
|
@@ -414,7 +414,7 @@ def main():
|
|
|
414
414
|
print("=" * 90)
|
|
415
415
|
|
|
416
416
|
# Print SLA Verdict
|
|
417
|
-
print(
|
|
417
|
+
print("\n🎯 [QUALITY GATE SLA VERDICT]")
|
|
418
418
|
if passed:
|
|
419
419
|
print("✅ ALL PERFORMANCE SLAs PASSED! Ready for deployment/merge.")
|
|
420
420
|
else:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synapse Hub — Static Quality & Policy Sentinels (tools.lint)
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
Domain package grouping all static code defect, encoding, license,
|
|
5
|
+
and AI documentation drift verification tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tools import (
|
|
9
|
+
author_integrity,
|
|
10
|
+
check_version_sync,
|
|
11
|
+
git_pre_commit_guard,
|
|
12
|
+
infer_linter,
|
|
13
|
+
lint_ai_docs,
|
|
14
|
+
skill_collision_guard,
|
|
15
|
+
skillspector,
|
|
16
|
+
spec_drift_sentinel,
|
|
17
|
+
validate_groovy_scripts,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"author_integrity",
|
|
22
|
+
"check_version_sync",
|
|
23
|
+
"git_pre_commit_guard",
|
|
24
|
+
"infer_linter",
|
|
25
|
+
"lint_ai_docs",
|
|
26
|
+
"skill_collision_guard",
|
|
27
|
+
"skillspector",
|
|
28
|
+
"spec_drift_sentinel",
|
|
29
|
+
"validate_groovy_scripts",
|
|
30
|
+
]
|
package/tools/lint_ai_docs.py
CHANGED
|
@@ -281,8 +281,50 @@ def lint_docs(repo_root):
|
|
|
281
281
|
if mname not in valid_methods:
|
|
282
282
|
errors.append(f"[{doc}:{line_no}] Non-existent method referenced: '{cname}.{mname}()' (Class '{cname}' does not define this method)")
|
|
283
283
|
|
|
284
|
+
# 6. Documentation Metrics Consistency (Detects stale skill/tool/gate numbers)
|
|
285
|
+
metric_errors = check_doc_metrics_consistency(repo_root)
|
|
286
|
+
errors.extend(metric_errors)
|
|
287
|
+
|
|
284
288
|
return errors, warnings
|
|
285
289
|
|
|
290
|
+
|
|
291
|
+
def check_doc_metrics_consistency(repo_root):
|
|
292
|
+
"""Verifies that documented counts (skills, tools, DoD gates) accurately match disk."""
|
|
293
|
+
errors = []
|
|
294
|
+
skills_dir = os.path.join(repo_root, ".agents", "skills")
|
|
295
|
+
tools_dir = os.path.join(repo_root, "tools")
|
|
296
|
+
|
|
297
|
+
if not os.path.isdir(skills_dir) or not os.path.isdir(tools_dir):
|
|
298
|
+
return errors
|
|
299
|
+
|
|
300
|
+
skills_count = len([d for d in os.listdir(skills_dir) if os.path.isdir(os.path.join(skills_dir, d)) and os.path.exists(os.path.join(skills_dir, d, "SKILL.md"))])
|
|
301
|
+
tools_count = len([f for f in os.listdir(tools_dir) if f.endswith(".py") and f != "__init__.py"])
|
|
302
|
+
|
|
303
|
+
obsolete_patterns = [
|
|
304
|
+
(re.compile(r'\b(42|50)\s+(kỹ năng|skills?)\b', re.IGNORECASE), f"Obsolete skill count detected (real count: {skills_count})"),
|
|
305
|
+
(re.compile(r'\b(60|68)\s+(công cụ|tools?)\b', re.IGNORECASE), f"Obsolete tool count detected (real count: {tools_count})"),
|
|
306
|
+
(re.compile(r'\b9\s+Cổng\s+Chất\s+Lượng\b', re.IGNORECASE), "Obsolete gate count detected: must refer to 10 Cổng Chất Lượng DoD (Gate 0–9)"),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
docs_to_check = [
|
|
310
|
+
os.path.join(repo_root, "README.md"),
|
|
311
|
+
os.path.join(repo_root, "README.en.md"),
|
|
312
|
+
os.path.join(repo_root, "docs", "ARCHITECTURE.md"),
|
|
313
|
+
os.path.join(repo_root, "docs", "CONTRIBUTING.md"),
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
for doc_path in docs_to_check:
|
|
317
|
+
if not os.path.exists(doc_path):
|
|
318
|
+
continue
|
|
319
|
+
rel_doc = os.path.relpath(doc_path, repo_root)
|
|
320
|
+
with open(doc_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
321
|
+
for line_no, line in enumerate(f, 1):
|
|
322
|
+
for pattern, msg in obsolete_patterns:
|
|
323
|
+
if pattern.search(line):
|
|
324
|
+
errors.append(f"[{rel_doc}:{line_no}] {msg}: '{line.strip()}'")
|
|
325
|
+
|
|
326
|
+
return errors
|
|
327
|
+
|
|
286
328
|
def main():
|
|
287
329
|
import argparse
|
|
288
330
|
parser = argparse.ArgumentParser(description="Enterprise AI Documentation & Artifact Linter")
|
package/tools/lookup_tc.py
CHANGED
package/tools/matrix_sharder.py
CHANGED
|
@@ -52,13 +52,15 @@ def estimate_test_duration(file_path: str, full_path: str) -> float:
|
|
|
52
52
|
if os.path.exists(CACHE_DB_PATH):
|
|
53
53
|
try:
|
|
54
54
|
conn = sqlite3.connect(CACHE_DB_PATH)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
try:
|
|
56
|
+
cur = conn.cursor()
|
|
57
|
+
cur.execute("CREATE TABLE IF NOT EXISTS test_runtimes (test_path TEXT PRIMARY KEY, duration REAL)")
|
|
58
|
+
cur.execute("SELECT duration FROM test_runtimes WHERE test_path = ?", (file_path,))
|
|
59
|
+
row = cur.fetchone()
|
|
60
|
+
if row and row[0]:
|
|
61
|
+
return float(row[0])
|
|
62
|
+
finally:
|
|
63
|
+
conn.close()
|
|
62
64
|
except sqlite3.Error:
|
|
63
65
|
conn = None
|
|
64
66
|
|
|
@@ -93,14 +95,14 @@ def shard_tests_lpt(test_files: List[str], target_dir: str, num_shards: int) ->
|
|
|
93
95
|
tests_with_cost.sort(key=lambda x: x[1], reverse=True)
|
|
94
96
|
|
|
95
97
|
# 3. Initialize shards
|
|
96
|
-
shards = [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
|
|
98
|
+
shards: List[Dict[str, Any]] = [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
|
|
97
99
|
|
|
98
100
|
# 4. Greedily assign to the shard with the least current cumulative duration
|
|
99
101
|
for test_file, dur in tests_with_cost:
|
|
100
102
|
# Find shard with min duration
|
|
101
|
-
min_shard = min(shards, key=lambda s: s["total_duration"])
|
|
103
|
+
min_shard = min(shards, key=lambda s: float(s["total_duration"]))
|
|
102
104
|
min_shard["tests"].append(test_file)
|
|
103
|
-
min_shard["total_duration"] = round(min_shard["total_duration"] + dur, 2)
|
|
105
|
+
min_shard["total_duration"] = round(float(min_shard["total_duration"]) + dur, 2)
|
|
104
106
|
|
|
105
107
|
return shards
|
|
106
108
|
|
|
@@ -119,9 +121,9 @@ def main():
|
|
|
119
121
|
if args.json:
|
|
120
122
|
print(json.dumps({"total_tests": len(test_files), "shards_count": args.shards, "shards": shards}, indent=2))
|
|
121
123
|
else:
|
|
122
|
-
print(
|
|
124
|
+
print("\n======================================================================")
|
|
123
125
|
print(f" DURATION-BALANCED TEST MATRIX SHARDER ({args.shards} PARALLEL WORKERS) ")
|
|
124
|
-
print(
|
|
126
|
+
print("======================================================================")
|
|
125
127
|
print(f" Target Scope : {target_dir}")
|
|
126
128
|
print(f" Total Tests : {len(test_files)} files\n")
|
|
127
129
|
|
|
@@ -137,7 +139,7 @@ def main():
|
|
|
137
139
|
durations = [s["total_duration"] for s in shards]
|
|
138
140
|
delta = max(durations) - min(durations) if durations else 0
|
|
139
141
|
print(f"⚡ Shard Imbalance Delta: {delta:.1f}s (Optimal parallel distribution)")
|
|
140
|
-
print(
|
|
142
|
+
print("======================================================================\n")
|
|
141
143
|
|
|
142
144
|
return 0
|
|
143
145
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synapse Hub — Model Context Protocol (MCP) Servers (tools.mcp)
|
|
3
|
+
--------------------------------------------------------------
|
|
4
|
+
Domain package grouping all standard MCP services: core QA automation
|
|
5
|
+
MCP server and offline Excel spreadsheet manipulation server.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tools import (
|
|
9
|
+
mcp_excel_server,
|
|
10
|
+
mcp_server,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"mcp_excel_server",
|
|
15
|
+
"mcp_server",
|
|
16
|
+
]
|
|
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
23
23
|
except Exception:
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
|
-
SERVER_VERSION = "
|
|
26
|
+
SERVER_VERSION = "3.0.1"
|
|
27
27
|
|
|
28
28
|
NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
|
|
29
29
|
NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
|
|
@@ -88,7 +88,7 @@ def read_sheet_rows(xlsx_path: str, sheet_name: Optional[str] = None, max_rows:
|
|
|
88
88
|
for c in row.iter(f"{NS_MAIN}c"):
|
|
89
89
|
cell_type = c.attrib.get("t")
|
|
90
90
|
val_elem = c.find(f"{NS_MAIN}v")
|
|
91
|
-
raw_val = val_elem.text if val_elem is not None else ""
|
|
91
|
+
raw_val = (val_elem.text or "") if val_elem is not None else ""
|
|
92
92
|
|
|
93
93
|
if cell_type == "s" and raw_val.isdigit():
|
|
94
94
|
idx = int(raw_val)
|