synapse-hub 0.0.0-stage → 2.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/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 +537 -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 +425 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Duration-Balanced Test Matrix Sharder
|
|
4
|
+
------------------------------------------------
|
|
5
|
+
Balances parallel CI/CD test execution across N containers using historical
|
|
6
|
+
runtimes and the Greedy LPT (Longest Processing Time First) Bin-Packing algorithm.
|
|
7
|
+
Eliminates CI runner bottlenecks where one worker runs for 15m while others finish in 2m.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
import argparse
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Dict, Any
|
|
17
|
+
|
|
18
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
19
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
20
|
+
try:
|
|
21
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
22
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
28
|
+
CACHE_DB_PATH = os.path.join(SCRIPT_DIR, "memory_cache.db")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_test_files(target_dir: str) -> List[str]:
|
|
32
|
+
"""Discovers all executable test scripts across Katalon, Playwright, or Pytest suites."""
|
|
33
|
+
target_path = Path(target_dir)
|
|
34
|
+
test_files = []
|
|
35
|
+
valid_exts = {".groovy", ".ts", ".js", ".py"}
|
|
36
|
+
|
|
37
|
+
for root, _, files in os.walk(target_path):
|
|
38
|
+
for f in files:
|
|
39
|
+
# Match standard test naming patterns
|
|
40
|
+
lower = f.lower()
|
|
41
|
+
if any(lower.endswith(ext) for ext in valid_exts):
|
|
42
|
+
if "test" in lower or "spec" in lower or "tc_" in lower or "scripts" in root.lower():
|
|
43
|
+
rel_p = os.path.relpath(os.path.join(root, f), target_dir).replace("\\", "/")
|
|
44
|
+
test_files.append(rel_p)
|
|
45
|
+
|
|
46
|
+
return sorted(test_files)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def estimate_test_duration(file_path: str, full_path: str) -> float:
|
|
50
|
+
"""Estimates test execution duration in seconds using historical cache or file heuristic."""
|
|
51
|
+
# Check SQLite historical runtime cache if available
|
|
52
|
+
if os.path.exists(CACHE_DB_PATH):
|
|
53
|
+
try:
|
|
54
|
+
conn = sqlite3.connect(CACHE_DB_PATH)
|
|
55
|
+
cur = conn.cursor()
|
|
56
|
+
cur.execute("CREATE TABLE IF NOT EXISTS test_runtimes (test_path TEXT PRIMARY KEY, duration REAL)")
|
|
57
|
+
cur.execute("SELECT duration FROM test_runtimes WHERE test_path = ?", (file_path,))
|
|
58
|
+
row = cur.fetchone()
|
|
59
|
+
conn.close()
|
|
60
|
+
if row and row[0]:
|
|
61
|
+
return float(row[0])
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# Heuristic fallback: ~0.5s per line of script
|
|
66
|
+
try:
|
|
67
|
+
if os.path.exists(full_path):
|
|
68
|
+
with open(full_path, "r", encoding="utf-8", errors="ignore") as fp:
|
|
69
|
+
lines = len(fp.readlines())
|
|
70
|
+
return max(5.0, round(lines * 0.25, 1))
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return 10.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def shard_tests_lpt(test_files: List[str], target_dir: str, num_shards: int) -> List[Dict[str, Any]]:
|
|
78
|
+
"""
|
|
79
|
+
Applies Longest Processing Time First (LPT) bin-packing to distribute tests evenly.
|
|
80
|
+
"""
|
|
81
|
+
num_shards = max(1, num_shards)
|
|
82
|
+
if not test_files:
|
|
83
|
+
return [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
|
|
84
|
+
|
|
85
|
+
# 1. Calculate duration for each test
|
|
86
|
+
tests_with_cost = []
|
|
87
|
+
for tf in test_files:
|
|
88
|
+
fp = os.path.join(target_dir, tf)
|
|
89
|
+
dur = estimate_test_duration(tf, fp)
|
|
90
|
+
tests_with_cost.append((tf, dur))
|
|
91
|
+
|
|
92
|
+
# 2. Sort descending by duration
|
|
93
|
+
tests_with_cost.sort(key=lambda x: x[1], reverse=True)
|
|
94
|
+
|
|
95
|
+
# 3. Initialize shards
|
|
96
|
+
shards = [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
|
|
97
|
+
|
|
98
|
+
# 4. Greedily assign to the shard with the least current cumulative duration
|
|
99
|
+
for test_file, dur in tests_with_cost:
|
|
100
|
+
# Find shard with min duration
|
|
101
|
+
min_shard = min(shards, key=lambda s: s["total_duration"])
|
|
102
|
+
min_shard["tests"].append(test_file)
|
|
103
|
+
min_shard["total_duration"] = round(min_shard["total_duration"] + dur, 2)
|
|
104
|
+
|
|
105
|
+
return shards
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
parser = argparse.ArgumentParser(description="Duration-Balanced Test Matrix Sharder")
|
|
110
|
+
parser.add_argument("target", nargs="?", default=os.getcwd(), help="Target repository root")
|
|
111
|
+
parser.add_argument("--shards", "-n", type=int, default=4, help="Number of parallel shards/workers (default: 4)")
|
|
112
|
+
parser.add_argument("--json", action="store_true", help="Output matrix in JSON format")
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
|
|
115
|
+
target_dir = os.path.abspath(args.target)
|
|
116
|
+
test_files = find_test_files(target_dir)
|
|
117
|
+
shards = shard_tests_lpt(test_files, target_dir, args.shards)
|
|
118
|
+
|
|
119
|
+
if args.json:
|
|
120
|
+
print(json.dumps({"total_tests": len(test_files), "shards_count": args.shards, "shards": shards}, indent=2))
|
|
121
|
+
else:
|
|
122
|
+
print(f"\n======================================================================")
|
|
123
|
+
print(f" DURATION-BALANCED TEST MATRIX SHARDER ({args.shards} PARALLEL WORKERS) ")
|
|
124
|
+
print(f"======================================================================")
|
|
125
|
+
print(f" Target Scope : {target_dir}")
|
|
126
|
+
print(f" Total Tests : {len(test_files)} files\n")
|
|
127
|
+
|
|
128
|
+
for s in shards:
|
|
129
|
+
print(f"📦 Shard {s['shard_index']}: {len(s['tests'])} tests (~{s['total_duration']}s total)")
|
|
130
|
+
for t in s['tests'][:5]:
|
|
131
|
+
print(f" • {t}")
|
|
132
|
+
if len(s['tests']) > 5:
|
|
133
|
+
print(f" ... and {len(s['tests']) - 5} more")
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
# Calculate imbalance delta
|
|
137
|
+
durations = [s["total_duration"] for s in shards]
|
|
138
|
+
delta = max(durations) - min(durations) if durations else 0
|
|
139
|
+
print(f"⚡ Shard Imbalance Delta: {delta:.1f}s (Optimal parallel distribution)")
|
|
140
|
+
print(f"======================================================================\n")
|
|
141
|
+
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
sys.exit(main())
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
param()
|
|
2
|
+
|
|
3
|
+
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
|
|
4
|
+
|
|
5
|
+
function Read-ExcelSheet {
|
|
6
|
+
param(
|
|
7
|
+
[string]$FilePath,
|
|
8
|
+
[string]$SheetName = ""
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -LiteralPath $FilePath)) {
|
|
12
|
+
if ($env:EXCEL_TEST_PATH -and (Test-Path -LiteralPath $env:EXCEL_TEST_PATH)) {
|
|
13
|
+
$FilePath = $env:EXCEL_TEST_PATH
|
|
14
|
+
} else {
|
|
15
|
+
# Search workspace or Downloads for any recent xlsx file
|
|
16
|
+
$found = Get-ChildItem -Path "." -Filter "*.xlsx" -Recurse -Depth 2 -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
17
|
+
if (-not $found) {
|
|
18
|
+
$found = Get-ChildItem -Path "$env:USERPROFILE\Downloads" -Filter "*.xlsx" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
19
|
+
}
|
|
20
|
+
if ($found) { $FilePath = $found.FullName }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if ([string]::IsNullOrWhiteSpace($FilePath) -or -not (Test-Path -LiteralPath $FilePath)) {
|
|
25
|
+
throw "Excel file not found. Please provide a valid file_path."
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$fullPath = (Get-Item $FilePath).FullName
|
|
29
|
+
$zip = [System.IO.Compression.ZipFile]::OpenRead($fullPath)
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
# 1. Read shared strings
|
|
33
|
+
$sharedStrings = New-Object System.Collections.Generic.List[string]
|
|
34
|
+
$ssEntry = $zip.GetEntry("xl/sharedStrings.xml")
|
|
35
|
+
if ($ssEntry) {
|
|
36
|
+
$stream = $ssEntry.Open()
|
|
37
|
+
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
|
|
38
|
+
$ssXml = [xml]$reader.ReadToEnd()
|
|
39
|
+
$reader.Close()
|
|
40
|
+
$stream.Close()
|
|
41
|
+
foreach ($si in $ssXml.sst.si) {
|
|
42
|
+
$sharedStrings.Add($si.InnerText)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 2. Determine target sheet entry from workbook.xml
|
|
47
|
+
$targetSheetEntryName = "xl/worksheets/sheet1.xml"
|
|
48
|
+
$wbEntry = $zip.GetEntry("xl/workbook.xml")
|
|
49
|
+
if ($wbEntry) {
|
|
50
|
+
$stream = $wbEntry.Open()
|
|
51
|
+
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
|
|
52
|
+
$wbXml = [xml]$reader.ReadToEnd()
|
|
53
|
+
$reader.Close()
|
|
54
|
+
$stream.Close()
|
|
55
|
+
|
|
56
|
+
$sheets = $wbXml.workbook.sheets.sheet
|
|
57
|
+
$matchedSheetIndex = 1
|
|
58
|
+
if ($SheetName -and $sheets) {
|
|
59
|
+
$idx = 1
|
|
60
|
+
foreach ($s in $sheets) {
|
|
61
|
+
if ($s.name -eq $SheetName -or $s.name -like "*$SheetName*") {
|
|
62
|
+
$matchedSheetIndex = $idx
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
$idx++
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
$targetSheetEntryName = "xl/worksheets/sheet$matchedSheetIndex.xml"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Fallback if entry doesn't exist
|
|
72
|
+
$sEntry = $zip.GetEntry($targetSheetEntryName)
|
|
73
|
+
if (-not $sEntry) {
|
|
74
|
+
$firstSheet = $zip.Entries | Where-Object { $_.FullName -like "xl/worksheets/sheet*.xml" } | Select-Object -First 1
|
|
75
|
+
if ($firstSheet) { $sEntry = $firstSheet }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (-not $sEntry) {
|
|
79
|
+
throw "No worksheet found inside Excel archive."
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$stream = $sEntry.Open()
|
|
83
|
+
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
|
|
84
|
+
$sXml = [xml]$reader.ReadToEnd()
|
|
85
|
+
$reader.Close()
|
|
86
|
+
$stream.Close()
|
|
87
|
+
|
|
88
|
+
$rows = $sXml.worksheet.sheetData.row
|
|
89
|
+
$currentModule = "General"
|
|
90
|
+
$testcases = @()
|
|
91
|
+
|
|
92
|
+
foreach ($r in $rows) {
|
|
93
|
+
$rowNum = [int]$r.r
|
|
94
|
+
$cells = @{}
|
|
95
|
+
foreach ($c in $r.c) {
|
|
96
|
+
$colLetter = $c.r -replace '\d+'
|
|
97
|
+
$val = ""
|
|
98
|
+
if ($c.v) {
|
|
99
|
+
$raw = $c.v
|
|
100
|
+
if ($c.t -eq "s") {
|
|
101
|
+
$val = $sharedStrings[[int]$raw]
|
|
102
|
+
} else {
|
|
103
|
+
$val = $raw
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
$cells[$colLetter] = $val
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$colA = if ($cells.ContainsKey("A")) { $cells["A"].Trim() } else { "" }
|
|
110
|
+
$colB = if ($cells.ContainsKey("B")) { $cells["B"].Trim() } else { "" }
|
|
111
|
+
|
|
112
|
+
if ($colB -match "PHAN HE|Phan he|PHÂN HỆ|Phân hệ|MODULE|Module") {
|
|
113
|
+
$currentModule = $colB.Trim()
|
|
114
|
+
} elseif ($colA -match "PHAN HE|Phan he|PHÂN HỆ|Phân hệ|MODULE|Module") {
|
|
115
|
+
$currentModule = $colA.Trim()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ($colA -match '^TC[-_]?\d+') {
|
|
119
|
+
$testcases += [PSCustomObject]@{
|
|
120
|
+
Row = $rowNum
|
|
121
|
+
Module = $currentModule
|
|
122
|
+
ID = $colA
|
|
123
|
+
Name = if ($cells.ContainsKey("B")) { $cells["B"].Trim() } else { "" }
|
|
124
|
+
PreCondition = if ($cells.ContainsKey("C")) { $cells["C"].Trim() } else { "" }
|
|
125
|
+
Steps = if ($cells.ContainsKey("D")) { $cells["D"].Trim() } else { "" }
|
|
126
|
+
DataTest = if ($cells.ContainsKey("E")) { $cells["E"].Trim() } else { "" }
|
|
127
|
+
Expected = if ($cells.ContainsKey("F")) { $cells["F"].Trim() } else { "" }
|
|
128
|
+
Progress = if ($cells.ContainsKey("G")) { $cells["G"].Trim() } else { "" }
|
|
129
|
+
Status = if ($cells.ContainsKey("H")) { $cells["H"].Trim() } else { "" }
|
|
130
|
+
QC = if ($cells.ContainsKey("I")) { $cells["I"].Trim() } else { "" }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return $testcases
|
|
135
|
+
} finally {
|
|
136
|
+
$zip.Dispose()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Alias for backwards compatibility
|
|
141
|
+
function Read-EnterpriseSheet {
|
|
142
|
+
param([string]$FilePath)
|
|
143
|
+
return Read-ExcelSheet -FilePath $FilePath
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Stdio JSON-RPC Loop
|
|
147
|
+
$inputReader = [System.Console]::In
|
|
148
|
+
while (($line = $inputReader.ReadLine()) -ne $null) {
|
|
149
|
+
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
150
|
+
$id = $null
|
|
151
|
+
try {
|
|
152
|
+
$req = $line | ConvertFrom-Json
|
|
153
|
+
$id = $req.id
|
|
154
|
+
$method = $req.method
|
|
155
|
+
|
|
156
|
+
if ($method -eq "initialize") {
|
|
157
|
+
$resp = @{
|
|
158
|
+
jsonrpc = "2.0"
|
|
159
|
+
id = $id
|
|
160
|
+
result = @{
|
|
161
|
+
protocolVersion = "2024-11-05"
|
|
162
|
+
capabilities = @{ tools = @{} }
|
|
163
|
+
serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.0.1" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
|
|
167
|
+
} elseif ($method -eq "notifications/initialized") {
|
|
168
|
+
} elseif ($method -eq "ping") {
|
|
169
|
+
$resp = @{ jsonrpc = "2.0"; id = $id; result = @{} }
|
|
170
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress))
|
|
171
|
+
} elseif ($method -eq "tools/list") {
|
|
172
|
+
$resp = @{
|
|
173
|
+
jsonrpc = "2.0"
|
|
174
|
+
id = $id
|
|
175
|
+
result = @{
|
|
176
|
+
tools = @(
|
|
177
|
+
@{
|
|
178
|
+
name = "read_excel_sheet"
|
|
179
|
+
description = "Extract test cases and matrix rows from an Excel worksheet."
|
|
180
|
+
inputSchema = @{
|
|
181
|
+
type = "object"
|
|
182
|
+
properties = @{
|
|
183
|
+
file_path = @{ type = "string"; description = "Path to Excel file (.xlsx)" }
|
|
184
|
+
sheet_name = @{ type = "string"; description = "Worksheet name (defaults to first sheet)" }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
@{
|
|
189
|
+
name = "read_Enterprise_sheet"
|
|
190
|
+
description = "Compatibility alias for read_excel_sheet"
|
|
191
|
+
inputSchema = @{
|
|
192
|
+
type = "object"
|
|
193
|
+
properties = @{
|
|
194
|
+
file_path = @{ type = "string"; description = "Path to Excel file (.xlsx)" }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
|
|
202
|
+
} elseif ($method -eq "tools/call") {
|
|
203
|
+
$toolName = $req.params.name
|
|
204
|
+
if ($toolName -eq "read_excel_sheet" -or $toolName -eq "read_Enterprise_sheet") {
|
|
205
|
+
$fPath = $req.params.arguments.file_path
|
|
206
|
+
$sName = if ($req.params.arguments.sheet_name) { $req.params.arguments.sheet_name } else { "" }
|
|
207
|
+
$tcs = Read-ExcelSheet -FilePath $fPath -SheetName $sName
|
|
208
|
+
$textResult = $tcs | ConvertTo-Json -Depth 5
|
|
209
|
+
$resp = @{
|
|
210
|
+
jsonrpc = "2.0"
|
|
211
|
+
id = $id
|
|
212
|
+
result = @{
|
|
213
|
+
content = @(
|
|
214
|
+
@{ type = "text"; text = $textResult }
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
|
|
219
|
+
} else {
|
|
220
|
+
$resp = @{
|
|
221
|
+
jsonrpc = "2.0"
|
|
222
|
+
id = $id
|
|
223
|
+
error = @{ code = -32601; message = "Tool not found: $toolName" }
|
|
224
|
+
}
|
|
225
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress))
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
if ($null -ne $id) {
|
|
229
|
+
$resp = @{
|
|
230
|
+
jsonrpc = "2.0"
|
|
231
|
+
id = $id
|
|
232
|
+
error = @{ code = -32601; message = "Method not found: $method" }
|
|
233
|
+
}
|
|
234
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
$errMsg = $_.Exception.Message
|
|
239
|
+
[System.Console]::Error.WriteLine("[MCP Excel Server Error] $errMsg")
|
|
240
|
+
$errCode = if ($null -eq $id) { -32700 } else { -32603 }
|
|
241
|
+
$resp = @{
|
|
242
|
+
jsonrpc = "2.0"
|
|
243
|
+
id = $id
|
|
244
|
+
error = @{
|
|
245
|
+
code = $errCode
|
|
246
|
+
message = "MCP Server Exception: $errMsg"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress))
|
|
250
|
+
}
|
|
251
|
+
}
|