test_s1_agents.py 6.41 KB
#!/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)