skills/memstate/scripts/test_skill.py (7426 bytes)
1 #!/usr/bin/env python3 2 """ 3 End-to-end test suite for the Memstate AI Skill. 4 Tests all 7 scripts against the live API. 5 """ 6 import json 7 import os 8 import subprocess 9 import sys 10 import time 11 12 SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) 13 PROJECT_ID = f"skill-test-{int(time.time())}" 14 15 PASS = "\033[92m✅ PASS\033[0m" 16 FAIL = "\033[91m❌ FAIL\033[0m" 17 SKIP = "\033[93m⚠️ SKIP\033[0m" 18 19 results = [] 20 21 def run_script(script, *args): 22 cmd = [sys.executable, os.path.join(SCRIPTS_DIR, script)] + list(args) 23 result = subprocess.run(cmd, capture_output=True, text=True) 24 return result.returncode, result.stdout, result.stderr 25 26 def test(name, script, args, expect_key=None, expect_value=None): 27 code, stdout, stderr = run_script(script, *args) 28 passed = code == 0 29 detail = "" 30 if passed and expect_key: 31 try: 32 json_str = stdout 33 if "Polling for completion..." in stdout: 34 json_str = stdout.split("Polling for completion...")[1].strip() 35 data = json.loads(json_str) 36 actual = data.get(expect_key) 37 if expect_value is not None and actual != expect_value: 38 passed = False 39 detail = f" (expected {expect_key}={expect_value!r}, got {actual!r})" 40 elif actual is None: 41 passed = False 42 detail = f" (key '{expect_key}' missing from response)" 43 except json.JSONDecodeError: 44 passed = False 45 detail = " (response was not valid JSON)" 46 status = PASS if passed else FAIL 47 print(f"{status} {name}{detail}") 48 if not passed: 49 if stderr: 50 print(f" stderr: {stderr.strip()}") 51 if stdout: 52 print(f" stdout: {stdout.strip()[:300]}") 53 results.append((name, passed)) 54 return passed, stdout 55 56 def main(): 57 print(f"\n{'='*60}") 58 print(f" Memstate AI Skill — End-to-End Test Suite") 59 print(f" Project: {PROJECT_ID}") 60 print(f"{'='*60}\n") 61 62 # 1. memstate_set — store a fact; the first write creates the project 63 ok, out = test( 64 "memstate_set: store a single fact", 65 "memstate_set.py", 66 ["--project", PROJECT_ID, "--new-project", "--keypath", "database.engine", "--value", "PostgreSQL", "--category", "decision"], 67 expect_key="action", expect_value="created" 68 ) 69 70 # 2. memstate_set — update the same keypath (should supersede) 71 ok, out = test( 72 "memstate_set: update same keypath (supersede)", 73 "memstate_set.py", 74 ["--project", PROJECT_ID, "--keypath", "database.engine", "--value", "PostgreSQL 16", "--category", "decision"], 75 expect_key="action", expect_value="superseded" 76 ) 77 memory_id = None 78 if ok: 79 try: 80 memory_id = json.loads(out)["stored"]["id"] 81 except Exception: 82 pass 83 84 # 3. memstate_set — store a second fact 85 test( 86 "memstate_set: store a second fact", 87 "memstate_set.py", 88 ["--project", PROJECT_ID, "--keypath", "auth.method", "--value", "JWT with httpOnly cookies", "--category", "decision"], 89 expect_key="action", expect_value="created" 90 ) 91 92 # 4. memstate_get — list all projects 93 test( 94 "memstate_get: list all projects", 95 "memstate_get.py", 96 ["--list-projects"], 97 expect_key="projects" 98 ) 99 100 # 5. memstate_get — project tree 101 test( 102 "memstate_get: project tree", 103 "memstate_get.py", 104 ["--project", PROJECT_ID], 105 expect_key="domains" 106 ) 107 108 # 6. memstate_get - subtree with content 109 test( 110 "memstate_get: subtree with --include-content", 111 "memstate_get.py", 112 ["--project", PROJECT_ID, "--keypath", "database", "--include-content"], 113 expect_key="memories" 114 ) 115 116 # 7. memstate_get — by memory_id 117 if memory_id: 118 test( 119 "memstate_get: fetch by memory_id", 120 "memstate_get.py", 121 ["--memory-id", str(memory_id)], 122 expect_key="id", expect_value=memory_id 123 ) 124 else: 125 print(f"{SKIP} memstate_get: fetch by memory_id (no memory_id from previous step)") 126 results.append(("memstate_get: fetch by memory_id", None)) 127 128 # 8. memstate_search — semantic search 129 test( 130 "memstate_search: semantic search", 131 "memstate_search.py", 132 ["--project", PROJECT_ID, "--query", "what database does the project use"], 133 expect_key="results" 134 ) 135 136 # 9. memstate_history - by keypath 137 test( 138 "memstate_history: by keypath", 139 "memstate_history.py", 140 ["--project", PROJECT_ID, "--keypath", "database.engine"], 141 expect_key="versions" 142 ) 143 144 # 10. memstate_remember — markdown ingestion (async) 145 test( 146 "memstate_remember: markdown ingestion", 147 "memstate_remember.py", 148 ["--project", PROJECT_ID, 149 "--content", "## Architecture Summary\n- Backend: FastAPI\n- Database: PostgreSQL 16\n- Auth: JWT with httpOnly cookies\n- Deploy: Docker on AWS ECS", 150 "--source", "agent"], 151 expect_key="method", expect_value="headings" 152 ) 153 154 # 11. memstate_delete — soft-delete a keypath 155 test( 156 "memstate_delete: soft-delete a keypath", 157 "memstate_delete.py", 158 ["--project", PROJECT_ID, "--keypath", "auth.method"], 159 expect_key="deleted_count" 160 ) 161 162 # 12. memstate_delete_project — cleanup 163 test( 164 "memstate_delete_project: cleanup test project", 165 "memstate_delete_project.py", 166 ["--project", PROJECT_ID], 167 expect_key="project_id", expect_value=PROJECT_ID 168 ) 169 170 # 13. user scope — the daemon allows only the allowlisted shapes there. 171 user_kp = f"preferences.skill_test_{int(time.time())}" 172 test( 173 "memstate_set: --scope user accepts a preferences fact", 174 "memstate_set.py", 175 ["--scope", "user", "--keypath", user_kp, "--value", "terse answers"], 176 expect_key="action", expect_value="created" 177 ) 178 code, stdout, stderr = run_script( 179 "memstate_set.py", "--scope", "user", "--keypath", "todo.skill_test", "--value", "nope") 180 rejected = code == 1 and "HTTP 400" in stderr and "allowed" in stderr 181 print(f"{PASS if rejected else FAIL} memstate_set: --scope user rejects a todo keypath") 182 if not rejected: 183 print(f" code={code} stderr={stderr.strip()[:300]}") 184 results.append(("memstate_set: --scope user rejects a todo keypath", rejected)) 185 test( 186 "memstate_get: --scope user tree carries user.host", 187 "memstate_get.py", 188 ["--scope", "user"], 189 expect_key="host" 190 ) 191 test( 192 "memstate_get: project tree carries the user scope", 193 "memstate_get.py", 194 ["--project", "skill_test_other"], 195 expect_key="user" 196 ) 197 test( 198 "memstate_delete: --scope user cleans up", 199 "memstate_delete.py", 200 ["--scope", "user", "--keypath", user_kp], 201 expect_key="deleted_count" 202 ) 203 204 # Summary 205 passed = sum(1 for _, r in results if r is True) 206 failed = sum(1 for _, r in results if r is False) 207 skipped = sum(1 for _, r in results if r is None) 208 total = len(results) 209 210 print(f"\n{'='*60}") 211 print(f" Results: {passed}/{total} passed | {failed} failed | {skipped} skipped") 212 print(f"{'='*60}\n") 213 214 return 0 if failed == 0 else 1 215 216 if __name__ == "__main__": 217 sys.exit(main())