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.
- package/.agents/agents/failure-analyst.md +4 -0
- package/.agents/agents/orchestrator.md +16 -0
- package/.agents/agents/pom-crafter.md +8 -0
- package/.agents/agents/quality-gatekeeper.md +5 -0
- package/.agents/agents/test-runner.md +5 -0
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/graphify.md +2 -0
- package/.agents/rules/mandatory-playwright-live.md +9 -9
- package/.agents/rules/mobile_appium_rules.md +8 -1
- package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
- package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
- package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
- package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
- package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
- package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
- package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
- package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
- package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
- package/.agents/skills/qa-memory-os/SKILL.md +80 -0
- package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
- package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
- package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
- package/CLAUDE.md +1 -1
- package/README.en.md +5 -5
- package/README.md +3 -3
- package/docs/ARCHITECTURE.md +8 -8
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/package.json +1 -1
- package/skills_manifest.json +88 -7
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +1 -1
- package/tools/TOOLS_RUNBOOK.md +1 -1
- package/tools/__init__.py +1 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +9 -6
- package/tools/auto_heal.py +19 -0
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +237 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +3 -3
- package/tools/dashboard_generator.py +4 -4
- package/tools/db_sandbox.py +4 -0
- package/tools/heal_pr_automator.py +25 -0
- package/tools/mcp_excel_server.py +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/pinpoint_failure.py +12 -0
- package/tools/project_state.py +4 -0
- package/tools/qa_event_stream.py +336 -0
- package/tools/reversible_sandbox.py +4 -0
- package/tools/swe_qa_aci.py +369 -0
- package/tools/sync_agent_rules.py +1 -1
- package/tools/test_memory_os.py +409 -0
- package/tools/tot_exploratory_tester.py +240 -0
- package/tools/update_sentinel.py +3 -3
- 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()
|
package/tools/update_sentinel.py
CHANGED
|
@@ -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.
|
|
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.
|
|
77
|
+
return data.get("version", "2.7.0")
|
|
78
78
|
except (OSError, json.JSONDecodeError):
|
|
79
79
|
data = {}
|
|
80
80
|
|
|
81
|
-
return "2.
|
|
81
|
+
return "2.7.0"
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def fetch_remote_version():
|
package/tools/welcome_page.py
CHANGED
|
@@ -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.
|
|
51
|
+
"version": "2.7.0",
|
|
52
52
|
"totalSkills": 42,
|
|
53
53
|
"author": "Thế Vị (@TeeVee998)",
|
|
54
54
|
"repo": "https://github.com/TeeVee998/Synapse-Hub"
|