synapse-hub 2.0.0 ā 2.1.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/rules/git-push-protocol.md +1 -1
- package/.agents/rules/lessons-learned.md +1 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +8 -5
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +13 -0
- package/README.md +172 -29
- package/package.json +9 -5
- package/skills_manifest.json +260 -260
- package/tools/TOOLS_RUNBOOK.md +54 -3
- package/tools/api_fuzzer.py +468 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +211 -0
- package/tools/cache_resolver.py +128 -0
- package/tools/cli.py +41 -1
- package/tools/export_postman_collection.py +606 -0
- package/tools/flaky_detector.py +180 -14
- package/tools/generate_jmx_plan.py +486 -0
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +56 -0
- package/tools/network_mock_shield.py +141 -0
- package/tools/repo_map.json +1 -1
- package/tools/repo_map.txt +1 -1
- package/tools/visual_som_helper.py +128 -9
|
@@ -109,6 +109,119 @@ class NetworkMockShield:
|
|
|
109
109
|
delay_opt = f", delay: {delay_ms}" if delay_ms > 0 else ""
|
|
110
110
|
return f"cy.intercept('{method}', '{url_pattern}', {{ statusCode: {status}, body: {body_json}{delay_opt} }}).as('mock_{url_pattern.split('/')[-1]}');"
|
|
111
111
|
|
|
112
|
+
@classmethod
|
|
113
|
+
def generate_playwright_har_harness(cls, har_path: str, mode: str = "replay", url_glob: str = "**/*", lang: str = "ts") -> str:
|
|
114
|
+
"""
|
|
115
|
+
Generates Playwright Native Zero-Latency HAR Record & Replay harness.
|
|
116
|
+
- record: Dumps real network traffic to HAR file.
|
|
117
|
+
- replay: Intercepts all matching requests and returns responses from HAR with 0ms delay.
|
|
118
|
+
"""
|
|
119
|
+
if mode == "record":
|
|
120
|
+
if lang == "python":
|
|
121
|
+
return "\n".join([
|
|
122
|
+
f"# --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright Python) ---",
|
|
123
|
+
f"# Dumps live network traffic to '{har_path}'",
|
|
124
|
+
f"context = browser.new_context(",
|
|
125
|
+
f" record_har_path='{har_path}',",
|
|
126
|
+
f" record_har_mode='minimal',",
|
|
127
|
+
f" record_har_url_filter='{url_glob}'",
|
|
128
|
+
f")",
|
|
129
|
+
f"page = context.new_page()",
|
|
130
|
+
f"# Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
|
|
131
|
+
])
|
|
132
|
+
else:
|
|
133
|
+
return "\n".join([
|
|
134
|
+
f"// --- ZERO-LATENCY HAR RECORDING HARNESS (Playwright TS) ---",
|
|
135
|
+
f"// Dumps live network traffic to '{har_path}'",
|
|
136
|
+
f"const context = await browser.newContext({{",
|
|
137
|
+
f" recordHar: {{",
|
|
138
|
+
f" path: '{har_path}',",
|
|
139
|
+
f" mode: 'minimal',",
|
|
140
|
+
f" urlFilter: '{url_glob}'",
|
|
141
|
+
f" }}",
|
|
142
|
+
f"}});",
|
|
143
|
+
f"const page = await context.newPage();",
|
|
144
|
+
f"// Note: Run 'python tools/network_mock_shield.py --har-sanitize {har_path}' before committing to git!"
|
|
145
|
+
])
|
|
146
|
+
else:
|
|
147
|
+
# Replay Mode
|
|
148
|
+
if lang == "python":
|
|
149
|
+
return "\n".join([
|
|
150
|
+
f"# --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright Python) ---",
|
|
151
|
+
f"# Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
|
|
152
|
+
f"page.route_from_har(",
|
|
153
|
+
f" har_id='{har_path}',",
|
|
154
|
+
f" url='{url_glob}',",
|
|
155
|
+
f" not_found='fallback',",
|
|
156
|
+
f" update=False",
|
|
157
|
+
f")"
|
|
158
|
+
])
|
|
159
|
+
else:
|
|
160
|
+
return "\n".join([
|
|
161
|
+
f"// --- ZERO-LATENCY HAR REPLAY HARNESS (Playwright TS) ---",
|
|
162
|
+
f"// Replays 100% network responses from local HAR with 0ms delay (Immune to backend downtime)",
|
|
163
|
+
f"await page.routeFromHAR('{har_path}', {{",
|
|
164
|
+
f" url: '{url_glob}',",
|
|
165
|
+
f" notFound: 'fallback',",
|
|
166
|
+
f" update: false",
|
|
167
|
+
f"}});"
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def sanitize_har_file(cls, input_har_path: str, output_har_path: Optional[str] = None) -> int:
|
|
172
|
+
"""
|
|
173
|
+
Parses a recorded HAR file and redacts sensitive PII & credentials:
|
|
174
|
+
Bearer tokens, Authorization headers, Cookie headers, passwords, API keys.
|
|
175
|
+
Returns the count of redacted entries.
|
|
176
|
+
"""
|
|
177
|
+
import re
|
|
178
|
+
if not os.path.exists(input_har_path):
|
|
179
|
+
raise FileNotFoundError(f"HAR file not found: {input_har_path}")
|
|
180
|
+
|
|
181
|
+
with open(input_har_path, "r", encoding="utf-8", errors="replace") as fp:
|
|
182
|
+
data = json.load(fp)
|
|
183
|
+
|
|
184
|
+
entries = data.get("log", {}).get("entries", [])
|
|
185
|
+
redacted_count = 0
|
|
186
|
+
|
|
187
|
+
pii_patterns = [
|
|
188
|
+
(re.compile(r'"(password|pass|secret|token|api_?key|access_?token)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "REDACTED_SECRET"'),
|
|
189
|
+
(re.compile(r'"(credit_?card|card_?number|pan)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "411111******1111"'),
|
|
190
|
+
(re.compile(r'"(cvv|cvc)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "999"'),
|
|
191
|
+
(re.compile(r'"(email)"\s*:\s*"[^"]+"', re.IGNORECASE), r'"\1": "auto_user@enterprise.local"')
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for entry in entries:
|
|
195
|
+
req = entry.get("request", {})
|
|
196
|
+
# 1. Scrub Request Headers
|
|
197
|
+
for header in req.get("headers", []):
|
|
198
|
+
h_name = header.get("name", "").lower()
|
|
199
|
+
if h_name in ("authorization", "cookie", "x-api-key", "token", "proxy-authorization"):
|
|
200
|
+
header["value"] = "Bearer REDACTED_TOKEN" if "bearer" in header.get("value", "").lower() else "REDACTED"
|
|
201
|
+
redacted_count += 1
|
|
202
|
+
|
|
203
|
+
# 2. Scrub Request Post Data
|
|
204
|
+
post_data = req.get("postData", {})
|
|
205
|
+
if "text" in post_data:
|
|
206
|
+
txt = post_data["text"]
|
|
207
|
+
for pat, rep in pii_patterns:
|
|
208
|
+
txt = pat.sub(rep, txt)
|
|
209
|
+
post_data["text"] = txt
|
|
210
|
+
|
|
211
|
+
# 3. Scrub Response Headers (Set-Cookie)
|
|
212
|
+
resp = entry.get("response", {})
|
|
213
|
+
for header in resp.get("headers", []):
|
|
214
|
+
h_name = header.get("name", "").lower()
|
|
215
|
+
if h_name in ("set-cookie", "authorization"):
|
|
216
|
+
header["value"] = "REDACTED_COOKIE"
|
|
217
|
+
redacted_count += 1
|
|
218
|
+
|
|
219
|
+
out_path = output_har_path or input_har_path
|
|
220
|
+
with open(out_path, "w", encoding="utf-8") as fp:
|
|
221
|
+
json.dump(data, fp, indent=2)
|
|
222
|
+
|
|
223
|
+
return redacted_count
|
|
224
|
+
|
|
112
225
|
@classmethod
|
|
113
226
|
def get_preset(cls, preset_name: str) -> Dict[str, Any]:
|
|
114
227
|
"""Returns preset details or raises ValueError."""
|
|
@@ -127,8 +240,36 @@ def main():
|
|
|
127
240
|
parser.add_argument("--lang", choices=["ts", "python", "cypress"], default="ts", help="Target script language")
|
|
128
241
|
parser.add_argument("--list-presets", action="store_true", help="List all available presets")
|
|
129
242
|
|
|
243
|
+
# HAR Record & Replay options
|
|
244
|
+
parser.add_argument("--har-mode", choices=["record", "replay"], default=None, help="Generate Playwright HAR record or replay harness")
|
|
245
|
+
parser.add_argument("--har-path", default="fixtures/network_trace.har", help="Path to HAR archive file")
|
|
246
|
+
parser.add_argument("--har-sanitize", default=None, help="Sanitize PII, tokens, and cookies from recorded HAR file")
|
|
247
|
+
parser.add_argument("--har-out", default=None, help="Optional output path for sanitized HAR")
|
|
248
|
+
|
|
130
249
|
args = parser.parse_args()
|
|
131
250
|
|
|
251
|
+
# Handle HAR Sanitization
|
|
252
|
+
if args.har_sanitize:
|
|
253
|
+
redacted = NetworkMockShield.sanitize_har_file(args.har_sanitize, output_har_path=args.har_out)
|
|
254
|
+
target = args.har_out or args.har_sanitize
|
|
255
|
+
print(f"ā
[HAR SANITIZED] Scrubbed {redacted} sensitive headers/tokens in: {target}")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Handle HAR Harness Generation
|
|
259
|
+
if args.har_mode:
|
|
260
|
+
code = NetworkMockShield.generate_playwright_har_harness(
|
|
261
|
+
har_path=args.har_path,
|
|
262
|
+
mode=args.har_mode,
|
|
263
|
+
url_glob=args.url,
|
|
264
|
+
lang=args.lang
|
|
265
|
+
)
|
|
266
|
+
print("======================================================================")
|
|
267
|
+
print(f" PLAYWRIGHT ZERO-LATENCY HAR {args.har_mode.upper()} HARNESS ({args.lang.upper()})")
|
|
268
|
+
print("======================================================================")
|
|
269
|
+
print(code)
|
|
270
|
+
print("======================================================================")
|
|
271
|
+
return
|
|
272
|
+
|
|
132
273
|
if args.list_presets:
|
|
133
274
|
print("======================================================================")
|
|
134
275
|
print(" AVAILABLE NETWORK MOCK & CHAOS FAULT PRESETS")
|
package/tools/repo_map.json
CHANGED
package/tools/repo_map.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
2
|
# ENTERPRISE AUTOMATION REPOSITORY MAP (Aider-Style AST Compression)
|
|
3
|
-
# Generated : 2026-10-29
|
|
3
|
+
# Generated : 2026-10-29 21:59:04
|
|
4
4
|
# Target : /Users/phungcao/Documents/Agent Skill
|
|
5
5
|
# Scope : 0 Classes (0 source lines, 0.0 KB)
|
|
6
6
|
# Purpose : Token-budgeted index (< 600 tokens) for instant method lookup
|
|
@@ -26,7 +26,15 @@ import json
|
|
|
26
26
|
import sqlite3
|
|
27
27
|
import argparse
|
|
28
28
|
from typing import Dict, Any, List, Optional, Tuple, Union
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
31
|
+
HAS_PIL = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
Image = None
|
|
34
|
+
ImageDraw = None
|
|
35
|
+
ImageFont = None
|
|
36
|
+
ImageFilter = None
|
|
37
|
+
HAS_PIL = False
|
|
30
38
|
|
|
31
39
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
32
40
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -222,6 +230,89 @@ class VisualSoMHelper:
|
|
|
222
230
|
|
|
223
231
|
return visual_hash
|
|
224
232
|
|
|
233
|
+
def detect_canvas_interactive_regions(
|
|
234
|
+
self,
|
|
235
|
+
image_path: str,
|
|
236
|
+
stride: int = 16,
|
|
237
|
+
edge_threshold: int = 28,
|
|
238
|
+
min_pixels_per_cell: int = 6,
|
|
239
|
+
min_w: int = 16,
|
|
240
|
+
min_h: int = 16,
|
|
241
|
+
max_ratio: float = 0.88
|
|
242
|
+
) -> List[Dict[str, Any]]:
|
|
243
|
+
"""
|
|
244
|
+
Standalone Computer Vision Contour / Active Blob Detector for Canvas/GIS.
|
|
245
|
+
Detects interactive visual components (buttons, polygon nodes, markers, sliders, chart icons)
|
|
246
|
+
without requiring DOM elements or accessibility nodes.
|
|
247
|
+
Uses pure PIL edge filtering and connected-grid component clustering (Zero-Dependency).
|
|
248
|
+
"""
|
|
249
|
+
if not os.path.exists(image_path) or not HAS_PIL:
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
img = Image.open(image_path).convert("L")
|
|
253
|
+
w, h = img.size
|
|
254
|
+
max_allowed_w = int(w * max_ratio)
|
|
255
|
+
max_allowed_h = int(h * max_ratio)
|
|
256
|
+
|
|
257
|
+
# Detect high-contrast edges & contours
|
|
258
|
+
edges = img.filter(ImageFilter.FIND_EDGES)
|
|
259
|
+
edges = edges.point(lambda p: 255 if p > edge_threshold else 0)
|
|
260
|
+
pixels = edges.load()
|
|
261
|
+
|
|
262
|
+
# Identify active cells with high edge density
|
|
263
|
+
active_cells = set()
|
|
264
|
+
for y in range(0, h, stride):
|
|
265
|
+
for x in range(0, w, stride):
|
|
266
|
+
count = 0
|
|
267
|
+
for cy in range(y, min(h, y + stride)):
|
|
268
|
+
for cx in range(x, min(w, x + stride)):
|
|
269
|
+
if pixels[cx, cy] > 0:
|
|
270
|
+
count += 1
|
|
271
|
+
if count >= min_pixels_per_cell:
|
|
272
|
+
active_cells.add((x // stride, y // stride))
|
|
273
|
+
|
|
274
|
+
# 8-direction BFS clustering to group connected components
|
|
275
|
+
visited = set()
|
|
276
|
+
detected_elements = []
|
|
277
|
+
cluster_id = 1
|
|
278
|
+
|
|
279
|
+
for cell in sorted(active_cells):
|
|
280
|
+
if cell in visited:
|
|
281
|
+
continue
|
|
282
|
+
queue = [cell]
|
|
283
|
+
visited.add(cell)
|
|
284
|
+
cluster = [cell]
|
|
285
|
+
|
|
286
|
+
while queue:
|
|
287
|
+
curr = queue.pop(0)
|
|
288
|
+
cx, cy = curr
|
|
289
|
+
for dx in (-1, 0, 1):
|
|
290
|
+
for dy in (-1, 0, 1):
|
|
291
|
+
nb = (cx + dx, cy + dy)
|
|
292
|
+
if nb in active_cells and nb not in visited:
|
|
293
|
+
visited.add(nb)
|
|
294
|
+
queue.append(nb)
|
|
295
|
+
cluster.append(nb)
|
|
296
|
+
|
|
297
|
+
min_x = max(0, min(c[0] for c in cluster) * stride - 4)
|
|
298
|
+
min_y = max(0, min(c[1] for c in cluster) * stride - 4)
|
|
299
|
+
box_w = min(w - min_x, (max(c[0] for c in cluster) + 1) * stride - min_x + 8)
|
|
300
|
+
box_h = min(h - min_y, (max(c[1] for c in cluster) + 1) * stride - min_y + 8)
|
|
301
|
+
|
|
302
|
+
# Filter out whole-canvas background borders or tiny noise
|
|
303
|
+
if box_w >= min_w and box_h >= min_h and (box_w < max_allowed_w or box_h < max_allowed_h):
|
|
304
|
+
detected_elements.append({
|
|
305
|
+
"x": min_x,
|
|
306
|
+
"y": min_y,
|
|
307
|
+
"width": box_w,
|
|
308
|
+
"height": box_h,
|
|
309
|
+
"label": f"Canvas_Target_{cluster_id}",
|
|
310
|
+
"tag": "canvas_interactive_node"
|
|
311
|
+
})
|
|
312
|
+
cluster_id += 1
|
|
313
|
+
|
|
314
|
+
return detected_elements
|
|
315
|
+
|
|
225
316
|
def generate_som_overlay(
|
|
226
317
|
self,
|
|
227
318
|
image_path: str,
|
|
@@ -230,22 +321,27 @@ class VisualSoMHelper:
|
|
|
230
321
|
) -> Tuple[str, Dict[int, Dict[str, Any]]]:
|
|
231
322
|
"""
|
|
232
323
|
Draws high-contrast numbered badges (Set-of-Marks) over each element bounding box.
|
|
324
|
+
If elements is omitted, autonomously runs detect_canvas_interactive_regions.
|
|
233
325
|
Returns (marked_image_path, marks_map).
|
|
234
326
|
"""
|
|
327
|
+
if not os.path.exists(image_path):
|
|
328
|
+
img = Image.new("RGB", (self.viewport_width, self.viewport_height), color="#1E1E2E")
|
|
329
|
+
draw = ImageDraw.Draw(img)
|
|
330
|
+
draw.text((50, 50), "Mock Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
|
|
331
|
+
draw.rectangle([100, 150, 220, 190], outline="#00CCFF", width=2)
|
|
332
|
+
draw.rectangle([240, 150, 360, 190], outline="#FF0055", width=2)
|
|
333
|
+
draw.rectangle([500, 300, 700, 350], outline="#00FF66", width=2)
|
|
334
|
+
img.save(image_path)
|
|
335
|
+
|
|
235
336
|
if not elements:
|
|
236
|
-
|
|
337
|
+
detected = self.detect_canvas_interactive_regions(image_path)
|
|
338
|
+
elements = detected if detected else [
|
|
237
339
|
{"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
|
|
238
340
|
{"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
|
|
239
341
|
{"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},
|
|
240
342
|
{"x": 720, "y": 300, "width": 150, "height": 50, "label": "Cancel Order", "tag": "shadow_button"}
|
|
241
343
|
]
|
|
242
344
|
|
|
243
|
-
if not os.path.exists(image_path):
|
|
244
|
-
img = Image.new("RGB", (self.viewport_width, self.viewport_height), color="#1E1E2E")
|
|
245
|
-
draw = ImageDraw.Draw(img)
|
|
246
|
-
draw.text((50, 50), "Mock Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
|
|
247
|
-
img.save(image_path)
|
|
248
|
-
|
|
249
345
|
img = Image.open(image_path).convert("RGBA")
|
|
250
346
|
overlay = Image.new("RGBA", img.size, (255, 255, 255, 0))
|
|
251
347
|
draw = ImageDraw.Draw(overlay)
|
|
@@ -335,6 +431,7 @@ def main():
|
|
|
335
431
|
parser.add_argument("--cache-lookup", action="store_true", help="Attempt zero-token visual cache resolution first")
|
|
336
432
|
parser.add_argument("--cache-save", action="store_true", help="Save resolved coordinates to visual cache")
|
|
337
433
|
parser.add_argument("--threshold", type=float, default=0.98, help="Visual similarity threshold for cache hit (default: 0.98)")
|
|
434
|
+
parser.add_argument("--detect-only", action="store_true", help="Run pure contour/blob detector and print detected bounding boxes")
|
|
338
435
|
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
339
436
|
args = parser.parse_args()
|
|
340
437
|
|
|
@@ -357,6 +454,27 @@ def main():
|
|
|
357
454
|
print(f" Action : {cached_result['playwright_action']}\n")
|
|
358
455
|
return 0
|
|
359
456
|
|
|
457
|
+
# Auto-generate mock canvas if image doesn't exist
|
|
458
|
+
if not os.path.exists(args.image):
|
|
459
|
+
img = Image.new("RGB", (helper.viewport_width, helper.viewport_height), color="#1E1E2E")
|
|
460
|
+
draw = ImageDraw.Draw(img)
|
|
461
|
+
draw.text((50, 50), "Mock GIS & Financial Dashboard (HTML5 Canvas)", fill="#FFFFFF")
|
|
462
|
+
draw.rectangle([100, 150, 220, 190], fill="#00CCFF", outline="#FFFFFF", width=2)
|
|
463
|
+
draw.rectangle([240, 150, 360, 190], fill="#FF0055", outline="#FFFFFF", width=2)
|
|
464
|
+
draw.rectangle([500, 300, 700, 350], fill="#00FF66", outline="#FFFFFF", width=2)
|
|
465
|
+
img.save(args.image)
|
|
466
|
+
|
|
467
|
+
# Detect Only
|
|
468
|
+
if args.detect_only:
|
|
469
|
+
blobs = helper.detect_canvas_interactive_regions(args.image)
|
|
470
|
+
if args.json:
|
|
471
|
+
print(json.dumps(blobs, indent=2))
|
|
472
|
+
else:
|
|
473
|
+
print(f"\nš [CANVAS CONTOURS DETECTED] Found {len(blobs)} interactive regions:")
|
|
474
|
+
for b in blobs:
|
|
475
|
+
print(f" ⢠{b['label']}: Bounds {b['x']},{b['y']} ({b['width']}x{b['height']} px)")
|
|
476
|
+
return 0
|
|
477
|
+
|
|
360
478
|
# Load elements
|
|
361
479
|
elements = []
|
|
362
480
|
if args.elements_json:
|
|
@@ -370,7 +488,8 @@ def main():
|
|
|
370
488
|
pass
|
|
371
489
|
|
|
372
490
|
if not elements:
|
|
373
|
-
|
|
491
|
+
detected = helper.detect_canvas_interactive_regions(args.image)
|
|
492
|
+
elements = detected if detected else [
|
|
374
493
|
{"x": 100, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom In", "tag": "canvas_control"},
|
|
375
494
|
{"x": 240, "y": 150, "width": 120, "height": 40, "label": "TradingView Zoom Out", "tag": "canvas_control"},
|
|
376
495
|
{"x": 500, "y": 300, "width": 200, "height": 50, "label": "Execute Trade Order", "tag": "shadow_button"},
|