Forráskód Böngészése

feat(M2): Phase I — AgentPack 端到端 dogfood 测试 + 手动脚本

tests/test_agentpack_e2e.py (11 new tests, 1 skipped):
  - test_install_loopback_blocked       — 非 loopback 调用 /install → 404
  - test_install_builtin_pack_via_api   — 预安装 pack 可通过 HTTP list 看到
  - test_installed_pack_appears_in_list — GET /agentpacks → count/id 正确
  - test_get_installed_pack_detail      — GET /agentpacks/{id} → 字段校验
  - test_create_agent_from_installed_pack → 201, agent_id 正确
  - test_created_agent_has_correct_config → agent_dir=pack.path, _config_dir=pack.path
  - test_agent_config_compiles_with_from_config → from_config() 不报错(关键集成缝)
  - test_agent_appears_in_agents_list   — GET /agents 可见新 agent
  - test_uninstall_pack_does_not_delete_agents — 卸载 pack 不级联删 agent
  - test_get_unknown_pack_returns_404
  - test_create_agent_from_uninstalled_pack_returns_404
  - test_run_agent_produces_review_report [SKIP: 需真实 LLM]

scripts/dogfood_agentpack.py:
  手动 E2E 脚本(需运行 server + ANTHROPIC_API_KEY):
  安装 pack → 列表 → 创建 agent → 运行 → 验证 workspace/review_report.md

全套: 189 passed, 1 skipped, 8 pre-existing failures (test_authenticate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kenny67nju 3 hónapja
szülő
commit
67beab7
2 módosított fájl, 617 hozzáadás és 0 törlés
  1. 268 0
      scripts/dogfood_agentpack.py
  2. 349 0
      tests/test_agentpack_e2e.py

+ 268 - 0
scripts/dogfood_agentpack.py

@@ -0,0 +1,268 @@
+#!/usr/bin/env python3
+"""
+Phase I dogfood — AgentPack end-to-end script with real LLM.
+
+Usage:
+    # Start the server in another terminal first:
+    AGENTPAAS_DEPLOYMENT_MODE=desktop uvicorn agentpaas.api.app:app --port 8000
+
+    # Run the dogfood script:
+    python scripts/dogfood_agentpack.py
+
+    # Or point at a non-default host/key:
+    python scripts/dogfood_agentpack.py --url http://127.0.0.1:8000 --key ap_xxxx
+
+The script:
+  1. Zips the built-in research.top-journal-reviewer pack from the repo
+  2. Installs it via POST /api/v1/agentpacks/install
+  3. Creates an agent via POST /api/v1/agentpacks/{id}/create-agent
+  4. Runs the agent with a short test abstract
+  5. Prints the workspace path and verifies review_report.md exists
+
+Requirements:
+  - Server running with ANTHROPIC_API_KEY or DASHSCOPE_API_KEY set
+  - pip install requests (standard, usually present)
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import zipfile
+from pathlib import Path
+
+
+# ── Config ────────────────────────────────────────────────────────────────────
+
+REVIEWER_PACK_ID = "research.top-journal-reviewer"
+
+TEST_ABSTRACT = """\
+Title: Quantum Speedup for Dense Matrix Multiplication via Phase Estimation.
+
+Abstract: We present a quantum algorithm that computes dense n×n matrix products
+in O(n^{1.5} log n) time using quantum phase estimation and amplitude amplification,
+outperforming the classical Strassen barrier O(n^{2.37}). We provide a gate-level
+circuit of depth O(n^{0.75} polylog n) with error probability δ < 0.01. Experiments
+on 16×16 matrices on a simulated 40-qubit processor confirm a 3.2× speedup.
+
+请审阅这篇论文的核心贡献声明、技术正确性和实验充分性,生成完整的审稿报告。
+"""
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+def _print(label: str, msg: str = "", ok: bool = True) -> None:
+    status = "✓" if ok else "✗"
+    color = "\033[32m" if ok else "\033[31m"
+    reset = "\033[0m"
+    print(f"  {color}{status}{reset}  {label}: {msg}")
+
+
+def _zip_pack(repo_root: Path, pack_id: str, dest_dir: Path) -> str:
+    pack_dir = repo_root / "agentexample" / "agentpacks" / pack_id
+    if not pack_dir.is_dir():
+        sys.exit(f"Pack not found on disk: {pack_dir}")
+    zip_path = str(dest_dir / f"{pack_id}.zip")
+    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
+        for p in pack_dir.rglob("*"):
+            if p.is_file():
+                zf.write(p, p.relative_to(pack_dir))
+    return zip_path
+
+
+def _read_api_key_from_config() -> str:
+    """Read the auto-bootstrapped API key from ~/.agentpaas/config.json."""
+    cfg_path = Path.home() / ".agentpaas" / "config.json"
+    try:
+        with open(cfg_path) as f:
+            return (json.load(f) or {}).get("api_key", "")
+    except Exception:
+        return ""
+
+
+# ── Main flow ─────────────────────────────────────────────────────────────────
+
+def run_dogfood(base_url: str, api_key: str, verbose: bool = False) -> bool:
+    try:
+        import requests
+    except ImportError:
+        sys.exit("Missing dependency: pip install requests")
+
+    session = requests.Session()
+    session.headers.update({"Authorization": f"Bearer {api_key}"})
+
+    repo_root = Path(__file__).parent.parent.resolve()
+    ok = True
+
+    print(f"\n{'─'*60}")
+    print(f"  AgentPack Dogfood  —  {base_url}")
+    print(f"{'─'*60}")
+
+    # Step 0: health check
+    print("\n[0] 服务健康检查")
+    try:
+        r = session.get(f"{base_url}/health", timeout=5)
+        r.raise_for_status()
+        _print("health", r.json().get("status", "?"))
+    except Exception as e:
+        _print("health", str(e), ok=False)
+        print("\n  服务未响应。请先启动:\n  uvicorn agentpaas.api.app:app --port 8000\n")
+        return False
+
+    # Step 1: zip + install pack
+    print(f"\n[1] 安装内置 Pack: {REVIEWER_PACK_ID}")
+    with tempfile.TemporaryDirectory() as td:
+        zip_path = _zip_pack(repo_root, REVIEWER_PACK_ID, Path(td))
+        _print("zip", f"{Path(zip_path).stat().st_size // 1024} KB")
+
+        # install is loopback-only — works when server is on localhost
+        r = session.post(
+            f"{base_url}/api/v1/agentpacks/install",
+            json={"zip_path": zip_path},
+            timeout=30,
+        )
+        if r.status_code == 200:
+            pack = r.json()["installed"]
+            _print("install", f"{pack['id']} v{pack['version']}")
+        elif r.status_code == 400 and "already installed" in r.text.lower():
+            _print("install", "已安装,跳过")
+        else:
+            _print("install", f"HTTP {r.status_code}: {r.text[:200]}", ok=False)
+            ok = False
+
+    # Step 2: verify pack is listed
+    print(f"\n[2] 列出已安装 packs")
+    r = session.get(f"{base_url}/api/v1/agentpacks", timeout=10)
+    packs = r.json().get("agentpacks", [])
+    found = any(p["id"] == REVIEWER_PACK_ID for p in packs)
+    _print("list", f"{len(packs)} pack(s),reviewer={'✓' if found else '✗'}", ok=found)
+    if not found:
+        ok = False
+
+    # Step 3: create agent from pack
+    print(f"\n[3] 从 Pack 创建智能体")
+    r = session.post(
+        f"{base_url}/api/v1/agentpacks/{REVIEWER_PACK_ID}/create-agent",
+        json={"name": "Dogfood 审稿助手", "description": "Phase I dogfood"},
+        timeout=15,
+    )
+    if r.status_code == 201:
+        body = r.json()
+        agent_id = body["agent_id"]
+        _print("create-agent", f"id={agent_id}")
+    else:
+        _print("create-agent", f"HTTP {r.status_code}: {r.text[:200]}", ok=False)
+        ok = False
+        return ok
+
+    # Step 4: verify agent in list
+    print(f"\n[4] 验证智能体出现在 /agents")
+    r = session.get(f"{base_url}/api/v1/agents", timeout=10)
+    agent_ids = [a["id"] for a in r.json().get("agents", [])]
+    found = agent_id in agent_ids
+    _print("agents list", f"agent_id in list: {'✓' if found else '✗'}", ok=found)
+    if not found:
+        ok = False
+
+    # Step 5: verify config._config_dir
+    print(f"\n[5] 验证 agent config._config_dir")
+    r = session.get(f"{base_url}/api/v1/agents/{agent_id}", timeout=10)
+    agent = r.json()
+    config_dir = agent.get("config", {}).get("_config_dir", "")
+    agent_dir = agent.get("agent_dir", "")
+    match = config_dir == agent_dir
+    _print("_config_dir", f"{'matches agent_dir ✓' if match else f'MISMATCH: {config_dir!r} != {agent_dir!r}'}", ok=match)
+    if not match:
+        ok = False
+
+    # Step 6: run agent
+    print(f"\n[6] 运行审稿智能体(LLM 调用,可能需要 1-3 分钟)")
+    t0 = time.time()
+    r = session.post(
+        f"{base_url}/api/v1/agents/{agent_id}/run",
+        json={"input": TEST_ABSTRACT},
+        timeout=300,
+    )
+    elapsed = int(time.time() - t0)
+
+    if r.status_code != 200:
+        _print("run", f"HTTP {r.status_code}: {r.text[:300]}", ok=False)
+        ok = False
+        return ok
+
+    run_body = r.json()
+    workspace = run_body.get("workspace_path", "")
+    _print("run", f"完成 ({elapsed}s), status={run_body.get('status', '?')}")
+    _print("workspace", workspace or "(未设置)")
+
+    # Step 7: verify workspace files
+    print(f"\n[7] 验证 workspace 产出物")
+    if not workspace:
+        _print("workspace_path", "run response 中未包含", ok=False)
+        ok = False
+    else:
+        for fname in ("review_report.md", "review_result.json"):
+            fpath = os.path.join(workspace, fname)
+            exists = os.path.isfile(fpath)
+            size = os.path.getsize(fpath) if exists else 0
+            _print(fname, f"{'存在' if exists else '不存在'} ({size} bytes)", ok=exists)
+            if not exists:
+                ok = False
+
+        if verbose and workspace and os.path.isfile(os.path.join(workspace, "review_report.md")):
+            print("\n  ── review_report.md (前 20 行) ──")
+            with open(os.path.join(workspace, "review_report.md"), encoding="utf-8") as f:
+                lines = f.readlines()[:20]
+            for line in lines:
+                print("  ", line, end="")
+
+    # Step 8: cleanup
+    print(f"\n[8] 清理(删除 dogfood agent)")
+    r = session.delete(f"{base_url}/api/v1/agents/{agent_id}", timeout=10)
+    _print("delete agent", f"HTTP {r.status_code}")
+
+    print(f"\n{'─'*60}")
+    result_str = "PASS ✓" if ok else "FAIL ✗"
+    color = "\033[32m" if ok else "\033[31m"
+    print(f"  {color}Dogfood result: {result_str}\033[0m")
+    print(f"{'─'*60}\n")
+    return ok
+
+
+# ── CLI ───────────────────────────────────────────────────────────────────────
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="AgentPack dogfood E2E script")
+    parser.add_argument(
+        "--url",
+        default="http://127.0.0.1:8000",
+        help="Base URL of the running agentpaas server (default: http://127.0.0.1:8000)",
+    )
+    parser.add_argument(
+        "--key",
+        default="",
+        help="API key (Bearer). Reads from ~/.agentpaas/config.json if omitted.",
+    )
+    parser.add_argument(
+        "--verbose", "-v",
+        action="store_true",
+        help="Print first 20 lines of review_report.md",
+    )
+    args = parser.parse_args()
+
+    api_key = args.key or _read_api_key_from_config()
+    if not api_key:
+        sys.exit(
+            "No API key found. Pass --key ap_xxx or run the server in desktop mode "
+            "to auto-generate one at ~/.agentpaas/config.json"
+        )
+
+    success = run_dogfood(args.url, api_key, verbose=args.verbose)
+    sys.exit(0 if success else 1)
+
+
+if __name__ == "__main__":
+    main()

+ 349 - 0
tests/test_agentpack_e2e.py

@@ -0,0 +1,349 @@
+"""
+Phase I dogfood — AgentPack end-to-end integration tests (no LLM).
+
+Exercises the full HTTP path that a desktop user takes:
+
+  install built-in pack ──► list ──► get detail
+        │
+        └──► create-agent ──► GET agent (verify config)
+                                    │
+                                    └──► from_config() ──► agent compiles ✓
+        │
+        └──► uninstall pack ──► agent STILL exists (no cascade delete)
+
+NOTE on install: POST /agentpacks/install is loopback-only (127.0.0.1).
+TestClient sends requests as host "testclient", so the loopback check
+returns 404. We test the loopback gate separately (test_install_loopback_blocked)
+and use install_from_zip() directly for the pack-setup fixture — consistent
+with test_agentpack.py::test_create_agent_from_pack_endpoint. All subsequent
+API calls (list / get / create-agent / delete) go through HTTP.
+
+Real LLM execution (review_report.md verification) lives in
+scripts/dogfood_agentpack.py and is marked skip here.
+"""
+from __future__ import annotations
+
+import json
+import os
+import zipfile
+
+import pytest
+import yaml
+
+os.environ.setdefault("AGENTPAAS_DATABASE_URL", "sqlite:///:memory:")
+os.environ.setdefault("AGENTPAAS_TESTING", "1")
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+def _pack_zip_from_disk(tmp_path, pack_id: str) -> str:
+    """Zip the on-disk agentexample pack so install_from_zip() can consume it."""
+    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    pack_dir = os.path.join(repo_root, "agentexample", "agentpacks", pack_id)
+    if not os.path.isdir(pack_dir):
+        pytest.skip(f"built-in pack not found on disk: {pack_dir}")
+
+    zip_path = str(tmp_path / f"{pack_id}.zip")
+    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
+        for dirpath, _, filenames in os.walk(pack_dir):
+            for fname in filenames:
+                full = os.path.join(dirpath, fname)
+                arcname = os.path.relpath(full, pack_dir)
+                zf.write(full, arcname)
+    return zip_path
+
+
+def _install_pack(packs_dir: str, zip_path: str):
+    """Install a pack directly (bypasses loopback gate — tested separately)."""
+    from agentpaas.engine.agentpack_store import install_from_zip
+    return install_from_zip(zip_path, packs_dir)
+
+
+@pytest.fixture()
+def http_client(tmp_path, monkeypatch):
+    """TestClient + fresh in-memory DB + one tenant/user/key + reviewer pack pre-installed."""
+    import secrets
+    from fastapi.testclient import TestClient
+    from agentpaas.api.app import app
+    from agentpaas.api.middleware.auth import hash_key
+    from agentpaas.config import settings
+    from agentpaas.db.models import Database, gen_id, now_utc
+    import agentpaas.db.session as _session_mod
+
+    packs_dir = tmp_path / "agentpacks"
+    packs_dir.mkdir()
+    monkeypatch.setattr(settings, "agentpacks_dir", str(packs_dir))
+
+    prev_db = _session_mod._db
+    _session_mod._db = Database("sqlite:///:memory:")
+    db = _session_mod._db
+
+    tid = gen_id("tn_")
+    uid = gen_id("usr_")
+    raw_key = f"ap_{secrets.token_hex(16)}"
+    now = now_utc()
+    db.execute(
+        "INSERT INTO tenants (id, name, plan, status, created_at) "
+        "VALUES (?, 'test', 'free', 'active', ?)", (tid, now),
+    )
+    db.execute(
+        "INSERT INTO users (id, tenant_id, email, role, created_at) "
+        "VALUES (?, ?, '', 'admin', ?)", (uid, tid, now),
+    )
+    db.execute(
+        "INSERT INTO api_keys "
+        "(id, tenant_id, user_id, key_hash, key_prefix, name, scopes, "
+        " rate_limit, status, created_at) "
+        "VALUES (?, ?, ?, ?, ?, 'test', ?, 600, 'active', ?)",
+        (gen_id("key_"), tid, uid, hash_key(raw_key), raw_key[:8],
+         json.dumps(["agents:*", "keys:*"]), now),
+    )
+    db.commit()
+
+    # Pre-install the built-in reviewer pack directly (bypass loopback gate)
+    zip_path = _pack_zip_from_disk(tmp_path, REVIEWER_ID)
+    pack = _install_pack(str(packs_dir), zip_path)
+
+    with TestClient(app) as client:
+        yield client, raw_key, str(packs_dir), pack
+
+    _session_mod._db = prev_db
+
+
+# ── Auth helper ───────────────────────────────────────────────────────────────
+
+def _auth(key: str) -> dict:
+    return {"Authorization": f"Bearer {key}"}
+
+
+# ── Tests ─────────────────────────────────────────────────────────────────────
+
+REVIEWER_ID = "research.top-journal-reviewer"
+
+
+def test_install_loopback_blocked(tmp_path, monkeypatch):
+    """POST /agentpacks/install from non-loopback → 404 (loopback gate).
+
+    TestClient host = 'testclient', not 127.0.0.1 — this is the gate we want.
+    """
+    import secrets
+    from fastapi.testclient import TestClient
+    from agentpaas.api.app import app
+    from agentpaas.config import settings
+
+    packs_dir = tmp_path / "agentpacks"
+    packs_dir.mkdir()
+    monkeypatch.setattr(settings, "agentpacks_dir", str(packs_dir))
+
+    zip_path = _pack_zip_from_disk(tmp_path, REVIEWER_ID)
+    with TestClient(app) as client:
+        r = client.post(
+            "/api/v1/agentpacks/install",
+            json={"zip_path": zip_path},
+        )
+    assert r.status_code == 404, r.text[:200]
+
+
+def test_install_builtin_pack_via_api(tmp_path, http_client):
+    """Pre-installed reviewer pack is visible via GET /agentpacks after direct install."""
+    client, key, packs_dir, pack = http_client
+    # pack was installed in fixture — verify it via the HTTP list
+    r = client.get("/api/v1/agentpacks", headers=_auth(key))
+    assert r.status_code == 200, r.text[:300]
+    body = r.json()
+    assert body["count"] >= 1
+    ids = [p["id"] for p in body["agentpacks"]]
+    assert REVIEWER_ID in ids
+    # spot-check detail fields
+    detail = next(p for p in body["agentpacks"] if p["id"] == REVIEWER_ID)
+    assert detail["version"] == "0.1.0"
+    assert detail["domain"] == "research"
+    assert detail["permissions"]["shell"] is False
+    assert detail["permissions"]["network"] is False
+
+
+def test_installed_pack_appears_in_list(tmp_path, http_client):
+    """GET /agentpacks → pre-installed pack shows up."""
+    client, key, packs_dir, pack = http_client
+    r = client.get("/api/v1/agentpacks", headers=_auth(key))
+    assert r.status_code == 200, r.text
+    body = r.json()
+    assert body["count"] >= 1
+    ids = [p["id"] for p in body["agentpacks"]]
+    assert REVIEWER_ID in ids
+
+
+def test_get_installed_pack_detail(tmp_path, http_client):
+    """GET /agentpacks/{id} → correct detail including permission_summary."""
+    client, key, packs_dir, pack = http_client
+    r = client.get(f"/api/v1/agentpacks/{REVIEWER_ID}", headers=_auth(key))
+    assert r.status_code == 200, r.text
+    p = r.json()
+    assert p["id"] == REVIEWER_ID
+    assert len(p["permission_summary"]) > 0
+    assert "professor" in p["audience"] or "phd_student" in p["audience"]
+
+
+def test_create_agent_from_installed_pack(tmp_path, http_client):
+    """POST create-agent → 201, agent_id returned."""
+    client, key, packs_dir, pack = http_client
+    r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "我的审稿助手", "description": "dogfood test"},
+        headers=_auth(key),
+    )
+    assert r.status_code == 201, r.text[:300]
+    body = r.json()
+    assert body["ok"] is True
+    assert body["agent_id"].startswith("ag_")
+    assert body["pack"]["id"] == REVIEWER_ID
+
+
+def test_created_agent_has_correct_config(tmp_path, http_client):
+    """GET /agents/{id} → agent_dir=pack.path, _config_dir correct."""
+    client, key, packs_dir, pack = http_client
+    create_r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "审稿助手"},
+        headers=_auth(key),
+    )
+    agent_id = create_r.json()["agent_id"]
+
+    r = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key))
+    assert r.status_code == 200, r.text
+    agent = r.json()
+
+    assert agent["agent_dir"] == pack.path
+    assert agent["agent_template"] == REVIEWER_ID
+
+    config = agent["config"]
+    # _config_dir must equal pack.path so sub-agent refs resolve correctly
+    assert config.get("_config_dir") == pack.path
+
+
+def test_agent_config_compiles_with_from_config(tmp_path, http_client):
+    """Critical seam: config stored in DB → from_config() → agent term.
+
+    Mirrors what _execute_agent does (dump to tmp YAML, compile).
+    If this passes, the agent can actually run (LLM aside).
+    """
+    import tempfile
+    from lambdagent.fromconfig import from_config as lambdagent_from_config
+
+    client, key, packs_dir, pack = http_client
+    create_r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "审稿助手"},
+        headers=_auth(key),
+    )
+    agent_id = create_r.json()["agent_id"]
+    config = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key)).json()["config"]
+
+    with tempfile.NamedTemporaryFile(
+        mode="w", suffix=".yml", delete=False, encoding="utf-8"
+    ) as f:
+        yaml.dump(config, f, allow_unicode=True)
+        tmp_yml = f.name
+    try:
+        term = lambdagent_from_config(tmp_yml)
+        assert term is not None
+    finally:
+        os.unlink(tmp_yml)
+
+
+def test_agent_appears_in_agents_list(tmp_path, http_client):
+    """GET /agents after create-agent → new agent visible."""
+    client, key, packs_dir, pack = http_client
+    create_r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "审稿助手"},
+        headers=_auth(key),
+    )
+    agent_id = create_r.json()["agent_id"]
+
+    r = client.get("/api/v1/agents", headers=_auth(key))
+    assert r.status_code == 200
+    agent_ids = [a["id"] for a in r.json()["agents"]]
+    assert agent_id in agent_ids
+
+
+def test_uninstall_pack_does_not_delete_agents(tmp_path, http_client):
+    """DELETE /agentpacks/{id} removes the pack; derived agent persists.
+
+    Agents are data, not derived assets. Deleting the template pack MUST NOT
+    cascade-delete agents (they may have run history and workspace artifacts).
+    """
+    client, key, packs_dir, pack = http_client
+    create_r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "审稿助手"},
+        headers=_auth(key),
+    )
+    agent_id = create_r.json()["agent_id"]
+
+    del_r = client.delete(f"/api/v1/agentpacks/{REVIEWER_ID}", headers=_auth(key))
+    assert del_r.status_code == 200
+    assert del_r.json()["ok"] is True
+
+    # Pack is gone
+    list_r = client.get("/api/v1/agentpacks", headers=_auth(key))
+    assert list_r.json()["count"] == 0
+
+    # Agent STILL exists
+    agent_r = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key))
+    assert agent_r.status_code == 200
+    assert agent_r.json()["name"] == "审稿助手"
+
+
+def test_get_unknown_pack_returns_404(tmp_path, http_client):
+    """GET /agentpacks/does.not.exist → 404."""
+    client, key, packs_dir, pack = http_client
+    r = client.get("/api/v1/agentpacks/does.not.exist", headers=_auth(key))
+    assert r.status_code == 404
+
+
+def test_create_agent_from_uninstalled_pack_returns_404(tmp_path, http_client):
+    """POST create-agent for a pack that was never installed → 404."""
+    client, key, packs_dir, pack = http_client
+    r = client.post(
+        "/api/v1/agentpacks/does.not.exist/create-agent",
+        json={"name": "x"},
+        headers=_auth(key),
+    )
+    assert r.status_code == 404
+
+
+@pytest.mark.skip(reason="requires real LLM (ANTHROPIC_API_KEY). Run: python scripts/dogfood_agentpack.py")
+def test_run_agent_produces_review_report(tmp_path, http_client):
+    """Smoke test: run reviewer pack agent with a short abstract.
+
+    Skipped in CI — needs a live LLM. Use scripts/dogfood_agentpack.py for the
+    full end-to-end dogfood (workspace/review_report.md verification).
+    """
+    client, key, packs_dir, pack = http_client
+    create_r = client.post(
+        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
+        json={"name": "审稿助手"},
+        headers=_auth(key),
+    )
+    agent_id = create_r.json()["agent_id"]
+
+    abstract = (
+        "Title: Quantum Speedup for Matrix Multiplication.\n"
+        "Abstract: We present a quantum algorithm achieving O(n^{1.5}) "
+        "matrix multiplication, improving on the classical Strassen bound O(n^{2.37}).\n"
+        "Verify this claim and produce a review report."
+    )
+    run_r = client.post(
+        f"/api/v1/agents/{agent_id}/run",
+        json={"input": abstract},
+        headers=_auth(key),
+        timeout=300,
+    )
+    assert run_r.status_code == 200, run_r.text[:500]
+    body = run_r.json()
+    workspace = body.get("workspace_path", "")
+    assert workspace, "workspace_path missing from response"
+    assert os.path.isfile(os.path.join(workspace, "review_report.md")), \
+        f"review_report.md not found in {workspace}"
+    assert os.path.isfile(os.path.join(workspace, "review_result.json")), \
+        f"review_result.json not found in {workspace}"