File size: 5,709 Bytes
4d97285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Exercise the HTTP surface against the fakes: routes, admin gate, SSE.

    python tests/test_api.py
"""

from __future__ import annotations

import json
import os
import sys
import threading
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from tests.fake_servers import LLM_PORT, MCP_PORT, start_background  # noqa: E402

ADMIN = "admin-secret"
os.environ.update(
    {
        "LLM_BASE_URL": f"http://127.0.0.1:{LLM_PORT}/v1",
        "LLM_MODEL": "fake-model",
        "LLM_API_KEY": "test-key",
        "VFRPLAN_MCP_URL": f"http://127.0.0.1:{MCP_PORT}/mcp",
        "VFRPLAN_MCP_TOKEN": "",
        "APP_ADMIN_TOKEN": ADMIN,
    }
)

import httpx  # noqa: E402
import uvicorn  # noqa: E402

from app.main import app  # noqa: E402

APP_PORT = 8933
BASE = f"http://127.0.0.1:{APP_PORT}"

results: list[tuple[bool, str]] = []


def check(label: str, condition: bool, detail: str = "") -> None:
    results.append((condition, label))
    print(f"  [{'PASS' if condition else 'FAIL'}] {label}" + (f" — {detail}" if detail else ""))


def main() -> int:
    start_background()
    threading.Thread(
        target=lambda: uvicorn.run(app, host="127.0.0.1", port=APP_PORT, log_level="error"),
        daemon=True,
    ).start()
    time.sleep(2.5)

    with httpx.Client(timeout=60) as client:
        r = client.get(f"{BASE}/")
        check("GET / serves the UI", r.status_code == 200 and "Planificador VFR" in r.text)

        for asset in ("/static/app.js", "/static/styles.css"):
            r = client.get(f"{BASE}{asset}")
            check(f"GET {asset}", r.status_code == 200 and len(r.text) > 500)

        r = client.get(f"{BASE}/health")
        check("GET /health reports configured", r.json().get("llm_configured") is True, r.text[:100])

        r = client.get(f"{BASE}/api/tools")
        body = r.json()
        check("GET /api/tools lists MCP tools", body.get("ok") and body.get("count") == 2, str(body)[:120])

        r = client.get(f"{BASE}/api/model/status")
        body = r.json()
        check("GET /api/model/status reachable", body.get("state") == "running", str(body)[:140])

        # --- admin gate ---
        r = client.get(f"{BASE}/api/config")
        check("GET /api/config without token is 401", r.status_code == 401)

        r = client.get(f"{BASE}/api/config", headers={"X-Admin-Token": "wrong"})
        check("GET /api/config with wrong token is 401", r.status_code == 401)

        r = client.get(f"{BASE}/api/config", headers={"X-Admin-Token": ADMIN})
        body = r.json()
        masked = body["llm"]["api_key_masked"]
        check("GET /api/config with token returns config", r.status_code == 200)
        check("API key is masked, never returned raw", "test-key" not in json.dumps(body), masked)

        r = client.post(f"{BASE}/api/config", json={"llm": {"model": "x"}})
        check("POST /api/config without token is 401", r.status_code == 401)

        r = client.post(
            f"{BASE}/api/config",
            json={"llm": {"model": "changed-model", "api_key": ""}},
            headers={"X-Admin-Token": ADMIN},
        )
        check("POST /api/config applies override", r.json().get("model") == "changed-model", r.text[:120])

        r = client.get(f"{BASE}/api/config", headers={"X-Admin-Token": ADMIN})
        check(
            "blank api_key left the existing key untouched",
            r.json()["llm"]["has_api_key"] is True,
        )

        # restore, or the chat below would ask the fake LLM for a model it has
        # no opinion about (harmless here, but the test should be honest)
        client.post(
            f"{BASE}/api/config",
            json={"llm": {"model": "fake-model"}},
            headers={"X-Admin-Token": ADMIN},
        )

        # --- activation ---
        r = client.post(f"{BASE}/api/model/activate")
        check("POST /api/model/activate without token is 401", r.status_code == 401)

        r = client.post(f"{BASE}/api/model/activate", headers={"X-Admin-Token": ADMIN})
        body = r.json()
        check(
            "activate on an unmanaged endpoint probes instead of resuming",
            r.status_code == 200 and body.get("managed") is False and body.get("ok") is True,
            str(body)[:160],
        )

        # --- chat SSE ---
        events: list[dict] = []
        with client.stream(
            "POST", f"{BASE}/api/chat", json={"message": "METAR at LEBL?", "history": []}
        ) as response:
            check("POST /api/chat streams", response.status_code == 200)
            for line in response.iter_lines():
                if line.startswith("data: "):
                    events.append(json.loads(line[6:]))

        kinds = [e["type"] for e in events]
        check("stream includes tools_loaded", "tools_loaded" in kinds, str(kinds))
        check("stream includes reasoning", "reasoning" in kinds)
        check("stream includes tool_call", "tool_call" in kinds)
        check("stream includes tool_result", "tool_result" in kinds)
        check("stream includes final", "final" in kinds)
        check("stream terminates with done", kinds[-1] == "done", str(kinds[-3:]))
        check("no error events", "error" not in kinds, str([e for e in events if e["type"] == "error"])[:200])

        r = client.post(f"{BASE}/api/chat", json={"message": "  "})
        check("empty message is rejected", r.status_code == 400)

    print("-" * 60)
    failed = [label for ok, label in results if not ok]
    print(f"  {len(results) - len(failed)}/{len(results)} passed")
    for label in failed:
        print(f"  FAILED: {label}")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())