Files
agent-desktop/tests/integration/test_local_models.py
T

176 lines
5.7 KiB
Python

# -*- coding: utf-8 -*-
"""Integration tests for the local-model (llama.cpp) router."""
from __future__ import annotations
import pytest
from helpers import default_http_timeout
_LOCAL_MODELS_HTTP_TIMEOUT = default_http_timeout(15.0)
@pytest.mark.integration
@pytest.mark.p1
def test_local_models_server_status_returns_contract(app_server) -> None:
"""Test purpose:
- Verify GET /api/local-models/server returns the ServerStatus
contract: the three boolean fields (available / installable /
installed) are always present so the Console can render the
local-model dashboard regardless of whether llama.cpp is installed.
Test flow:
1. GET /api/local-models/server.
2. Assert 200 and the response contains the three boolean keys with
boolean values.
API endpoints:
- GET /api/local-models/server
"""
resp = app_server.api_request(
"GET",
"/api/local-models/server",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert resp.status_code == 200, app_server.logs_tail()
payload = resp.json()
for key in ("available", "installable", "installed"):
assert key in payload, f"missing key: {key}"
assert isinstance(payload[key], bool)
@pytest.mark.integration
@pytest.mark.p1
def test_local_models_models_list_returns_array(app_server) -> None:
"""Test purpose:
- Verify GET /api/local-models/models returns an array of models
(recommended + downloaded). Console populates the local-model
picker from this list; a regression hides every local model.
Test flow:
1. GET /api/local-models/models.
2. Assert 200 and the body is a list (may be empty in environments
where neither recommendations nor downloaded models exist).
API endpoints:
- GET /api/local-models/models
"""
resp = app_server.api_request(
"GET",
"/api/local-models/models",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert resp.status_code == 200, app_server.logs_tail()
assert isinstance(resp.json(), list)
@pytest.mark.integration
@pytest.mark.p2
def test_local_models_delete_unknown_model_returns_404(app_server) -> None:
"""Test purpose:
- Verify DELETE /api/local-models/models/{model_id:path} returns 404
with a descriptive detail when the model has never been downloaded,
so Console can surface a clear error to the user.
Test flow:
1. DELETE /api/local-models/models/<unknown-id>.
2. Assert 404 status and a non-empty detail field.
API endpoints:
- DELETE /api/local-models/models/{model_id:path}
"""
unknown_id = "integ-unknown-model-xyz"
resp = app_server.api_request(
"DELETE",
f"/api/local-models/models/{unknown_id}",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert resp.status_code == 404, app_server.logs_tail()
assert isinstance(resp.json().get("detail"), str) and resp.json()["detail"]
# ------------------------------------------------------------------ #
# Sprint 3.1-D additions
# ------------------------------------------------------------------ #
@pytest.mark.integration
@pytest.mark.p2
def test_local_models_config_roundtrip(app_server) -> None:
"""Test purpose:
- Verify PUT /api/local-models/config persists max_context_length and
GET returns the updated value.
Test flow:
1. GET baseline config.
2. PUT a new max_context_length (must be >= 32768 per validator).
3. GET and assert the new value is returned.
4. Restore baseline.
API endpoints:
- GET /api/local-models/config
- PUT /api/local-models/config
"""
get_resp = app_server.api_request(
"GET",
"/api/local-models/config",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert get_resp.status_code == 200, app_server.logs_tail()
baseline = get_resp.json()
assert isinstance(baseline, dict), baseline
original_ctx = baseline.get("max_context_length")
new_ctx = 65536 if original_ctx != 65536 else 98304
try:
put_resp = app_server.api_request(
"PUT",
"/api/local-models/config",
json={"max_context_length": new_ctx},
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert put_resp.status_code == 200, app_server.logs_tail()
assert put_resp.json().get("status") == "ok", put_resp.json()
get_after = app_server.api_request(
"GET",
"/api/local-models/config",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
body = get_after.json()
assert (
body.get("max_context_length") == new_ctx
), f"max_context_length not persisted: {body}"
finally:
if original_ctx is not None:
app_server.api_request(
"PUT",
"/api/local-models/config",
json={"max_context_length": original_ctx},
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
@pytest.mark.integration
@pytest.mark.p2
def test_local_models_server_update_info_contract(app_server) -> None:
"""Test purpose:
- Verify GET /api/local-models/server/update returns a structured
response so Console can decide whether to prompt the user about
a server upgrade. Endpoint must respond 200 even when no update is
pending.
Test flow:
1. GET /api/local-models/server/update.
2. Assert 200 and the response is a dict.
API endpoints:
- GET /api/local-models/server/update
"""
resp = app_server.api_request(
"GET",
"/api/local-models/server/update",
timeout=_LOCAL_MODELS_HTTP_TIMEOUT,
)
assert resp.status_code == 200, app_server.logs_tail()
body = resp.json()
assert isinstance(body, dict), body