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,480 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Universal Testing Intermediate Representation (IR) Schema
|
|
4
|
+
---------------------------------------------------------
|
|
5
|
+
Defines a language-agnostic and framework-agnostic schema for test automation.
|
|
6
|
+
Represents pages, locators, methods, test cases, and assertions independently of
|
|
7
|
+
whether the target is Playwright, Selenium, Cypress, Katalon, or Appium.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
12
|
+
from dataclasses import dataclass, field, asdict
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ActionType(str, Enum):
|
|
17
|
+
GOTO = "goto"
|
|
18
|
+
CLICK = "click"
|
|
19
|
+
DBLCLICK = "dblclick"
|
|
20
|
+
FILL = "fill"
|
|
21
|
+
PRESS_KEY = "press_key"
|
|
22
|
+
HOVER = "hover"
|
|
23
|
+
SELECT = "select"
|
|
24
|
+
CHECK = "check"
|
|
25
|
+
UNCHECK = "uncheck"
|
|
26
|
+
WAIT = "wait"
|
|
27
|
+
ASSERT = "assert"
|
|
28
|
+
CUSTOM_CALL = "custom_call"
|
|
29
|
+
API_CALL = "api_call"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LocatorStrategy(str, Enum):
|
|
33
|
+
ROLE = "role"
|
|
34
|
+
LABEL = "label"
|
|
35
|
+
TEST_ID = "test_id"
|
|
36
|
+
TEXT = "text"
|
|
37
|
+
CSS = "css"
|
|
38
|
+
XPATH = "xpath"
|
|
39
|
+
PLACEHOLDER = "placeholder"
|
|
40
|
+
ID = "id"
|
|
41
|
+
NAME = "name"
|
|
42
|
+
CLASS_NAME = "class_name"
|
|
43
|
+
UNKNOWN = "unknown"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AssertionType(str, Enum):
|
|
47
|
+
VISIBLE = "visible"
|
|
48
|
+
HIDDEN = "hidden"
|
|
49
|
+
EQUALS = "equals"
|
|
50
|
+
CONTAINS_TEXT = "contains_text"
|
|
51
|
+
HAS_VALUE = "has_value"
|
|
52
|
+
IS_ENABLED = "is_enabled"
|
|
53
|
+
IS_DISABLED = "is_disabled"
|
|
54
|
+
URL_CONTAINS = "url_contains"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WaitStrategy(str, Enum):
|
|
58
|
+
AUTO = "auto"
|
|
59
|
+
EXPLICIT_VISIBLE = "explicit_visible"
|
|
60
|
+
NETWORK_IDLE = "network_idle"
|
|
61
|
+
STATIC_SLEEP = "static_sleep" # Flagged antipattern
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DataDependencyType(str, Enum):
|
|
65
|
+
ID = "id"
|
|
66
|
+
AUTH_TOKEN = "auth_token"
|
|
67
|
+
ORDER_NUMBER = "order_number"
|
|
68
|
+
EMAIL = "email"
|
|
69
|
+
SESSION = "session"
|
|
70
|
+
CUSTOM = "custom"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class HermeticFixture:
|
|
75
|
+
variable_name: str
|
|
76
|
+
fixture_type: str = "synthetic_mock" # "api_seed", "synthetic_mock", "auth_session"
|
|
77
|
+
seed_endpoint: Optional[str] = None
|
|
78
|
+
mock_data: Dict[str, Any] = field(default_factory=dict)
|
|
79
|
+
code_snippet: str = ""
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
"variable_name": self.variable_name,
|
|
84
|
+
"fixture_type": self.fixture_type,
|
|
85
|
+
"seed_endpoint": self.seed_endpoint,
|
|
86
|
+
"mock_data": self.mock_data,
|
|
87
|
+
"code_snippet": self.code_snippet
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, data: Dict[str, Any]) -> "HermeticFixture":
|
|
92
|
+
return cls(
|
|
93
|
+
variable_name=data.get("variable_name", ""),
|
|
94
|
+
fixture_type=data.get("fixture_type", "synthetic_mock"),
|
|
95
|
+
seed_endpoint=data.get("seed_endpoint"),
|
|
96
|
+
mock_data=data.get("mock_data", {}),
|
|
97
|
+
code_snippet=data.get("code_snippet", "")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SharedStateVariable:
|
|
103
|
+
name: str
|
|
104
|
+
producer_test_id: Optional[str] = None
|
|
105
|
+
consumer_test_ids: List[str] = field(default_factory=list)
|
|
106
|
+
dep_type: DataDependencyType = DataDependencyType.CUSTOM
|
|
107
|
+
synthetic_fixture_code: str = ""
|
|
108
|
+
mock_route: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"name": self.name,
|
|
113
|
+
"producer_test_id": self.producer_test_id,
|
|
114
|
+
"consumer_test_ids": self.consumer_test_ids,
|
|
115
|
+
"dep_type": self.dep_type.value if isinstance(self.dep_type, DataDependencyType) else self.dep_type,
|
|
116
|
+
"synthetic_fixture_code": self.synthetic_fixture_code,
|
|
117
|
+
"mock_route": self.mock_route
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class LocatorIR:
|
|
123
|
+
name: str
|
|
124
|
+
strategy: LocatorStrategy
|
|
125
|
+
value: str
|
|
126
|
+
description: str = ""
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
129
|
+
return {
|
|
130
|
+
"name": self.name,
|
|
131
|
+
"strategy": self.strategy.value if isinstance(self.strategy, LocatorStrategy) else self.strategy,
|
|
132
|
+
"value": self.value,
|
|
133
|
+
"description": self.description,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LocatorIR":
|
|
138
|
+
strat = data.get("strategy", "unknown")
|
|
139
|
+
try:
|
|
140
|
+
strategy_enum = LocatorStrategy(strat)
|
|
141
|
+
except ValueError:
|
|
142
|
+
strategy_enum = LocatorStrategy.UNKNOWN
|
|
143
|
+
return cls(
|
|
144
|
+
name=data.get("name", "loc_unnamed"),
|
|
145
|
+
strategy=strategy_enum,
|
|
146
|
+
value=data.get("value", ""),
|
|
147
|
+
description=data.get("description", ""),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class StepIR:
|
|
153
|
+
action: ActionType
|
|
154
|
+
target: Optional[str] = None # Locator name or raw selector
|
|
155
|
+
value: Optional[str] = None # Input text, key code, URL, etc.
|
|
156
|
+
assertion_type: Optional[AssertionType] = None
|
|
157
|
+
expected: Optional[str] = None
|
|
158
|
+
wait_strategy: Optional[WaitStrategy] = None
|
|
159
|
+
is_sanitized_wait: bool = False
|
|
160
|
+
timeout_ms: int = 5000
|
|
161
|
+
description: str = ""
|
|
162
|
+
|
|
163
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
164
|
+
return {
|
|
165
|
+
"action": self.action.value if isinstance(self.action, ActionType) else self.action,
|
|
166
|
+
"target": self.target,
|
|
167
|
+
"value": self.value,
|
|
168
|
+
"assertion_type": self.assertion_type.value if isinstance(self.assertion_type, AssertionType) and self.assertion_type else (self.assertion_type or None),
|
|
169
|
+
"expected": self.expected,
|
|
170
|
+
"wait_strategy": self.wait_strategy.value if isinstance(self.wait_strategy, WaitStrategy) and self.wait_strategy else (self.wait_strategy or None),
|
|
171
|
+
"is_sanitized_wait": self.is_sanitized_wait,
|
|
172
|
+
"timeout_ms": self.timeout_ms,
|
|
173
|
+
"description": self.description,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StepIR":
|
|
178
|
+
act = data.get("action", "custom_call")
|
|
179
|
+
try:
|
|
180
|
+
act_enum = ActionType(act)
|
|
181
|
+
except ValueError:
|
|
182
|
+
act_enum = ActionType.CUSTOM_CALL
|
|
183
|
+
|
|
184
|
+
assert_type = data.get("assertion_type")
|
|
185
|
+
assert_enum = None
|
|
186
|
+
if assert_type:
|
|
187
|
+
try:
|
|
188
|
+
assert_enum = AssertionType(assert_type)
|
|
189
|
+
except ValueError:
|
|
190
|
+
assert_enum = None
|
|
191
|
+
|
|
192
|
+
wait_strat = data.get("wait_strategy")
|
|
193
|
+
wait_enum = None
|
|
194
|
+
if wait_strat:
|
|
195
|
+
try:
|
|
196
|
+
wait_enum = WaitStrategy(wait_strat)
|
|
197
|
+
except ValueError:
|
|
198
|
+
wait_enum = None
|
|
199
|
+
|
|
200
|
+
return cls(
|
|
201
|
+
action=act_enum,
|
|
202
|
+
target=data.get("target"),
|
|
203
|
+
value=data.get("value"),
|
|
204
|
+
assertion_type=assert_enum,
|
|
205
|
+
expected=data.get("expected"),
|
|
206
|
+
wait_strategy=wait_enum,
|
|
207
|
+
is_sanitized_wait=data.get("is_sanitized_wait", False),
|
|
208
|
+
timeout_ms=data.get("timeout_ms", 5000),
|
|
209
|
+
description=data.get("description", ""),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class MethodIR:
|
|
215
|
+
name: str
|
|
216
|
+
parameters: List[str] = field(default_factory=list)
|
|
217
|
+
return_type: str = "void"
|
|
218
|
+
steps: List[StepIR] = field(default_factory=list)
|
|
219
|
+
description: str = ""
|
|
220
|
+
|
|
221
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
222
|
+
return {
|
|
223
|
+
"name": self.name,
|
|
224
|
+
"parameters": self.parameters,
|
|
225
|
+
"return_type": self.return_type,
|
|
226
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
227
|
+
"description": self.description,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MethodIR":
|
|
232
|
+
return cls(
|
|
233
|
+
name=data.get("name", "execute"),
|
|
234
|
+
parameters=data.get("parameters", []),
|
|
235
|
+
return_type=data.get("return_type", "void"),
|
|
236
|
+
steps=[StepIR.from_dict(s) for s in data.get("steps", [])],
|
|
237
|
+
description=data.get("description", ""),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class PageClassIR:
|
|
243
|
+
name: str
|
|
244
|
+
base_class: str = "BasePage"
|
|
245
|
+
url: Optional[str] = None
|
|
246
|
+
locators: List[LocatorIR] = field(default_factory=list)
|
|
247
|
+
methods: List[MethodIR] = field(default_factory=list)
|
|
248
|
+
description: str = ""
|
|
249
|
+
|
|
250
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
251
|
+
return {
|
|
252
|
+
"name": self.name,
|
|
253
|
+
"base_class": self.base_class,
|
|
254
|
+
"url": self.url,
|
|
255
|
+
"locators": [loc.to_dict() for loc in self.locators],
|
|
256
|
+
"methods": [m.to_dict() for m in self.methods],
|
|
257
|
+
"description": self.description,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PageClassIR":
|
|
262
|
+
return cls(
|
|
263
|
+
name=data.get("name", "UnnamedPage"),
|
|
264
|
+
base_class=data.get("base_class", "BasePage"),
|
|
265
|
+
url=data.get("url"),
|
|
266
|
+
locators=[LocatorIR.from_dict(loc) for loc in data.get("locators", [])],
|
|
267
|
+
methods=[MethodIR.from_dict(m) for m in data.get("methods", [])],
|
|
268
|
+
description=data.get("description", ""),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class TestCaseIR:
|
|
274
|
+
id: str
|
|
275
|
+
title: str
|
|
276
|
+
steps: List[StepIR] = field(default_factory=list)
|
|
277
|
+
tags: List[str] = field(default_factory=list)
|
|
278
|
+
description: str = ""
|
|
279
|
+
produced_variables: List[str] = field(default_factory=list)
|
|
280
|
+
consumed_variables: List[str] = field(default_factory=list)
|
|
281
|
+
hermetic_fixtures: List[HermeticFixture] = field(default_factory=list)
|
|
282
|
+
|
|
283
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
284
|
+
return {
|
|
285
|
+
"id": self.id,
|
|
286
|
+
"title": self.title,
|
|
287
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
288
|
+
"tags": self.tags,
|
|
289
|
+
"description": self.description,
|
|
290
|
+
"produced_variables": self.produced_variables,
|
|
291
|
+
"consumed_variables": self.consumed_variables,
|
|
292
|
+
"hermetic_fixtures": [hf.to_dict() for hf in self.hermetic_fixtures],
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TestCaseIR":
|
|
297
|
+
return cls(
|
|
298
|
+
id=data.get("id", "TC_001"),
|
|
299
|
+
title=data.get("title", "Automated Test Case"),
|
|
300
|
+
steps=[StepIR.from_dict(s) for s in data.get("steps", [])],
|
|
301
|
+
tags=data.get("tags", []),
|
|
302
|
+
description=data.get("description", ""),
|
|
303
|
+
produced_variables=data.get("produced_variables", []),
|
|
304
|
+
consumed_variables=data.get("consumed_variables", []),
|
|
305
|
+
hermetic_fixtures=[HermeticFixture.from_dict(hf) for hf in data.get("hermetic_fixtures", [])],
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class ProjectIR:
|
|
311
|
+
name: str = "OmniTestProject"
|
|
312
|
+
source_language: str = "generic"
|
|
313
|
+
source_framework: str = "generic"
|
|
314
|
+
pages: List[PageClassIR] = field(default_factory=list)
|
|
315
|
+
test_cases: List[TestCaseIR] = field(default_factory=list)
|
|
316
|
+
|
|
317
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
318
|
+
return {
|
|
319
|
+
"name": self.name,
|
|
320
|
+
"source_language": self.source_language,
|
|
321
|
+
"source_framework": self.source_framework,
|
|
322
|
+
"pages": [p.to_dict() for p in self.pages],
|
|
323
|
+
"test_cases": [tc.to_dict() for tc in self.test_cases],
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def to_json(self, indent: int = 2) -> str:
|
|
327
|
+
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ProjectIR":
|
|
331
|
+
return cls(
|
|
332
|
+
name=data.get("name", "OmniTestProject"),
|
|
333
|
+
source_language=data.get("source_language", "generic"),
|
|
334
|
+
source_framework=data.get("source_framework", "generic"),
|
|
335
|
+
pages=[PageClassIR.from_dict(p) for p in data.get("pages", [])],
|
|
336
|
+
test_cases=[TestCaseIR.from_dict(tc) for tc in data.get("test_cases", [])],
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def from_json(cls, json_str: str) -> "ProjectIR":
|
|
341
|
+
data = json.loads(json_str)
|
|
342
|
+
return cls.from_dict(data)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
import re
|
|
346
|
+
|
|
347
|
+
SHARED_VAR_PATTERNS = [
|
|
348
|
+
(r"\b(?:created_id|order_id|user_id|item_id|customer_id|invoice_id)\b", DataDependencyType.ID),
|
|
349
|
+
(r"\b(?:auth_token|bearer_token|access_token|jwt_token|token)\b", DataDependencyType.AUTH_TOKEN),
|
|
350
|
+
(r"\b(?:last_order_number|order_number|receipt_no|tracking_code)\b", DataDependencyType.ORDER_NUMBER),
|
|
351
|
+
(r"\b(?:email|user_email|customer_email)\b", DataDependencyType.EMAIL),
|
|
352
|
+
(r"\b(?:session_id|cookie_session|session_token)\b", DataDependencyType.SESSION),
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def analyze_data_dependencies(project: ProjectIR) -> List[SharedStateVariable]:
|
|
357
|
+
"""
|
|
358
|
+
Hermetic State & Data-Dependency Resolver:
|
|
359
|
+
Performs data-flow analysis across test cases in ProjectIR to identify shared state
|
|
360
|
+
and test coupling (variables produced in one test and consumed in another).
|
|
361
|
+
"""
|
|
362
|
+
producers: Dict[str, str] = {}
|
|
363
|
+
consumers: Dict[str, List[str]] = {}
|
|
364
|
+
var_types: Dict[str, DataDependencyType] = {}
|
|
365
|
+
|
|
366
|
+
for tc in project.test_cases:
|
|
367
|
+
tc_produced = set(tc.produced_variables)
|
|
368
|
+
tc_consumed = set(tc.consumed_variables)
|
|
369
|
+
|
|
370
|
+
for s in tc.steps:
|
|
371
|
+
text = f"{s.target or ''} {s.value or ''} {s.description or ''}"
|
|
372
|
+
|
|
373
|
+
creation_match = re.search(
|
|
374
|
+
r"\b(?:const|let|var|def|String|int)?\s*([a-zA-Z0-9_]+)\s*=\s*(?:response|created|new|generate|extract|get)",
|
|
375
|
+
text,
|
|
376
|
+
re.I
|
|
377
|
+
)
|
|
378
|
+
if creation_match:
|
|
379
|
+
var_name = creation_match.group(1).lower()
|
|
380
|
+
tc_produced.add(var_name)
|
|
381
|
+
if var_name not in producers:
|
|
382
|
+
producers[var_name] = tc.id
|
|
383
|
+
|
|
384
|
+
for pattern, dep_type in SHARED_VAR_PATTERNS:
|
|
385
|
+
matches = re.findall(pattern, text, re.I)
|
|
386
|
+
for m in matches:
|
|
387
|
+
v_name = m.lower()
|
|
388
|
+
var_types[v_name] = dep_type
|
|
389
|
+
if v_name not in tc_produced:
|
|
390
|
+
tc_consumed.add(v_name)
|
|
391
|
+
if v_name not in consumers:
|
|
392
|
+
consumers[v_name] = []
|
|
393
|
+
if tc.id not in consumers[v_name]:
|
|
394
|
+
consumers[v_name].append(tc.id)
|
|
395
|
+
|
|
396
|
+
tc.produced_variables = sorted(list(tc_produced))
|
|
397
|
+
tc.consumed_variables = sorted(list(tc_consumed))
|
|
398
|
+
|
|
399
|
+
shared_vars: List[SharedStateVariable] = []
|
|
400
|
+
all_var_names = set(producers.keys()) | set(consumers.keys())
|
|
401
|
+
for v_name in sorted(all_var_names):
|
|
402
|
+
d_type = var_types.get(v_name, DataDependencyType.CUSTOM)
|
|
403
|
+
shared_vars.append(SharedStateVariable(
|
|
404
|
+
name=v_name,
|
|
405
|
+
producer_test_id=producers.get(v_name),
|
|
406
|
+
consumer_test_ids=consumers.get(v_name, []),
|
|
407
|
+
dep_type=d_type,
|
|
408
|
+
synthetic_fixture_code=f"const {v_name} = 'HERMETIC_AUTO_' + Date.now();",
|
|
409
|
+
mock_route=f"/api/v1/mock/{v_name}"
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
return shared_vars
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def resolve_hermetic_fixtures(project: ProjectIR) -> ProjectIR:
|
|
416
|
+
"""
|
|
417
|
+
Transforms coupled test cases into 100% Hermetic Tests:
|
|
418
|
+
Auto-generates and prepends Pre-flight Fixtures or API Data Seeds for any test case
|
|
419
|
+
consuming external state without producing it locally.
|
|
420
|
+
Enables zero-collision parallel sharding and isolated test impact execution.
|
|
421
|
+
"""
|
|
422
|
+
shared_vars = analyze_data_dependencies(project)
|
|
423
|
+
var_map = {sv.name: sv for sv in shared_vars}
|
|
424
|
+
|
|
425
|
+
for tc in project.test_cases:
|
|
426
|
+
fixtures_to_inject = []
|
|
427
|
+
for consumed in list(tc.consumed_variables):
|
|
428
|
+
if consumed not in tc.produced_variables:
|
|
429
|
+
sv = var_map.get(consumed)
|
|
430
|
+
dep_type = sv.dep_type if sv else DataDependencyType.CUSTOM
|
|
431
|
+
|
|
432
|
+
if dep_type in (DataDependencyType.AUTH_TOKEN, DataDependencyType.SESSION):
|
|
433
|
+
fixture = HermeticFixture(
|
|
434
|
+
variable_name=consumed,
|
|
435
|
+
fixture_type="auth_session",
|
|
436
|
+
seed_endpoint="/api/v1/auth/mock",
|
|
437
|
+
mock_data={"token": "HERMETIC_MOCK_JWT", "role": "ADMIN", "active": True},
|
|
438
|
+
code_snippet=f"// Hermetic Auth Fixture\nawait page.setExtraHTTPHeaders({{ Authorization: 'Bearer HERMETIC_JWT_{consumed}' }});"
|
|
439
|
+
)
|
|
440
|
+
seed_step = StepIR(
|
|
441
|
+
action=ActionType.API_CALL,
|
|
442
|
+
target="/api/v1/auth/session",
|
|
443
|
+
value=f'{{"{consumed}": "HERMETIC_MOCK_JWT", "status": "ACTIVE"}}',
|
|
444
|
+
description=f"Hermetic Pre-flight: Injected Zero-Second Auth Session for {consumed}"
|
|
445
|
+
)
|
|
446
|
+
elif dep_type in (DataDependencyType.ID, DataDependencyType.ORDER_NUMBER):
|
|
447
|
+
endpoint = f"/api/v1/{consumed.replace('_id', 's')}/seed"
|
|
448
|
+
fixture = HermeticFixture(
|
|
449
|
+
variable_name=consumed,
|
|
450
|
+
fixture_type="api_seed",
|
|
451
|
+
seed_endpoint=endpoint,
|
|
452
|
+
mock_data={"id": f"AUTO_{consumed.upper()}_SEED", "status": "CONFIRMED"},
|
|
453
|
+
code_snippet=f"const {consumed} = 'AUTO_SEED_' + Date.now();"
|
|
454
|
+
)
|
|
455
|
+
seed_step = StepIR(
|
|
456
|
+
action=ActionType.API_CALL,
|
|
457
|
+
target=endpoint,
|
|
458
|
+
value=f'{{"{consumed}": "AUTO_SEED_" + Date.now(), "status": "CONFIRMED"}}',
|
|
459
|
+
description=f"Hermetic Pre-flight: Seed direct API data record for {consumed}"
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
fixture = HermeticFixture(
|
|
463
|
+
variable_name=consumed,
|
|
464
|
+
fixture_type="synthetic_mock",
|
|
465
|
+
code_snippet=f"const {consumed} = 'HERMETIC_' + Date.now();"
|
|
466
|
+
)
|
|
467
|
+
seed_step = StepIR(
|
|
468
|
+
action=ActionType.CUSTOM_CALL,
|
|
469
|
+
value=f"const {consumed} = 'HERMETIC_' + Date.now();",
|
|
470
|
+
description=f"Hermetic Pre-flight: Synthetic state generator for {consumed}"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
tc.hermetic_fixtures.append(fixture)
|
|
474
|
+
fixtures_to_inject.append(seed_step)
|
|
475
|
+
tc.produced_variables.append(consumed)
|
|
476
|
+
|
|
477
|
+
if fixtures_to_inject:
|
|
478
|
+
tc.steps = fixtures_to_inject + tc.steps
|
|
479
|
+
|
|
480
|
+
return project
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pre-check validator for Groovy scripts and POM keywords in enterprise test suites.
|
|
4
|
+
Catches syntax errors, broken GString escape sequences, and structural balance issues
|
|
5
|
+
before running tests in CI, ensuring 100% parity with Katalon compiler checks.
|
|
6
|
+
Supports dynamic target repository resolution (CLI flag, CWD auto-detect, ENV var).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import re
|
|
12
|
+
import argparse
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
16
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
17
|
+
try:
|
|
18
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
19
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
HUB_ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
27
|
+
"""
|
|
28
|
+
Resolves the target repository path:
|
|
29
|
+
1. Explicit CLI flag: --repo / --path or positional target.
|
|
30
|
+
2. Environment variable: TARGET_PROJECT_DIR.
|
|
31
|
+
3. Current Working Directory (if different from Hub and has Scripts/ or Keywords/).
|
|
32
|
+
4. Default fallback: QA Hub root (parent of tools/).
|
|
33
|
+
"""
|
|
34
|
+
chosen = cli_repo or cli_target
|
|
35
|
+
if chosen:
|
|
36
|
+
resolved = Path(chosen).resolve()
|
|
37
|
+
if not resolved.exists():
|
|
38
|
+
raise FileNotFoundError(f"Target repository path not found: {resolved}")
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
env_dir = os.environ.get("TARGET_PROJECT_DIR")
|
|
42
|
+
if env_dir and os.path.exists(env_dir):
|
|
43
|
+
return Path(env_dir).resolve()
|
|
44
|
+
|
|
45
|
+
cwd = Path.cwd().resolve()
|
|
46
|
+
if cwd != HUB_ROOT:
|
|
47
|
+
if (cwd / "Scripts").exists() or (cwd / "Keywords").exists():
|
|
48
|
+
return cwd
|
|
49
|
+
|
|
50
|
+
return HUB_ROOT
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def strip_comments_and_strings(source_code: str) -> str:
|
|
54
|
+
"""Strips comments, string literals, and regexes so literal braces/parentheses don't cause false positives."""
|
|
55
|
+
pattern = re.compile(
|
|
56
|
+
r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
|
|
57
|
+
r'(?P<LINE_COMMENT>//[^\r\n]*)|'
|
|
58
|
+
r'(?P<TRIPLE_DOUBLE>"""[\s\S]*?""")|'
|
|
59
|
+
r'(?P<TRIPLE_SINGLE>\'\'\'[\s\S]*?\'\'\')|'
|
|
60
|
+
r'(?P<DOLLAR_SLASHY>\$/[\s\S]*?/\$)|'
|
|
61
|
+
r'(?P<DOUBLE_STRING>"(?:\\.|[^"\\])*")|'
|
|
62
|
+
r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')',
|
|
63
|
+
re.DOTALL
|
|
64
|
+
)
|
|
65
|
+
code = pattern.sub(' ', source_code)
|
|
66
|
+
|
|
67
|
+
# Strip slashy regexes (e.g., ~/regex/ or = /regex/) while avoiding arithmetic division (e.g. a / b + c / d)
|
|
68
|
+
# In Groovy, slashy regex only appears after operators ~, =, (, [, {, :, ,, ;
|
|
69
|
+
def strip_slashy(m):
|
|
70
|
+
return (m.group(1) or '') + ' '
|
|
71
|
+
|
|
72
|
+
return re.sub(r'([=~:,(\[{;]\s*)/(?:\\/|[^/\r\n])+/(?![/*])', strip_slashy, code)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_file(file_path: Path) -> list:
|
|
76
|
+
errors = []
|
|
77
|
+
try:
|
|
78
|
+
content = file_path.read_text(encoding='utf-8')
|
|
79
|
+
except UnicodeDecodeError:
|
|
80
|
+
try:
|
|
81
|
+
content = file_path.read_text(encoding='latin-1')
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return [f"Cannot read file: {e}"]
|
|
84
|
+
|
|
85
|
+
lines = content.splitlines()
|
|
86
|
+
for idx, line in enumerate(lines, start=1):
|
|
87
|
+
# 1. Check for illegal backslash escapes like [\]
|
|
88
|
+
if re.search(r"\[\\\]", line):
|
|
89
|
+
errors.append(f"Line {idx}: Illegal escape sequence '[\\]'. Did you mean '[${{var}}]'?")
|
|
90
|
+
|
|
91
|
+
# 2. Check for lonely unescaped backslash at end of quotes
|
|
92
|
+
if re.search(r'["\']\\["\']', line):
|
|
93
|
+
errors.append(f"Line {idx}: Unescaped single backslash in string.")
|
|
94
|
+
|
|
95
|
+
# 3 & 4. Check structural balance on clean code without string/comment noise
|
|
96
|
+
clean_content = strip_comments_and_strings(content)
|
|
97
|
+
open_braces = clean_content.count('{')
|
|
98
|
+
close_braces = clean_content.count('}')
|
|
99
|
+
if open_braces != close_braces:
|
|
100
|
+
errors.append(f"Unmatched braces: {open_braces} '{{' vs {close_braces} '}}'")
|
|
101
|
+
|
|
102
|
+
open_parens = clean_content.count('(')
|
|
103
|
+
close_parens = clean_content.count(')')
|
|
104
|
+
if open_parens != close_parens:
|
|
105
|
+
errors.append(f"Unmatched parentheses: {open_parens} '(' vs {close_parens} ')'")
|
|
106
|
+
|
|
107
|
+
return errors
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
parser = argparse.ArgumentParser(description="Enterprise Groovy Syntax & Structural Validator")
|
|
112
|
+
parser.add_argument("target", nargs="?", default=None, help="Target repository or file/directory to validate")
|
|
113
|
+
parser.add_argument("--repo", "--path", dest="repo_path", default=None, help="Target repository root")
|
|
114
|
+
parser.add_argument("--dir", default=None, help="Specific directory to scan for .groovy files")
|
|
115
|
+
args = parser.parse_args()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
target_repo = resolve_target_repo(cli_target=args.target, cli_repo=args.repo_path)
|
|
119
|
+
except FileNotFoundError as e:
|
|
120
|
+
print(f"❌ Error: {e}")
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
print("=" * 60)
|
|
124
|
+
print("Enterprise Automation: Pre-check Groovy Scripts & Keywords")
|
|
125
|
+
print(f"Target Repository : {target_repo}")
|
|
126
|
+
print("=" * 60)
|
|
127
|
+
|
|
128
|
+
all_errors = {}
|
|
129
|
+
files_checked = 0
|
|
130
|
+
|
|
131
|
+
if args.dir:
|
|
132
|
+
scan_dir = Path(args.dir).resolve()
|
|
133
|
+
target_dirs = [scan_dir] if scan_dir.exists() else []
|
|
134
|
+
elif args.target and Path(args.target).is_file():
|
|
135
|
+
target_dirs = []
|
|
136
|
+
target_file = Path(args.target).resolve()
|
|
137
|
+
files_checked += 1
|
|
138
|
+
errs = check_file(target_file)
|
|
139
|
+
if errs:
|
|
140
|
+
all_errors[str(target_file)] = errs
|
|
141
|
+
else:
|
|
142
|
+
target_dirs = [target_repo / 'Scripts', target_repo / 'Keywords']
|
|
143
|
+
|
|
144
|
+
for t_dir in target_dirs:
|
|
145
|
+
if not t_dir.exists():
|
|
146
|
+
continue
|
|
147
|
+
for f in t_dir.rglob('*.groovy'):
|
|
148
|
+
files_checked += 1
|
|
149
|
+
errs = check_file(f)
|
|
150
|
+
if errs:
|
|
151
|
+
try:
|
|
152
|
+
rel_name = str(f.relative_to(target_repo))
|
|
153
|
+
except ValueError:
|
|
154
|
+
rel_name = str(f)
|
|
155
|
+
all_errors[rel_name] = errs
|
|
156
|
+
|
|
157
|
+
print(f"Checked {files_checked} Groovy files.")
|
|
158
|
+
|
|
159
|
+
if all_errors:
|
|
160
|
+
print("\n❌ SYNTAX PRE-CHECK FAILED! The following files have syntax errors:\n")
|
|
161
|
+
for f_name, errs in all_errors.items():
|
|
162
|
+
print(f"File: {f_name}")
|
|
163
|
+
for err in errs:
|
|
164
|
+
print(f" - {err}")
|
|
165
|
+
print()
|
|
166
|
+
return 1
|
|
167
|
+
else:
|
|
168
|
+
print("✅ All Groovy scripts and keywords passed syntax pre-check 100%!")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == '__main__':
|
|
173
|
+
sys.exit(main())
|