#!/usr/bin/env python3 """S1 test: Agents API backend β€” profile scan, CRUD, chat endpoints.""" import json, sys, urllib.request BASE = "http://127.0.0.1:5001" passed = 0 failed = 0 skipped = 0 def api_get(path, check=None): global passed, failed try: url = f"{BASE}{path}" req = urllib.request.Request(url, method="GET") with urllib.request.urlopen(req, timeout=10) as r: body = json.loads(r.read()) if check: ok, msg = check(r.status, body) else: ok, msg = body.get("ok", False), f"HTTP {r.status}" if ok: passed += 1 print(f" PASS {path}: {msg}") else: failed += 1 print(f" FAIL {path}: {msg}") except Exception as e: failed += 1 print(f" FAIL {path}: {e}") def api_post(path, data, check=None): global passed, failed try: url = f"{BASE}{path}" req = urllib.request.Request(url, data=json.dumps(data).encode(), headers={"Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(req, timeout=30) as r: body = json.loads(r.read()) if check: ok, msg = check(r.status, body) else: ok, msg = body.get("ok", False), f"HTTP {r.status}" if ok: passed += 1 print(f" PASS POST {path}: {msg}") else: failed += 1 print(f" FAIL POST {path}: {msg}") except Exception as e: failed += 1 print(f" FAIL POST {path}: {e}") def api_delete(path, check=None): global passed, failed try: url = f"{BASE}{path}" req = urllib.request.Request(url, method="DELETE") with urllib.request.urlopen(req, timeout=30) as r: body = json.loads(r.read()) if check: ok, msg = check(r.status, body) else: ok, msg = body.get("ok", False), f"HTTP {r.status}" if ok: passed += 1 print(f" PASS DELETE {path}: {msg}") else: failed += 1 print(f" FAIL DELETE {path}: {msg}") except Exception as e: failed += 1 print(f" FAIL DELETE {path}: {e}") # TC1: GET /api/profiles returns JSON with default profile + metadata def check_profiles(status, body): if not body.get("ok"): return False, f"ok=false" data = body.get("data", []) if not isinstance(data, list) or len(data) == 0: return False, f"data is empty or not list" p = data[0] required = ["name", "model", "provider", "skills_count", "memory_exists", "description", "soul_preview"] missing = [k for k in required if k not in p] if missing: return False, f"missing fields: {missing}" has_default = any(p.get("name") == "default" for p in data) return has_default, f"profiles={len(data)} has_default={has_default}" api_get("/api/profiles", check=check_profiles) # TC2: GET /api/profile/default returns SOUL.md + skills list def check_profile_detail(status, body): if not body.get("ok"): return False, f"ok=false" data = body.get("data", {}) soul = data.get("soul", "") skills = data.get("skills", []) config = data.get("config", {}) if not soul: return False, "soul is empty" if not isinstance(skills, list): return False, "skills is not list" if not config: return False, "config is empty" return True, f"soul_len={len(soul)} skills={len(skills)} config_keys={list(config.keys())[:3]}" api_get("/api/profile/default", check=check_profile_detail) # TC3: POST /api/profile creates a test profile def check_create(status, body): if not body.get("ok"): return False, f"ok=false error={body.get('error','')}" return True, f"created: {body.get('data', {}).get('name', '?')}" api_post("/api/profile", {"name": "test-s1-agent", "description": "S1 test agent"}, check=check_create) # TC4: DELETE /api/profile/test-s1-agent deletes it def check_delete(status, body): if not body.get("ok"): return False, f"ok=false error={body.get('error','')}" return True, f"deleted: {body.get('data', {}).get('name', '?')}" api_delete("/api/profile/test-s1-agent", check=check_delete) # TC5: DELETE /api/profile/default returns 403 (forbidden) try: url = f"{BASE}/api/profile/default" req = urllib.request.Request(url, method="DELETE") try: with urllib.request.urlopen(req, timeout=10) as r: # If we get here, it didn't 403 failed += 1 print(f" FAIL DELETE /api/profile/default: should return 403 but got {r.status}") except urllib.error.HTTPError as e: if e.code == 403: passed += 1 print(f" PASS DELETE /api/profile/default: 403 forbidden (correct)") else: failed += 1 print(f" FAIL DELETE /api/profile/default: expected 403, got {e.code}") except Exception as e: failed += 1 print(f" FAIL DELETE /api/profile/default: {e}") # TC6: POST /api/profile/default/chat β€” may timeout, mark as skip if so try: import subprocess r = subprocess.run([ "curl", "-s", "-X", "POST", f"{BASE}/api/profile/default/chat", "-H", "Content-Type: application/json", "-d", json.dumps({"message": "ping"}), "--max-time", "65" ], capture_output=True, text=True, timeout=70) body = json.loads(r.stdout) if body.get("ok"): resp = body.get("data", {}).get("response", "") if resp: passed += 1 print(f" PASS POST /api/profile/default/chat: response_len={len(resp)}") else: failed += 1 print(f" FAIL POST /api/profile/default/chat: empty response") else: err = body.get("error", "") if "timeout" in err.lower(): skipped += 1 print(f" SKIP POST /api/profile/default/chat: timeout (expected for slow model)") else: failed += 1 print(f" FAIL POST /api/profile/default/chat: error={err}") except subprocess.TimeoutExpired: skipped += 1 print(f" SKIP POST /api/profile/default/chat: subprocess timeout") except Exception as e: skipped += 1 print(f" SKIP POST /api/profile/default/chat: {e}") print(f"\n{passed}/{passed+failed+skipped} ι€šθΏ‡ ({skipped} skipped)") sys.exit(0 if failed == 0 else 1)