421a664336
- Remove legacy provider profiles, admin-token auth, /model command, model picker widget, and config.yaml LLM fields (design doc section 10) - Wire CLI/channels/cron and async sub-agents through the local snapshot entry; run creation rejects model config outside runtime_snapshot_id - Add periodic run-snapshot TTL cleanup to the config service lifespan - Isolate tests from the real config dir and activate the registry where run/model paths fail closed in bootstrap Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
"""Smoke tests for the custom routes mounted via langgraph.json's ``http``
|
|
field. We test the Starlette app directly — no need to spin up langgraph dev.
|
|
|
|
``GET /api/models`` and ``POST /api/runtime-snapshots`` belong to the unified
|
|
model registry API (``EvoScientist.model_registry.http_api``); their contract
|
|
tests live in ``tests/test_model_registry_http.py`` and
|
|
``tests/test_delegation_auth.py``. The legacy provider routes
|
|
(``/api/provider-profiles``, ``/api/provider-actions``, ``/api/config``,
|
|
``/api/default-model``) and the ``x-evoscientist-admin-token`` check were
|
|
removed with the unified model configuration refactor; the first test pins
|
|
down that they are gone.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
from starlette.testclient import TestClient
|
|
|
|
from EvoScientist.langgraph_dev.http import app
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("method", "path"),
|
|
[
|
|
("GET", "/api/provider-profiles"),
|
|
("PUT", "/api/provider-profiles"),
|
|
("GET", "/api/config"),
|
|
("PATCH", "/api/config"),
|
|
("POST", "/api/config"),
|
|
("POST", "/api/provider-actions"),
|
|
("PUT", "/api/default-model"),
|
|
],
|
|
)
|
|
def test_legacy_provider_routes_are_gone(method, path):
|
|
"""The removed routes must not exist — even with the old admin token."""
|
|
headers = {"X-EvoScientist-Admin-Token": "admin-secret"}
|
|
response = client.request(method, path, headers=headers, json={})
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_final_answer_extracts_latest_ai_text_blocks():
|
|
async def fake_metadata(_thread_id):
|
|
return {"updated_at": "2026-07-06T14:14:53+00:00"}
|
|
|
|
async def fake_messages(_thread_id):
|
|
return [
|
|
HumanMessage(content="question"),
|
|
AIMessage(content="old answer"),
|
|
AIMessage(
|
|
content=[
|
|
{"type": "reasoning", "text": "internal"},
|
|
{"type": "text", "text": "Part A"},
|
|
{"type": "tool_use", "name": "search"},
|
|
{"type": "output_text", "text": "Part B"},
|
|
]
|
|
),
|
|
]
|
|
|
|
async def fake_runtime(_request, _thread_id):
|
|
return {
|
|
"found": True,
|
|
"complete": True,
|
|
"completed_at": "2026-07-06T14:15:00+00:00",
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_metadata_for_http",
|
|
new=fake_metadata,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_messages_for_http",
|
|
new=fake_messages,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._read_thread_runtime_state",
|
|
new=fake_runtime,
|
|
),
|
|
):
|
|
resp = client.get("/api/threads/thread-1/final-answer")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {
|
|
"content": "Part A\n\nPart B",
|
|
"completed_at": "2026-07-06T14:15:00+00:00",
|
|
"complete": True,
|
|
}
|
|
|
|
|
|
def test_final_answer_skips_tool_selection_json_text():
|
|
async def fake_metadata(_thread_id):
|
|
return {"updated_at": "2026-07-06T14:14:53+00:00"}
|
|
|
|
async def fake_messages(_thread_id):
|
|
return [
|
|
HumanMessage(content="question"),
|
|
AIMessage(content="stable answer"),
|
|
AIMessage(
|
|
content=(
|
|
'{"tools":["search_papers","get_abstract"]}'
|
|
'{"tools":["web_search_exa"]}'
|
|
)
|
|
),
|
|
]
|
|
|
|
async def fake_runtime(_request, _thread_id):
|
|
return {
|
|
"found": True,
|
|
"complete": True,
|
|
"completed_at": "2026-07-06T14:15:00+00:00",
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_metadata_for_http",
|
|
new=fake_metadata,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_messages_for_http",
|
|
new=fake_messages,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._read_thread_runtime_state",
|
|
new=fake_runtime,
|
|
),
|
|
):
|
|
resp = client.get("/api/threads/thread-1/final-answer")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json()["content"] == "stable answer"
|
|
|
|
|
|
def test_final_answer_returns_404_for_unknown_thread():
|
|
async def fake_metadata(_thread_id):
|
|
return None
|
|
|
|
with patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_metadata_for_http",
|
|
new=fake_metadata,
|
|
):
|
|
resp = client.get("/api/threads/missing/final-answer")
|
|
|
|
assert resp.status_code == 404
|
|
assert resp.json() == {"error": "thread not found"}
|
|
|
|
|
|
def test_final_answer_does_not_mark_complete_when_runtime_state_fails():
|
|
async def fake_metadata(_thread_id):
|
|
return {"updated_at": "2026-07-06T14:14:53+00:00"}
|
|
|
|
async def fake_messages(_thread_id):
|
|
return [AIMessage(content="checkpoint answer")]
|
|
|
|
async def fake_runtime(_request, _thread_id):
|
|
raise RuntimeError("langgraph runtime unavailable")
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_metadata_for_http",
|
|
new=fake_metadata,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._get_thread_messages_for_http",
|
|
new=fake_messages,
|
|
),
|
|
patch(
|
|
"EvoScientist.langgraph_dev.http._read_thread_runtime_state",
|
|
new=fake_runtime,
|
|
),
|
|
):
|
|
resp = client.get("/api/threads/thread-1/final-answer")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {
|
|
"content": "checkpoint answer",
|
|
"completed_at": None,
|
|
"complete": False,
|
|
}
|