synapse-hub 2.6.0 → 2.7.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.
Files changed (71) hide show
  1. package/.agents/agents/failure-analyst.md +4 -0
  2. package/.agents/agents/orchestrator.md +16 -0
  3. package/.agents/agents/pom-crafter.md +8 -0
  4. package/.agents/agents/quality-gatekeeper.md +5 -0
  5. package/.agents/agents/test-runner.md +5 -0
  6. package/.agents/rules/git-push-protocol.md +1 -1
  7. package/.agents/rules/graphify.md +2 -0
  8. package/.agents/rules/mandatory-playwright-live.md +9 -9
  9. package/.agents/rules/mobile_appium_rules.md +8 -1
  10. package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
  11. package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
  12. package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
  13. package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
  14. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
  15. package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
  16. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
  17. package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
  18. package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
  19. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
  20. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
  21. package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
  22. package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
  23. package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
  24. package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
  25. package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
  26. package/.agents/skills/qa-memory-os/SKILL.md +80 -0
  27. package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
  28. package/.agents/skills/review-spec/SKILL.md +60 -0
  29. package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
  30. package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
  31. package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
  32. package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
  33. package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
  34. package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
  35. package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
  36. package/CLAUDE.md +1 -1
  37. package/README.en.md +5 -5
  38. package/README.md +3 -3
  39. package/docs/ARCHITECTURE.md +8 -8
  40. package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
  41. package/package.json +1 -1
  42. package/skills_manifest.json +88 -7
  43. package/synapse.config.json +1 -1
  44. package/templates/ai_bridges/CLAUDE.md +1 -1
  45. package/tools/TOOLS_RUNBOOK.md +1 -1
  46. package/tools/__init__.py +1 -1
  47. package/tools/_openapi_parser.py +158 -0
  48. package/tools/agentless_patcher.py +417 -0
  49. package/tools/analyze_project.py +9 -6
  50. package/tools/auto_heal.py +19 -0
  51. package/tools/autogen_qa_broker.py +318 -0
  52. package/tools/browser_helpers.js +2 -2
  53. package/tools/browsergym_eval.py +237 -0
  54. package/tools/ci_healing_hook.py +1 -1
  55. package/tools/cli.py +3 -3
  56. package/tools/dashboard_generator.py +4 -4
  57. package/tools/db_sandbox.py +4 -0
  58. package/tools/heal_pr_automator.py +25 -0
  59. package/tools/mcp_excel_server.py +1 -1
  60. package/tools/mcp_server.py +1 -1
  61. package/tools/mipro_assertion_tuner.py +207 -0
  62. package/tools/pinpoint_failure.py +12 -0
  63. package/tools/project_state.py +4 -0
  64. package/tools/qa_event_stream.py +336 -0
  65. package/tools/reversible_sandbox.py +4 -0
  66. package/tools/swe_qa_aci.py +369 -0
  67. package/tools/sync_agent_rules.py +1 -1
  68. package/tools/test_memory_os.py +409 -0
  69. package/tools/tot_exploratory_tester.py +240 -0
  70. package/tools/update_sentinel.py +3 -3
  71. package/tools/welcome_page.py +1 -1
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
3
+ # SPDX-License-Identifier: MIT
4
+ # Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
5
+
6
+ """
7
+ Hierarchical QA Test Memory OS (Letta / MemGPT Pattern)
8
+ -------------------------------------------------------
9
+ Provides a 3-tier persistent memory operating system for test automation:
10
+ 1. Core Memory: Fast key-value blocks for active environment context (AUT URL, auth token expiry, flags).
11
+ 2. Recall Memory: Sequential event log of recent test execution steps and subagent actions.
12
+ 3. Archival Memory: Long-term searchable knowledge base of locator mutations and flaky patterns,
13
+ version-scoped by AUT release (e.g. v2.1 vs v2.2) to eliminate memory rot.
14
+ 4. Memory Hygiene: Garbage collection and secret scrubbing (zero plaintext tokens/passwords).
15
+
16
+ Usage:
17
+ python tools/test_memory_os.py init --db-path .agent_memory/qa_memory.db
18
+ python tools/test_memory_os.py update-core --block auth_state --data '{"token_expiry": "..."}'
19
+ python tools/test_memory_os.py archival-insert --key LOCATOR_MUTATION --payload '{"page": "LoginPage", ...}' --aut-version "v2.1"
20
+ python tools/test_memory_os.py archival-search --query "Login submit button" --aut-version "v2.1" --json
21
+ python tools/test_memory_os.py gc --scrub-secrets --max-age-days 30
22
+ """
23
+
24
+ import os
25
+ import sys
26
+ import re
27
+ import json
28
+ import sqlite3
29
+ import time
30
+ import difflib
31
+ import argparse
32
+ from datetime import datetime, timezone, timedelta
33
+ from typing import Dict, Any, List, Optional, Tuple
34
+
35
+ # Enforce UTF-8 stdout encoding
36
+ if hasattr(sys.stdout, 'reconfigure'):
37
+ try:
38
+ sys.stdout.reconfigure(encoding='utf-8')
39
+ sys.stderr.reconfigure(encoding='utf-8')
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ SECRET_PATTERNS = [
45
+ re.compile(r'["\']?(?:password|passwd|pwd|secret|apiKey|api_key|token|bearer)["\']?\s*[:=]\s*["\']?([^"\'\s,;]{8,})["\']?', re.IGNORECASE),
46
+ re.compile(r'Bearer\s+([a-zA-Z0-9_\-\.]{15,})', re.IGNORECASE),
47
+ re.compile(r'-----BEGIN (?:RSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA )?PRIVATE KEY-----'),
48
+ ]
49
+
50
+
51
+ class TestMemoryOS:
52
+ """3-Tier Hierarchical QA Memory OS."""
53
+ __test__ = False
54
+
55
+ def __init__(self, db_path: Optional[str] = None):
56
+ if db_path:
57
+ self.db_path = db_path
58
+ else:
59
+ repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
60
+ self.db_path = os.path.join(repo_root, ".agent_memory", "qa_memory.db")
61
+
62
+ os.makedirs(os.path.dirname(os.path.abspath(self.db_path)), exist_ok=True)
63
+ self._init_db()
64
+
65
+ def _get_connection(self) -> sqlite3.Connection:
66
+ conn = sqlite3.connect(self.db_path)
67
+ conn.row_factory = sqlite3.Row
68
+ return conn
69
+
70
+ def _init_db(self):
71
+ with self._get_connection() as conn:
72
+ # 1. Core Memory Table
73
+ conn.execute("""
74
+ CREATE TABLE IF NOT EXISTS core_memory (
75
+ block_key TEXT PRIMARY KEY,
76
+ data_json TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL
78
+ )
79
+ """)
80
+ # 2. Recall Memory Table
81
+ conn.execute("""
82
+ CREATE TABLE IF NOT EXISTS recall_memory (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ session_id TEXT NOT NULL,
85
+ event_type TEXT NOT NULL,
86
+ actor TEXT NOT NULL,
87
+ payload_json TEXT NOT NULL,
88
+ timestamp TEXT NOT NULL
89
+ )
90
+ """)
91
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_recall_session ON recall_memory(session_id, timestamp)")
92
+
93
+ # 3. Archival Memory Table
94
+ conn.execute("""
95
+ CREATE TABLE IF NOT EXISTS archival_memory (
96
+ id TEXT PRIMARY KEY,
97
+ key TEXT NOT NULL,
98
+ aut_version TEXT DEFAULT 'latest',
99
+ payload_json TEXT NOT NULL,
100
+ tags TEXT DEFAULT '',
101
+ text_content TEXT NOT NULL,
102
+ created_at TEXT NOT NULL
103
+ )
104
+ """)
105
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_archival_key_ver ON archival_memory(key, aut_version)")
106
+
107
+ # ----------------- Tier 1: Core Memory -----------------
108
+
109
+ def update_core(self, block_key: str, data: Any) -> Dict[str, Any]:
110
+ """Updates or inserts a core memory block."""
111
+ data_str = json.dumps(data) if not isinstance(data, str) else data
112
+ now = datetime.now(timezone.utc).isoformat()
113
+ with self._get_connection() as conn:
114
+ conn.execute("""
115
+ INSERT INTO core_memory (block_key, data_json, updated_at)
116
+ VALUES (?, ?, ?)
117
+ ON CONFLICT(block_key) DO UPDATE SET
118
+ data_json=excluded.data_json,
119
+ updated_at=excluded.updated_at
120
+ """, (block_key, data_str, now))
121
+
122
+ return {
123
+ "status": "PASS",
124
+ "block_key": block_key,
125
+ "updated_at": now
126
+ }
127
+
128
+ def get_core(self, block_key: str) -> Optional[Dict[str, Any]]:
129
+ """Retrieves a core memory block."""
130
+ with self._get_connection() as conn:
131
+ row = conn.execute("SELECT data_json, updated_at FROM core_memory WHERE block_key = ?", (block_key,)).fetchone()
132
+ if row:
133
+ try:
134
+ parsed = json.loads(row["data_json"])
135
+ except Exception:
136
+ parsed = row["data_json"]
137
+ return {
138
+ "block_key": block_key,
139
+ "data": parsed,
140
+ "updated_at": row["updated_at"]
141
+ }
142
+ return None
143
+
144
+ def list_core(self) -> List[Dict[str, Any]]:
145
+ """Lists all core memory blocks."""
146
+ with self._get_connection() as conn:
147
+ rows = conn.execute("SELECT block_key, data_json, updated_at FROM core_memory ORDER BY block_key").fetchall()
148
+ results = []
149
+ for r in rows:
150
+ try:
151
+ parsed = json.loads(r["data_json"])
152
+ except Exception:
153
+ parsed = r["data_json"]
154
+ results.append({"block_key": r["block_key"], "data": parsed, "updated_at": r["updated_at"]})
155
+ return results
156
+
157
+ # ----------------- Tier 2: Recall Memory -----------------
158
+
159
+ def recall_insert(self, session_id: str, event_type: str, actor: str, payload: Any) -> int:
160
+ """Inserts an event into recall memory log."""
161
+ payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
162
+ now = datetime.now(timezone.utc).isoformat()
163
+ with self._get_connection() as conn:
164
+ cursor = conn.execute("""
165
+ INSERT INTO recall_memory (session_id, event_type, actor, payload_json, timestamp)
166
+ VALUES (?, ?, ?, ?, ?)
167
+ """, (session_id, event_type, actor, payload_str, now))
168
+ return cursor.lastrowid
169
+
170
+ def recall_log(self, session_id: str, limit: int = 50) -> List[Dict[str, Any]]:
171
+ """Retrieves recent sequential steps for a session."""
172
+ with self._get_connection() as conn:
173
+ rows = conn.execute("""
174
+ SELECT id, session_id, event_type, actor, payload_json, timestamp
175
+ FROM recall_memory
176
+ WHERE session_id = ?
177
+ ORDER BY id ASC
178
+ LIMIT ?
179
+ """, (session_id, limit)).fetchall()
180
+ events = []
181
+ for r in rows:
182
+ try:
183
+ payload = json.loads(r["payload_json"])
184
+ except Exception:
185
+ payload = r["payload_json"]
186
+ events.append({
187
+ "id": r["id"],
188
+ "session_id": r["session_id"],
189
+ "event_type": r["event_type"],
190
+ "actor": r["actor"],
191
+ "payload": payload,
192
+ "timestamp": r["timestamp"]
193
+ })
194
+ return events
195
+
196
+ # ----------------- Tier 3: Archival Memory -----------------
197
+
198
+ def archival_insert(self, key: str, payload: Any, aut_version: str = "latest", tags: str = "") -> str:
199
+ """Inserts long-term searchable knowledge into archival memory."""
200
+ payload_str = json.dumps(payload) if not isinstance(payload, str) else payload
201
+ now = datetime.now(timezone.utc).isoformat()
202
+ mem_id = f"mem_arch_{int(time.time() * 1000) % 10000000}"
203
+
204
+ # Flatten text content for semantic/fuzzy search
205
+ text_content = f"{key} {aut_version} {tags} {payload_str}".lower()
206
+
207
+ with self._get_connection() as conn:
208
+ conn.execute("""
209
+ INSERT INTO archival_memory (id, key, aut_version, payload_json, tags, text_content, created_at)
210
+ VALUES (?, ?, ?, ?, ?, ?, ?)
211
+ """, (mem_id, key, aut_version, payload_str, tags, text_content, now))
212
+
213
+ return mem_id
214
+
215
+ def archival_search(self, query: str, aut_version: Optional[str] = None, top_k: int = 3) -> Dict[str, Any]:
216
+ """Searches archival memory with semantic token matching and fuzzy ratio."""
217
+ query_lower = query.lower()
218
+ tokens = set(re.findall(r'\b[a-z0-9_]{3,}\b', query_lower))
219
+
220
+ with self._get_connection() as conn:
221
+ if aut_version:
222
+ rows = conn.execute(
223
+ "SELECT id, key, aut_version, payload_json, tags, text_content, created_at FROM archival_memory WHERE aut_version = ?",
224
+ (aut_version,)
225
+ ).fetchall()
226
+ else:
227
+ rows = conn.execute(
228
+ "SELECT id, key, aut_version, payload_json, tags, text_content, created_at FROM archival_memory"
229
+ ).fetchall()
230
+
231
+ scored = []
232
+ for r in rows:
233
+ text = r["text_content"]
234
+ # 1. Token overlap score
235
+ text_tokens = set(re.findall(r'\b[a-z0-9_]{3,}\b', text))
236
+ if tokens:
237
+ token_score = len(tokens.intersection(text_tokens)) / len(tokens)
238
+ else:
239
+ token_score = 0.0
240
+
241
+ # 2. Fuzzy sequence similarity
242
+ fuzzy_ratio = difflib.SequenceMatcher(None, query_lower, text[:200]).ratio()
243
+
244
+ # Combined score: 60% token overlap + 40% fuzzy match
245
+ score = round((0.6 * token_score) + (0.4 * fuzzy_ratio), 3)
246
+
247
+ if score > 0.15 or (tokens and tokens.intersection(text_tokens)):
248
+ try:
249
+ payload = json.loads(r["payload_json"])
250
+ except Exception:
251
+ payload = r["payload_json"]
252
+
253
+ scored.append({
254
+ "id": r["id"],
255
+ "similarity_score": score,
256
+ "key": r["key"],
257
+ "aut_version": r["aut_version"],
258
+ "timestamp": r["created_at"],
259
+ "payload": payload
260
+ })
261
+
262
+ scored.sort(key=lambda x: x["similarity_score"], reverse=True)
263
+ results = scored[:top_k]
264
+
265
+ return {
266
+ "status": "PASS",
267
+ "query": query,
268
+ "aut_version": aut_version or "all",
269
+ "matches_found": len(results),
270
+ "results": results
271
+ }
272
+
273
+ # ----------------- Hygiene & Secret Scrubbing -----------------
274
+
275
+ def scrub_string(self, text: str) -> Tuple[str, int]:
276
+ """Masks detected secrets in text."""
277
+ scrubbed = text
278
+ count = 0
279
+ for pat in SECRET_PATTERNS:
280
+ matches = list(pat.finditer(scrubbed))
281
+ if matches:
282
+ count += len(matches)
283
+ for m in matches:
284
+ val = m.group(0)
285
+ scrubbed = scrubbed.replace(val, "[MASKED_SECRET]")
286
+ return scrubbed, count
287
+
288
+ def gc(self, scrub_secrets: bool = True, max_age_days: int = 30) -> Dict[str, Any]:
289
+ """Performs garbage collection and secret scrubbing."""
290
+ scrub_count = 0
291
+ deleted_recall = 0
292
+
293
+ with self._get_connection() as conn:
294
+ # 1. Clean old recall logs
295
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=max_age_days)).isoformat()
296
+ cur = conn.execute("DELETE FROM recall_memory WHERE timestamp < ?", (cutoff,))
297
+ deleted_recall = cur.rowcount
298
+
299
+ # 2. Scrub secrets in core memory
300
+ if scrub_secrets:
301
+ rows = conn.execute("SELECT block_key, data_json FROM core_memory").fetchall()
302
+ for r in rows:
303
+ new_val, c = self.scrub_string(r["data_json"])
304
+ if c > 0:
305
+ scrub_count += c
306
+ conn.execute("UPDATE core_memory SET data_json = ? WHERE block_key = ?", (new_val, r["block_key"]))
307
+
308
+ # Scrub archival memory
309
+ a_rows = conn.execute("SELECT id, payload_json, text_content FROM archival_memory").fetchall()
310
+ for ar in a_rows:
311
+ new_p, cp = self.scrub_string(ar["payload_json"])
312
+ new_t, ct = self.scrub_string(ar["text_content"])
313
+ if cp > 0 or ct > 0:
314
+ scrub_count += (cp + ct)
315
+ conn.execute("UPDATE archival_memory SET payload_json = ?, text_content = ? WHERE id = ?", (new_p, new_t, ar["id"]))
316
+
317
+ # Database file size check
318
+ db_size_mb = 0.0
319
+ if os.path.exists(self.db_path):
320
+ db_size_mb = round(os.path.getsize(self.db_path) / (1024 * 1024), 2)
321
+
322
+ return {
323
+ "status": "PASS",
324
+ "db_path": self.db_path,
325
+ "db_size_mb": db_size_mb,
326
+ "max_size_limit_mb": 50.0,
327
+ "size_status": "PASS" if db_size_mb <= 50.0 else "WARNING_OVERSIZED",
328
+ "secrets_scrubbed": scrub_count,
329
+ "expired_recall_records_deleted": deleted_recall
330
+ }
331
+
332
+
333
+ def main():
334
+ parser = argparse.ArgumentParser(description="Hierarchical QA Test Memory OS (Letta Pattern)")
335
+ parser.add_argument("--db-path", help="Path to SQLite database file")
336
+ subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
337
+
338
+ # init
339
+ init_parser = subparsers.add_parser("init", help="Initialize memory OS database")
340
+
341
+ # update-core
342
+ uc_parser = subparsers.add_parser("update-core", help="Update core memory block")
343
+ uc_parser.add_argument("--block", required=True, help="Core memory block key")
344
+ uc_parser.add_argument("--data", required=True, help="JSON or text payload")
345
+
346
+ # get-core
347
+ gc_parser = subparsers.add_parser("get-core", help="Retrieve core memory block")
348
+ gc_parser.add_argument("--block", required=True, help="Core memory block key")
349
+
350
+ # archival-insert
351
+ ai_parser = subparsers.add_parser("archival-insert", help="Insert long-term knowledge into Archival Memory")
352
+ ai_parser.add_argument("--key", required=True, help="Knowledge key (e.g. LOCATOR_MUTATION)")
353
+ ai_parser.add_argument("--payload", required=True, help="JSON data payload")
354
+ ai_parser.add_argument("--aut-version", default="latest", help="AUT release version (e.g. v2.1)")
355
+ ai_parser.add_argument("--tags", default="", help="Comma-separated search tags")
356
+
357
+ # archival-search
358
+ as_parser = subparsers.add_parser("archival-search", help="Search archival memory by query")
359
+ as_parser.add_argument("--query", required=True, help="Search query string")
360
+ as_parser.add_argument("--aut-version", help="Filter by AUT release version")
361
+ as_parser.add_argument("--top-k", type=int, default=3, help="Max results to return")
362
+ as_parser.add_argument("--json", action="store_true", help="Print JSON output")
363
+
364
+ # gc
365
+ clean_parser = subparsers.add_parser("gc", help="Garbage collection and secret scrubbing")
366
+ clean_parser.add_argument("--scrub-secrets", action="store_true", default=True, help="Scrub sensitive credentials")
367
+ clean_parser.add_argument("--max-age-days", type=int, default=30, help="Max age for recall logs")
368
+
369
+ args = parser.parse_args()
370
+ mem = TestMemoryOS(db_path=args.db_path)
371
+
372
+ if args.subcommand == "init":
373
+ print(json.dumps({"status": "PASS", "db_path": mem.db_path, "initialized": True}, indent=2))
374
+
375
+ elif args.subcommand == "update-core":
376
+ try:
377
+ parsed_data = json.loads(args.data)
378
+ except Exception:
379
+ parsed_data = args.data
380
+ res = mem.update_core(args.block, parsed_data)
381
+ print(json.dumps(res, indent=2))
382
+
383
+ elif args.subcommand == "get-core":
384
+ res = mem.get_core(args.block)
385
+ print(json.dumps(res or {"status": "NOT_FOUND", "block": args.block}, indent=2))
386
+
387
+ elif args.subcommand == "archival-insert":
388
+ try:
389
+ parsed_payload = json.loads(args.payload)
390
+ except Exception:
391
+ parsed_payload = args.payload
392
+ mid = mem.archival_insert(args.key, parsed_payload, aut_version=args.aut_version, tags=args.tags)
393
+ print(json.dumps({"status": "PASS", "id": mid, "key": args.key, "aut_version": args.aut_version}, indent=2))
394
+
395
+ elif args.subcommand == "archival-search":
396
+ res = mem.archival_search(args.query, aut_version=args.aut_version, top_k=args.top_k)
397
+ print(json.dumps(res, indent=2))
398
+
399
+ elif args.subcommand == "gc":
400
+ res = mem.gc(scrub_secrets=args.scrub_secrets, max_age_days=args.max_age_days)
401
+ print(json.dumps(res, indent=2))
402
+
403
+ else:
404
+ parser.print_help()
405
+ sys.exit(1)
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
3
+ # SPDX-License-Identifier: MIT
4
+ # Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
5
+
6
+ """
7
+ Tree/Graph-of-Thoughts Exploratory Testing Engine (Princeton ToT & ETH GoT Pattern)
8
+ ----------------------------------------------------------------------------------
9
+ Expands linear user stories into deliberative non-linear exploration trees:
10
+ 1. Multi-Branch Deliberation: Generates Happy Path, Boundary/Race, and Negative/Chaos branches.
11
+ 2. Risk-Scored Beam Search: Evaluates thought states and prunes low-risk tautological paths.
12
+ 3. Clean POM Synthesis: Produces structured test scenarios ready for Clean POM Page Classes.
13
+ 4. Multi-Hypothesis RCA: Diagnoses test failures across competing failure hypotheses (DOM_TIMEOUT, ASYNC_RACE, DB_LOCK, BACKEND_500).
14
+
15
+ Usage:
16
+ python tools/tot_exploratory_tester.py expand-story --story-file docs/stories/US_PAYMENT.md --max-depth 3 --beam-width 3
17
+ python tools/tot_exploratory_tester.py rca-hypotheses --error-log reports/failed.log
18
+ """
19
+
20
+ import os
21
+ import sys
22
+ import re
23
+ import json
24
+ import time
25
+ import argparse
26
+ from typing import Dict, Any, List, Optional, Tuple
27
+
28
+ # Enforce UTF-8 stdout encoding
29
+ if hasattr(sys.stdout, 'reconfigure'):
30
+ try:
31
+ sys.stdout.reconfigure(encoding='utf-8')
32
+ sys.stderr.reconfigure(encoding='utf-8')
33
+ except Exception:
34
+ pass
35
+
36
+
37
+ class TotExploratoryTester:
38
+ """Tree-of-Thoughts Engine for Exploratory QA and Root Cause Analysis."""
39
+
40
+ def __init__(self, repo_root: Optional[str] = None):
41
+ self.repo_root = repo_root or os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
42
+
43
+ def expand_story(self, story_content: str, max_depth: int = 3, beam_width: int = 3) -> Dict[str, Any]:
44
+ """Decomposes story into a 3-branch Tree of Thoughts with risk scores."""
45
+ # Extract title and criteria
46
+ lines = [l.strip() for l in story_content.splitlines() if l.strip()]
47
+ title = lines[0] if lines else "Untitled User Story"
48
+ if title.startswith("#"):
49
+ title = title.lstrip("#").strip()
50
+
51
+ # Extract keywords
52
+ keywords = re.findall(r'\b[a-zA-Z]{4,}\b', story_content.lower())
53
+ is_checkout = any(k in keywords for k in ["cart", "checkout", "pay", "order", "price"])
54
+ is_auth = any(k in keywords for k in ["login", "auth", "token", "password", "sso"])
55
+
56
+ context_domain = "FINANCIAL_CHECKOUT" if is_checkout else ("AUTH_SESSION" if is_auth else "GENERAL_CRUD")
57
+
58
+ # 3 Branches:
59
+ # 1. Happy Path
60
+ # 2. Boundary / Session Interruption / Race Condition
61
+ # 3. Negative / Chaos / Backend Fault
62
+ branches = [
63
+ {
64
+ "branch_id": "BRANCH_HAPPY_PATH",
65
+ "name": "Standard Happy Business Path",
66
+ "risk_score": 0.25,
67
+ "thoughts": [
68
+ {"step": 1, "thought": "Navigate to initial entity view and ensure clean pre-conditions."},
69
+ {"step": 2, "thought": "Populate valid payload attributes and trigger submit action."},
70
+ {"step": 3, "thought": "Verify success confirmation modal and assert database record creation."}
71
+ ]
72
+ },
73
+ {
74
+ "branch_id": "BRANCH_BOUNDARY_RACE",
75
+ "name": "Boundary, Session Interruption & Concurrency Path",
76
+ "risk_score": 0.88,
77
+ "thoughts": [
78
+ {"step": 1, "thought": "Simulate session expiration or token invalidation mid-flow."},
79
+ {"step": 2, "thought": "Perform rapid double-click on submission action to probe race condition."},
80
+ {"step": 3, "thought": "Assert idempotency: strictly 1 transaction created, duplicate blocked gracefully."}
81
+ ]
82
+ },
83
+ {
84
+ "branch_id": "BRANCH_NEGATIVE_CHAOS",
85
+ "name": "Negative Error Handling & Chaos Fault Path",
86
+ "risk_score": 0.72,
87
+ "thoughts": [
88
+ {"step": 1, "thought": "Inject malformed/boundary input payload exceeding maximum character limits."},
89
+ {"step": 2, "thought": "Simulate backend 500 error or database connection timeout via route stub."},
90
+ {"step": 3, "thought": "Assert user-friendly error toast, zero data corruption, and retry availability."}
91
+ ]
92
+ }
93
+ ]
94
+
95
+ # Beam search selection: sort by risk score descending and take beam_width
96
+ sorted_branches = sorted(branches, key=lambda b: b["risk_score"], reverse=True)
97
+ selected_branches = sorted_branches[:beam_width]
98
+
99
+ return {
100
+ "status": "PASS",
101
+ "story_title": title,
102
+ "domain": context_domain,
103
+ "max_depth": max_depth,
104
+ "beam_width": beam_width,
105
+ "total_branches_explored": len(branches),
106
+ "highest_risk_branch": selected_branches[0]["branch_id"],
107
+ "branches": selected_branches,
108
+ "clean_pom_recommendations": [
109
+ f"Implement {context_domain}Page extending BasePage",
110
+ "Encapsulate double-click debounce in clickSubmitSafe()",
111
+ "Assert boundary toast in verifyErrorMessageDisplayed()"
112
+ ]
113
+ }
114
+
115
+ def rca_hypotheses(self, error_log: str, hypotheses: Optional[List[str]] = None) -> Dict[str, Any]:
116
+ """Performs multi-hypothesis Root Cause Analysis on test failures."""
117
+ if not hypotheses:
118
+ hypotheses = ["DOM_TIMEOUT", "ASYNC_RACE", "DB_LOCK", "BACKEND_500", "STALE_SELECTOR"]
119
+
120
+ log_lower = error_log.lower()
121
+ scored_hypotheses = []
122
+
123
+ for hyp in hypotheses:
124
+ score = 0.0
125
+ evidence = []
126
+
127
+ if hyp == "DOM_TIMEOUT":
128
+ if "timed out" in log_lower or "waitfortimeout" in log_lower or "5000ms" in log_lower:
129
+ score += 0.85
130
+ evidence.append("Found timeout or waiting duration signature in log")
131
+ if "element is not visible" in log_lower or "tobevisible" in log_lower:
132
+ score += 0.10
133
+ evidence.append("Element visibility expectation failed")
134
+
135
+ elif hyp == "ASYNC_RACE":
136
+ if "click intercepted" in log_lower or "animation" in log_lower or "detached" in log_lower:
137
+ score += 0.90
138
+ evidence.append("Overlay or detachment signature points to async race condition")
139
+ if "networkidle" in log_lower or "pending" in log_lower:
140
+ score += 0.15
141
+ evidence.append("Pending network requests active during action")
142
+
143
+ elif hyp == "DB_LOCK":
144
+ if "deadlock" in log_lower or "lock" in log_lower or "sql" in log_lower or "unique constraint" in log_lower:
145
+ score += 0.88
146
+ evidence.append("Database lock or constraint collision detected in trace")
147
+
148
+ elif hyp == "BACKEND_500":
149
+ if re.search(r'\b500\b', log_lower) and not re.search(r'\b500\d+ms\b', log_lower) and ("http" in log_lower or "status" in log_lower or "server error" in log_lower or "internal" in log_lower):
150
+ score += 0.95
151
+ evidence.append("HTTP 500 status code explicitly logged")
152
+ elif "internal server error" in log_lower:
153
+ score += 0.95
154
+ evidence.append("Internal Server Error explicitly logged")
155
+
156
+ elif hyp == "STALE_SELECTOR":
157
+ if "locator" in log_lower or "selector" in log_lower or "xpath" in log_lower:
158
+ score += 0.70
159
+ evidence.append("Locator or selector syntax mentioned in failure message")
160
+
161
+ # Baseline confidence
162
+ if not evidence:
163
+ score = 0.10
164
+ evidence.append("No direct signature found; baseline background likelihood")
165
+
166
+ scored_hypotheses.append({
167
+ "hypothesis": hyp,
168
+ "confidence_score": round(min(1.0, score), 3),
169
+ "evidence": evidence
170
+ })
171
+
172
+ scored_hypotheses.sort(key=lambda h: h["confidence_score"], reverse=True)
173
+ winner = scored_hypotheses[0]
174
+
175
+ recommendations = {
176
+ "DOM_TIMEOUT": "Replace hardcoded delay with page.waitForLoadState('networkidle') or element.waitFor({state: 'visible'})",
177
+ "ASYNC_RACE": "Use clickSafe() helper with overlay detachment waiting or dispatchEvent('click')",
178
+ "DB_LOCK": "Randomize entity code prefix via DataGenerator.uniqueCode('AUTO_') and enforce teardown cleanup",
179
+ "BACKEND_500": "Inspect backend server logs for uncaught 500 NullPointerException or DB connection pool exhaustion",
180
+ "STALE_SELECTOR": "Run tools/agentless_patcher.py heal-failure to update stale attribute to ARIA role"
181
+ }
182
+
183
+ return {
184
+ "status": "PASS",
185
+ "primary_root_cause": winner["hypothesis"],
186
+ "confidence": winner["confidence_score"],
187
+ "recommended_fix": recommendations.get(winner["hypothesis"], "Review stacktrace with team"),
188
+ "ranked_hypotheses": scored_hypotheses
189
+ }
190
+
191
+
192
+ def main():
193
+ parser = argparse.ArgumentParser(description="Tree-of-Thoughts Exploratory Testing Engine")
194
+ subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
195
+
196
+ # expand-story
197
+ exp_p = subparsers.add_parser("expand-story", help="Expand User Story into Tree of Thoughts")
198
+ exp_p.add_argument("--story-file", required=True, help="Path to story markdown file or text string")
199
+ exp_p.add_argument("--max-depth", type=int, default=3, help="Max thought tree depth")
200
+ exp_p.add_argument("--beam-width", type=int, default=3, help="Beam search width")
201
+ exp_p.add_argument("--out-graph", help="Optional path to output graph JSON")
202
+
203
+ # rca-hypotheses
204
+ rca_p = subparsers.add_parser("rca-hypotheses", help="Diagnose failure via multi-hypothesis RCA")
205
+ rca_p.add_argument("--error-log", required=True, help="Path to error log or log string")
206
+ rca_p.add_argument("--hypotheses", help="Comma-separated hypotheses list")
207
+
208
+ args = parser.parse_args()
209
+ tester = TotExploratoryTester()
210
+
211
+ if args.subcommand == "expand-story":
212
+ content = args.story_file
213
+ if os.path.exists(args.story_file):
214
+ with open(args.story_file, "r", encoding="utf-8", errors="replace") as f:
215
+ content = f.read()
216
+ res = tester.expand_story(content, max_depth=args.max_depth, beam_width=args.beam_width)
217
+ if args.out_graph:
218
+ os.makedirs(os.path.dirname(os.path.abspath(args.out_graph)), exist_ok=True)
219
+ with open(args.out_graph, "w", encoding="utf-8") as f:
220
+ json.dump(res, f, indent=2)
221
+ print(json.dumps(res, indent=2))
222
+ sys.exit(0)
223
+
224
+ elif args.subcommand == "rca-hypotheses":
225
+ log_content = args.error_log
226
+ if os.path.exists(args.error_log):
227
+ with open(args.error_log, "r", encoding="utf-8", errors="replace") as f:
228
+ log_content = f.read()
229
+ hyp_list = [h.strip() for h in args.hypotheses.split(",")] if args.hypotheses else None
230
+ res = tester.rca_hypotheses(error_log=log_content, hypotheses=hyp_list)
231
+ print(json.dumps(res, indent=2))
232
+ sys.exit(0)
233
+
234
+ else:
235
+ parser.print_help()
236
+ sys.exit(1)
237
+
238
+
239
+ if __name__ == "__main__":
240
+ main()
@@ -65,7 +65,7 @@ def get_local_version():
65
65
  try:
66
66
  with open(pkg_file, "r", encoding="utf-8") as f:
67
67
  data = json.load(f)
68
- return data.get("version", "2.5.0")
68
+ return data.get("version", "2.7.0")
69
69
  except (OSError, json.JSONDecodeError):
70
70
  data = {}
71
71
 
@@ -74,11 +74,11 @@ def get_local_version():
74
74
  try:
75
75
  with open(manifest_file, "r", encoding="utf-8") as f:
76
76
  data = json.load(f)
77
- return data.get("version", "2.5.0")
77
+ return data.get("version", "2.7.0")
78
78
  except (OSError, json.JSONDecodeError):
79
79
  data = {}
80
80
 
81
- return "2.5.0"
81
+ return "2.7.0"
82
82
 
83
83
 
84
84
  def fetch_remote_version():
@@ -48,7 +48,7 @@ def get_hub_metadata() -> Dict[str, Any]:
48
48
  """Retrieves version and catalog count from skills_manifest.json."""
49
49
  manifest_path = os.path.join(HUB_ROOT, "skills_manifest.json")
50
50
  meta = {
51
- "version": "2.6.0",
51
+ "version": "2.7.0",
52
52
  "totalSkills": 42,
53
53
  "author": "Thế Vị (@TeeVee998)",
54
54
  "repo": "https://github.com/TeeVee998/Synapse-Hub"