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,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
tools/generate_tc_from_story.py ā Large Action Model (LAM) Script Generator
|
|
5
|
+
Inspired by LaVague: Converts natural language user stories into Clean POM test scripts.
|
|
6
|
+
Supports configurable packages, dynamic Page Class discovery, and cross-platform execution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import glob
|
|
13
|
+
import argparse
|
|
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
|
+
DEFAULT_PAGE_MAP = {
|
|
24
|
+
"login": "LoginPage",
|
|
25
|
+
"auth": "LoginPage",
|
|
26
|
+
"user": "UserAdminPage",
|
|
27
|
+
"common": "CommonCategoryPage",
|
|
28
|
+
"category": "CategoryPage",
|
|
29
|
+
"planning": "PlanningPage",
|
|
30
|
+
"report": "ReportPage",
|
|
31
|
+
"search": "SearchPage",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def detect_project_package(repo_root="."):
|
|
36
|
+
"""Detects the standard package prefix from existing project structure or guide."""
|
|
37
|
+
# 1. Check PROJECT_AI_GUIDE.md for explicit package hints
|
|
38
|
+
guide_path = os.path.join(repo_root, "PROJECT_AI_GUIDE.md")
|
|
39
|
+
if os.path.isfile(guide_path):
|
|
40
|
+
try:
|
|
41
|
+
with open(guide_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
m = re.search(r'import\s+([\w\.]+)\.pages\.', content)
|
|
44
|
+
if m:
|
|
45
|
+
return f"{m.group(1)}.pages"
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# 2. Check existing Keywords/ directory
|
|
50
|
+
keywords_dir = os.path.join(repo_root, "Keywords")
|
|
51
|
+
if os.path.isdir(keywords_dir):
|
|
52
|
+
for root, _, files in os.walk(keywords_dir):
|
|
53
|
+
for file in files:
|
|
54
|
+
if file.endswith(".groovy"):
|
|
55
|
+
rel = os.path.relpath(root, keywords_dir)
|
|
56
|
+
parts = [p for p in rel.replace("\\", "/").split("/") if p and p != "."]
|
|
57
|
+
if parts:
|
|
58
|
+
return ".".join(parts)
|
|
59
|
+
|
|
60
|
+
return "pages"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def discover_page_classes(repo_root="."):
|
|
64
|
+
"""Discovers available Page classes dynamically from workspace files."""
|
|
65
|
+
pages = dict(DEFAULT_PAGE_MAP)
|
|
66
|
+
search_dirs = [
|
|
67
|
+
os.path.join(repo_root, "Keywords"),
|
|
68
|
+
os.path.join(repo_root, "pages"),
|
|
69
|
+
os.path.join(repo_root, "src", "pages"),
|
|
70
|
+
os.path.join(repo_root, "tests", "pages")
|
|
71
|
+
]
|
|
72
|
+
for d in search_dirs:
|
|
73
|
+
if os.path.isdir(d):
|
|
74
|
+
for f in glob.glob(os.path.join(d, "**", "*Page*.*"), recursive=True):
|
|
75
|
+
base = os.path.basename(f)
|
|
76
|
+
name, _ = os.path.splitext(base)
|
|
77
|
+
clean_key = name.lower().replace("page", "").replace("_", "")
|
|
78
|
+
if clean_key:
|
|
79
|
+
pages[clean_key] = name
|
|
80
|
+
return pages
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def generate_script(story, module_name="CategoryPage", tc_name="TC_AutoGenerated", package_name=None, repo_root="."):
|
|
84
|
+
detected_package = package_name or detect_project_package(repo_root)
|
|
85
|
+
discovered_pages = discover_page_classes(repo_root)
|
|
86
|
+
|
|
87
|
+
# Normalize module name
|
|
88
|
+
mod_clean = module_name.lower().replace("page", "").replace("_", "")
|
|
89
|
+
target_page = discovered_pages.get(mod_clean, f"{module_name[:1].upper()}{module_name[1:]}")
|
|
90
|
+
if not target_page.endswith("Page"):
|
|
91
|
+
target_page += "Page"
|
|
92
|
+
|
|
93
|
+
# Detect action intent from story keywords
|
|
94
|
+
story_lower = story.lower()
|
|
95
|
+
is_delete = any(k in story_lower for k in ["xóa", "xoa", "delete", "remove"])
|
|
96
|
+
is_edit = any(k in story_lower for k in ["sį»a", "sua", "chį»nh sį»a", "chinh sua", "edit", "update"])
|
|
97
|
+
is_search = any(k in story_lower for k in ["tìm", "tim", "tìm kiếm", "tim kiem", "search", "filter"])
|
|
98
|
+
is_add = not (is_delete or is_edit or is_search) or any(k in story_lower for k in ["thĆŖm", "them", "tįŗ”o mį»i", "tao moi", "add", "create"])
|
|
99
|
+
|
|
100
|
+
lines = []
|
|
101
|
+
lines.append("// ===========================================================================")
|
|
102
|
+
lines.append(f"// Test Case: {tc_name}")
|
|
103
|
+
lines.append(f"// Requirement: {story}")
|
|
104
|
+
lines.append("// Architecture: Clean POM & Flat Architecture (Generated by LaVague LAM Engine)")
|
|
105
|
+
lines.append("// ===========================================================================")
|
|
106
|
+
lines.append("")
|
|
107
|
+
lines.append(f"import {detected_package}.LoginPage")
|
|
108
|
+
lines.append(f"import {detected_package}.{target_page}")
|
|
109
|
+
lines.append("")
|
|
110
|
+
lines.append("// 1. Ensure system authentication session")
|
|
111
|
+
lines.append("LoginPage.ensureLoggedIn()")
|
|
112
|
+
lines.append("")
|
|
113
|
+
lines.append("// 2. Initialize dynamic idempotent test data")
|
|
114
|
+
lines.append('String uniqueCode = "AUTO_" + System.currentTimeMillis()')
|
|
115
|
+
lines.append('String testName = "Test Entity " + uniqueCode')
|
|
116
|
+
lines.append("")
|
|
117
|
+
|
|
118
|
+
if is_add:
|
|
119
|
+
lines.append("// 3. Execute entity creation workflow")
|
|
120
|
+
lines.append(f"{target_page}.openCreateModal()")
|
|
121
|
+
lines.append(f"{target_page}.fillCreateForm(uniqueCode, testName)")
|
|
122
|
+
lines.append(f"{target_page}.confirmCreate()")
|
|
123
|
+
lines.append(f"{target_page}.verifyCreateSuccess(uniqueCode)")
|
|
124
|
+
|
|
125
|
+
elif is_edit:
|
|
126
|
+
lines.append("// 3. Execute entity update workflow")
|
|
127
|
+
lines.append('String updatedName = testName + " [Updated]"')
|
|
128
|
+
lines.append(f"{target_page}.searchKeyword(uniqueCode)")
|
|
129
|
+
lines.append(f"{target_page}.openEditFirstRow()")
|
|
130
|
+
lines.append(f"{target_page}.updateName(updatedName)")
|
|
131
|
+
lines.append(f"{target_page}.confirmEdit()")
|
|
132
|
+
lines.append(f"{target_page}.verifyEditSuccess(updatedName)")
|
|
133
|
+
|
|
134
|
+
elif is_delete:
|
|
135
|
+
lines.append("// 3. Execute entity deletion workflow")
|
|
136
|
+
lines.append(f"{target_page}.searchKeyword(uniqueCode)")
|
|
137
|
+
lines.append(f"{target_page}.clickDeleteFirstRow()")
|
|
138
|
+
lines.append(f"{target_page}.confirmDelete()")
|
|
139
|
+
lines.append(f"{target_page}.verifyDeleted(uniqueCode)")
|
|
140
|
+
|
|
141
|
+
elif is_search:
|
|
142
|
+
lines.append("// 3. Execute entity search/filter workflow")
|
|
143
|
+
lines.append(f"{target_page}.searchKeyword(uniqueCode)")
|
|
144
|
+
lines.append(f"{target_page}.verifySearchResults(uniqueCode)")
|
|
145
|
+
|
|
146
|
+
lines.append("")
|
|
147
|
+
lines.append("// 4. Automated idempotent teardown cleanup")
|
|
148
|
+
lines.append(f"// {target_page}.teardown(uniqueCode)")
|
|
149
|
+
lines.append("")
|
|
150
|
+
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
parser = argparse.ArgumentParser(description="LaVague-style LAM Script Generator for Clean POM Automation")
|
|
156
|
+
parser.add_argument("--story", "-s", required=True, help="User story or Acceptance Criteria (natural language)")
|
|
157
|
+
parser.add_argument("--module", "-m", default="CategoryPage", help="Target Page Class or Module name")
|
|
158
|
+
parser.add_argument("--name", "-n", default="TC_AutoGenerated", help="Test Case name")
|
|
159
|
+
parser.add_argument("--package", "-p", default=None, help="Base package name for imports (e.g. 'com.company.pages')")
|
|
160
|
+
parser.add_argument("--out", "-o", default=None, help="Output file path (optional)")
|
|
161
|
+
args = parser.parse_args()
|
|
162
|
+
|
|
163
|
+
script_content = generate_script(
|
|
164
|
+
story=args.story,
|
|
165
|
+
module_name=args.module,
|
|
166
|
+
tc_name=args.name,
|
|
167
|
+
package_name=args.package
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if args.out:
|
|
171
|
+
out_dir = os.path.dirname(args.out)
|
|
172
|
+
if out_dir:
|
|
173
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
174
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
175
|
+
f.write(script_content)
|
|
176
|
+
print(f"Generated Clean POM script successfully saved to: {args.out}")
|
|
177
|
+
else:
|
|
178
|
+
print(script_content)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PowerShell Script to ensure TestReports and summaryTemp.txt for CI/CD Dashboard
|
|
2
|
+
param(
|
|
3
|
+
[string]$ProjectName = "Enterprise-Automation"
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
7
|
+
Write-Host "=== [Centralize CI/CD] Generating TestReports & summaryTemp.txt for $ProjectName ===" -ForegroundColor Cyan
|
|
8
|
+
|
|
9
|
+
$testReportsDir = Join-Path $PSScriptRoot "..\TestReports"
|
|
10
|
+
$reportsDir = Join-Path $PSScriptRoot "..\Reports"
|
|
11
|
+
|
|
12
|
+
if (-not (Test-Path $testReportsDir)) {
|
|
13
|
+
New-Item -ItemType Directory -Path $testReportsDir -Force | Out-Null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
$passed = 0
|
|
17
|
+
$failures = 0
|
|
18
|
+
$errors = 0
|
|
19
|
+
$incomplete = 0
|
|
20
|
+
$skipped = 0
|
|
21
|
+
$foundExistingSummary = $false
|
|
22
|
+
|
|
23
|
+
# 0. Check if Cleanup_Browser already exported summaryTemp.txt with business test case counts
|
|
24
|
+
$existingSummaryFile = Join-Path $reportsDir "summaryTemp.txt"
|
|
25
|
+
if (-not (Test-Path $existingSummaryFile)) {
|
|
26
|
+
$existingSummaryFile = Join-Path $testReportsDir "summaryTemp.txt"
|
|
27
|
+
}
|
|
28
|
+
if (Test-Path $existingSummaryFile) {
|
|
29
|
+
try {
|
|
30
|
+
$rawSummary = [System.IO.File]::ReadAllText($existingSummaryFile, [System.Text.Encoding]::UTF8)
|
|
31
|
+
if ($rawSummary -match "(\d+)\s+Passed\s+-\s+.*?(\d+)\s+Failures\s+-\s+.*?(\d+)\s+Errors") {
|
|
32
|
+
$passed = [int]$matches[1]
|
|
33
|
+
$failures = [int]$matches[2]
|
|
34
|
+
$errors = [int]$matches[3]
|
|
35
|
+
if ($passed -gt 0) {
|
|
36
|
+
$foundExistingSummary = $true
|
|
37
|
+
Write-Host "[+] Found valid summaryTemp.txt from Cleanup_Browser: $rawSummary" -ForegroundColor Green
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# 1. Find the latest report directory (check Reports, then TestReports)
|
|
44
|
+
$latestReport = Get-ChildItem -Path @($reportsDir, $testReportsDir) -Recurse -Filter "*junit*.xml" -ErrorAction SilentlyContinue |
|
|
45
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
46
|
+
|
|
47
|
+
if (-not $foundExistingSummary -and $latestReport) {
|
|
48
|
+
Write-Host "[+] Found latest JUnit report: $($latestReport.FullName)" -ForegroundColor Green
|
|
49
|
+
try {
|
|
50
|
+
[xml]$xml = Get-Content $latestReport.FullName
|
|
51
|
+
if ($xml.testsuites) {
|
|
52
|
+
$passed = [int]($xml.testsuites.passed)
|
|
53
|
+
$failures = [int]($xml.testsuites.failures)
|
|
54
|
+
$errors = [int]($xml.testsuites.errors)
|
|
55
|
+
$incomplete = [int]($xml.testsuites.incompleted)
|
|
56
|
+
$skipped = [int]($xml.testsuites.skipped)
|
|
57
|
+
} elseif ($xml.testsuite) {
|
|
58
|
+
$tests = [int]($xml.testsuite.tests)
|
|
59
|
+
$failures = [int]($xml.testsuite.failures)
|
|
60
|
+
$errors = [int]($xml.testsuite.errors)
|
|
61
|
+
$skipped = [int]($xml.testsuite.skipped)
|
|
62
|
+
$passed = [Math]::Max(0, $tests - $failures - $errors - $skipped)
|
|
63
|
+
}
|
|
64
|
+
$foundExistingSummary = $true
|
|
65
|
+
} catch {
|
|
66
|
+
Write-Warning "[!] Failed to parse XML: $_"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (-not $foundExistingSummary) {
|
|
71
|
+
# Fallback to console0.log
|
|
72
|
+
$latestLog = Get-ChildItem -Path @($reportsDir, $testReportsDir) -Recurse -Filter "console0.log" -ErrorAction SilentlyContinue |
|
|
73
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
74
|
+
|
|
75
|
+
if ($latestLog) {
|
|
76
|
+
Write-Host "[+] Fallback: Parsing console log: $($latestLog.FullName)" -ForegroundColor Yellow
|
|
77
|
+
$logContent = Get-Content $latestLog.FullName -Raw
|
|
78
|
+
if ($logContent -match "Passed:\s*(\d+)\s*\|\s*Failed:\s*(\d+)\s*\|\s*Errors:\s*(\d+)") {
|
|
79
|
+
$passed = [int]$matches[1]
|
|
80
|
+
$failures = [int]$matches[2]
|
|
81
|
+
$errors = [int]$matches[3]
|
|
82
|
+
} elseif ($logContent -match "Tį»ng sį»:\s*(\d+)\s*\|\s*Äįŗ”t:\s*(\d+)\s*\|\s*Thįŗ„t bįŗ”i:\s*(\d+)\s*\|\s*Lį»i:\s*(\d+)") {
|
|
83
|
+
$passed = [int]$matches[2]
|
|
84
|
+
$failures = [int]$matches[3]
|
|
85
|
+
$errors = [int]$matches[4]
|
|
86
|
+
} else {
|
|
87
|
+
$passed = ([regex]::Matches($logContent, "\[TEST_CASE\]\[PASSED\]")).Count
|
|
88
|
+
$failures = ([regex]::Matches($logContent, "\[TEST_CASE\]\[FAILED\]")).Count
|
|
89
|
+
$errors = ([regex]::Matches($logContent, "\[TEST_CASE\]\[ERROR\]")).Count
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Write-Host "[+] Metrics: Passed=$passed, Failures=$failures, Errors=$errors, Incomplete=$incomplete, Skipped=$skipped" -ForegroundColor Cyan
|
|
95
|
+
|
|
96
|
+
# 2. Write summaryTemp.txt: \u2705 {P} Passed - \u274C {F} Failures - \u274C {E} Errors - \u2757 {I} Incomplete - \u26AA {S} Skipped
|
|
97
|
+
$summaryContent = "\u2705 $passed Passed - \u274C $failures Failures - \u274C $errors Errors - \u2757 $incomplete Incomplete - \u26AA $skipped Skipped"
|
|
98
|
+
|
|
99
|
+
$summaryFile = Join-Path $testReportsDir "summaryTemp.txt"
|
|
100
|
+
[System.IO.File]::WriteAllText($summaryFile, $summaryContent, [System.Text.Encoding]::ASCII)
|
|
101
|
+
Write-Host "[+] Created: $summaryFile" -ForegroundColor Green
|
|
102
|
+
Write-Host " $summaryContent"
|
|
103
|
+
|
|
104
|
+
# Also write to Reports/summaryTemp.txt for fallback
|
|
105
|
+
if (Test-Path $reportsDir) {
|
|
106
|
+
[System.IO.File]::WriteAllText((Join-Path $reportsDir "summaryTemp.txt"), $summaryContent, [System.Text.Encoding]::ASCII)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 3. Synchronize report files into TestReports/<Project>_<timestamp>/ if Katalon saved to Reports/
|
|
110
|
+
if ($latestReport) {
|
|
111
|
+
$reportParentDir = $latestReport.Directory.FullName
|
|
112
|
+
$nowStr = Get-Date -Format "yyyyMMddHHmmss"
|
|
113
|
+
if ($ProjectName -match "_\d{10,14}$") {
|
|
114
|
+
$targetProjDir = Join-Path $testReportsDir $ProjectName
|
|
115
|
+
} else {
|
|
116
|
+
$targetProjDir = Join-Path $testReportsDir "${ProjectName}_$nowStr"
|
|
117
|
+
}
|
|
118
|
+
$targetSubDir = Join-Path $targetProjDir (Split-Path $reportParentDir -Leaf)
|
|
119
|
+
|
|
120
|
+
if (-not (Test-Path $targetSubDir)) {
|
|
121
|
+
New-Item -ItemType Directory -Path $targetSubDir -Force | Out-Null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Copy-Item -Path "$reportParentDir\*" -Destination $targetSubDir -Recurse -Force
|
|
125
|
+
Write-Host "[+] Synced artifacts to: $targetSubDir" -ForegroundColor Green
|
|
126
|
+
|
|
127
|
+
# Ensure results.html exists in targetProjDir
|
|
128
|
+
$htmlFiles = Get-ChildItem -Path $targetSubDir -Filter "*.html"
|
|
129
|
+
if ($htmlFiles.Count -gt 0) {
|
|
130
|
+
Copy-Item -Path $htmlFiles[0].FullName -Destination (Join-Path $targetProjDir "results.html") -Force
|
|
131
|
+
Write-Host "[+] Created results.html in: $targetProjDir" -ForegroundColor Green
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Write-Host "=== [Centralize CI/CD] Report Generation Complete ===" -ForegroundColor Cyan
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Git Pre-Commit Architecture & Discipline Guard
|
|
4
|
+
--------------------------------------------------
|
|
5
|
+
Automated gatekeeper executed before every 'git commit'.
|
|
6
|
+
Ensures:
|
|
7
|
+
1. 100% UTF-8 No BOM across repository files.
|
|
8
|
+
2. Flat Architecture & Clean POM: Zero raw XPaths or findTestObject in Scripts/TC-xxx.
|
|
9
|
+
3. Path Portability: Zero absolute machine paths (Users/, phungcao, /path/to/project).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
FORBIDDEN_PATHS = ["/Users/", "C:\\Workspace\\", "D:\\Workspace\\", "file:///c:", "file:///d:"]
|
|
17
|
+
|
|
18
|
+
def run_cmd(cmd):
|
|
19
|
+
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
|
|
20
|
+
|
|
21
|
+
def check_bom(files):
|
|
22
|
+
bom_files = []
|
|
23
|
+
for f in files:
|
|
24
|
+
if not os.path.exists(f):
|
|
25
|
+
continue
|
|
26
|
+
try:
|
|
27
|
+
with open(f, "rb") as fp:
|
|
28
|
+
if fp.read(3) == b"\xef\xbb\xbf":
|
|
29
|
+
bom_files.append(f)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return bom_files
|
|
33
|
+
|
|
34
|
+
def check_forbidden_paths(files):
|
|
35
|
+
violations = []
|
|
36
|
+
for f in files:
|
|
37
|
+
if not os.path.exists(f) or os.path.isdir(f):
|
|
38
|
+
continue
|
|
39
|
+
# Skip this guard script itself and binary files
|
|
40
|
+
if f.endswith((".py", ".db", ".png", ".jpg", ".zip", ".tar", ".gz")):
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
|
|
44
|
+
for line_no, line in enumerate(fp, 1):
|
|
45
|
+
# Allow mention in historical report or gitignore
|
|
46
|
+
if "FINAL_REPORT.md" in f or ".gitignore" in f:
|
|
47
|
+
continue
|
|
48
|
+
for forbidden in FORBIDDEN_PATHS:
|
|
49
|
+
if forbidden in line:
|
|
50
|
+
violations.append((f, line_no, forbidden))
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
return violations
|
|
54
|
+
|
|
55
|
+
def check_scripts_clean_pom(files):
|
|
56
|
+
violations = []
|
|
57
|
+
for f in files:
|
|
58
|
+
if not os.path.exists(f):
|
|
59
|
+
continue
|
|
60
|
+
# Only check test scripts in Scripts/
|
|
61
|
+
if "Scripts/" in f and f.endswith(".groovy"):
|
|
62
|
+
try:
|
|
63
|
+
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
|
|
64
|
+
for line_no, line in enumerate(fp, 1):
|
|
65
|
+
stripped = line.strip()
|
|
66
|
+
if stripped.startswith("//"):
|
|
67
|
+
continue
|
|
68
|
+
if "findTestObject" in line:
|
|
69
|
+
violations.append((f, line_no, "Raw findTestObject is forbidden in Scripts/"))
|
|
70
|
+
if "//" in line and ("//table" in line or "//input" in line or "//button" in line):
|
|
71
|
+
violations.append((f, line_no, "Raw XPath is forbidden in Scripts/"))
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return violations
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
# If called with --install
|
|
78
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--install":
|
|
79
|
+
git_dir = run_cmd("git rev-parse --git-dir").stdout.strip()
|
|
80
|
+
if not git_dir or not os.path.isdir(git_dir):
|
|
81
|
+
print("ā Error: Not a git repository.")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
# 1. Pre-commit hook
|
|
85
|
+
pre_commit_path = os.path.join(git_dir, "hooks", "pre-commit")
|
|
86
|
+
pre_commit_content = (
|
|
87
|
+
"#!/bin/sh\n"
|
|
88
|
+
"# Enterprise Pre-Commit Guard\n"
|
|
89
|
+
"python3 \"$0/../tools/git_pre_commit_guard.py\" 2>/dev/null || python tools/git_pre_commit_guard.py \"$@\"\n"
|
|
90
|
+
)
|
|
91
|
+
with open(pre_commit_path, "w", encoding="utf-8") as fp:
|
|
92
|
+
fp.write(pre_commit_content)
|
|
93
|
+
os.chmod(pre_commit_path, 0o755)
|
|
94
|
+
|
|
95
|
+
# 2. Post-merge hook (Auto-sync memory cache on git pull)
|
|
96
|
+
post_merge_path = os.path.join(git_dir, "hooks", "post-merge")
|
|
97
|
+
post_merge_content = (
|
|
98
|
+
"#!/bin/sh\n"
|
|
99
|
+
"# Enterprise Post-Merge Hook: Automatically initialize & sync Central Agent Memory\n"
|
|
100
|
+
"if [ -f \"tools/cache_resolver.py\" ]; then\n"
|
|
101
|
+
" echo \"š [Git Post-Merge] Syncing Central Agent Memory...\"\n"
|
|
102
|
+
" python3 \"$0/../tools/cache_resolver.py\" init 2>/dev/null || python tools/cache_resolver.py init 2>/dev/null || true\n"
|
|
103
|
+
"fi\n"
|
|
104
|
+
)
|
|
105
|
+
with open(post_merge_path, "w", encoding="utf-8") as fp:
|
|
106
|
+
fp.write(post_merge_content)
|
|
107
|
+
os.chmod(post_merge_path, 0o755)
|
|
108
|
+
|
|
109
|
+
# 3. Pre-push hook (Auto-sync memory cache on git push)
|
|
110
|
+
pre_push_path = os.path.join(git_dir, "hooks", "pre-push")
|
|
111
|
+
pre_push_content = (
|
|
112
|
+
"#!/bin/sh\n"
|
|
113
|
+
"# Enterprise Pre-Push Hook: Synchronize Central Agent Memory before pushing\n"
|
|
114
|
+
"if [ -f \"tools/cache_resolver.py\" ]; then\n"
|
|
115
|
+
" echo \"š [Git Pre-Push] Synchronizing Central Agent Memory to local cache...\"\n"
|
|
116
|
+
" python3 \"$0/../tools/cache_resolver.py\" sync 2>/dev/null || python tools/cache_resolver.py sync 2>/dev/null || true\n"
|
|
117
|
+
"fi\n"
|
|
118
|
+
)
|
|
119
|
+
with open(pre_push_path, "w", encoding="utf-8") as fp:
|
|
120
|
+
fp.write(pre_push_content)
|
|
121
|
+
os.chmod(pre_push_path, 0o755)
|
|
122
|
+
|
|
123
|
+
print(f"ā
Git hooks installed successfully:")
|
|
124
|
+
print(f" - Pre-commit: {pre_commit_path}")
|
|
125
|
+
print(f" - Post-merge: {post_merge_path} (Auto-runs cache_resolver.py init on git pull)")
|
|
126
|
+
print(f" - Pre-push: {pre_push_path} (Auto-runs cache_resolver.py sync on git push)")
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
# Get staged files
|
|
130
|
+
res = run_cmd("git diff --cached --name-only")
|
|
131
|
+
staged_files = [f.strip() for f in res.stdout.splitlines() if f.strip()]
|
|
132
|
+
|
|
133
|
+
if not staged_files:
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
errors_found = False
|
|
137
|
+
|
|
138
|
+
# 1. BOM Check
|
|
139
|
+
target_exts = (".groovy", ".tc", ".ts", ".glbl", ".md", ".py", ".json", ".ps1")
|
|
140
|
+
check_files = [f for f in staged_files if f.endswith(target_exts)]
|
|
141
|
+
bom_files = check_bom(check_files)
|
|
142
|
+
if bom_files:
|
|
143
|
+
print("ā [PRE-COMMIT BLOCKED] UTF-8 BOM detected in staged files:")
|
|
144
|
+
for bf in bom_files:
|
|
145
|
+
print(f" - {bf}")
|
|
146
|
+
errors_found = True
|
|
147
|
+
|
|
148
|
+
# 2. Path Portability Check
|
|
149
|
+
path_violations = check_forbidden_paths(staged_files)
|
|
150
|
+
if path_violations:
|
|
151
|
+
print("ā [PRE-COMMIT BLOCKED] Absolute machine path detected in staged files:")
|
|
152
|
+
for f, line_no, term in path_violations:
|
|
153
|
+
print(f" - {f}:{line_no} (Forbidden: '{term}')")
|
|
154
|
+
errors_found = True
|
|
155
|
+
|
|
156
|
+
# 3. Clean POM Guard
|
|
157
|
+
pom_violations = check_scripts_clean_pom(staged_files)
|
|
158
|
+
if pom_violations:
|
|
159
|
+
print("ā [PRE-COMMIT BLOCKED] Clean POM violation in Scripts/:")
|
|
160
|
+
for f, line_no, reason in pom_violations:
|
|
161
|
+
print(f" - {f}:{line_no} ({reason})")
|
|
162
|
+
errors_found = True
|
|
163
|
+
|
|
164
|
+
# 4. AI Documentation & Anti-Drift Guard
|
|
165
|
+
doc_affected = any(f.endswith(".md") or ".agents" in f or "tools/" in f or "Keywords/" in f or "Test Suites/" in f for f in staged_files)
|
|
166
|
+
if doc_affected:
|
|
167
|
+
linter_script = os.path.join(os.path.dirname(__file__), "lint_ai_docs.py")
|
|
168
|
+
if os.path.exists(linter_script):
|
|
169
|
+
res = subprocess.run([sys.executable, linter_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
170
|
+
if res.returncode != 0:
|
|
171
|
+
print("ā [PRE-COMMIT BLOCKED] AI Documentation Linter detected drift:")
|
|
172
|
+
print(res.stdout.strip())
|
|
173
|
+
errors_found = True
|
|
174
|
+
|
|
175
|
+
# 5. Keywords/ change -> Auto-regenerate Repo Map
|
|
176
|
+
keywords_affected = any(f.startswith("Keywords/") and f.endswith(".groovy") for f in staged_files)
|
|
177
|
+
if keywords_affected:
|
|
178
|
+
repo_map_script = os.path.join(os.path.dirname(__file__), "repo_map.py")
|
|
179
|
+
if os.path.exists(repo_map_script):
|
|
180
|
+
subprocess.run([sys.executable, repo_map_script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
181
|
+
# Stage updated repo_map files
|
|
182
|
+
subprocess.run(["git", "add", "tools/repo_map.txt", "tools/repo_map.json"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
183
|
+
|
|
184
|
+
# 6. Scripts/ change -> Static Defect Linter (Meta Infer-Style)
|
|
185
|
+
scripts_affected = any(f.startswith("Scripts/") and f.endswith(".groovy") for f in staged_files)
|
|
186
|
+
if scripts_affected:
|
|
187
|
+
infer_script = os.path.join(os.path.dirname(__file__), "infer_linter.py")
|
|
188
|
+
if os.path.exists(infer_script):
|
|
189
|
+
res = subprocess.run([sys.executable, infer_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
190
|
+
if res.returncode != 0:
|
|
191
|
+
print("ā [PRE-COMMIT BLOCKED] Static Defect Sentinel detected critical issues in staged scripts:")
|
|
192
|
+
print(res.stdout.strip())
|
|
193
|
+
errors_found = True
|
|
194
|
+
|
|
195
|
+
if errors_found:
|
|
196
|
+
print("\nš« Commit aborted. Please fix violations before committing.")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
else:
|
|
199
|
+
print("ā
[PRE-COMMIT GUARD] All quality gates passed (No BOM, Clean POM, Clean Paths, Docs Aligned, Static Lint Pass).")
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|