243 lines
7.7 KiB
Python
243 lines
7.7 KiB
Python
from datetime import datetime
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from open_notebook.domain.transformation import Transformation
|
|
|
|
|
|
def _client() -> TestClient:
|
|
from api.main import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
def _transformation(model_id: str | None = None) -> Transformation:
|
|
return Transformation(
|
|
id="transformation:123",
|
|
name="summary",
|
|
title="Summary",
|
|
description="Summarize the source",
|
|
prompt="Summarize this",
|
|
apply_default=False,
|
|
model_id=model_id,
|
|
created=datetime(2026, 1, 1, 12, 0, 0),
|
|
updated=datetime(2026, 1, 1, 12, 0, 0),
|
|
)
|
|
|
|
|
|
def _create_payload(model_id: str | None = None) -> dict:
|
|
payload = {
|
|
"name": "summary",
|
|
"title": "Summary",
|
|
"description": "Summarize the source",
|
|
"prompt": "Summarize this",
|
|
"apply_default": False,
|
|
}
|
|
if model_id is not None:
|
|
payload["model_id"] = model_id
|
|
return payload
|
|
|
|
|
|
def test_create_transformation_with_model_id_persists_and_reads_back():
|
|
client = _client()
|
|
saved_transformations: list[Transformation] = []
|
|
|
|
async def capture_save(transformation: Transformation):
|
|
saved_transformations.append(transformation)
|
|
transformation.id = "transformation:created"
|
|
transformation.created = datetime(2026, 1, 1, 12, 0, 0)
|
|
transformation.updated = datetime(2026, 1, 1, 12, 0, 0)
|
|
|
|
with (
|
|
patch.object(
|
|
Transformation, "save", autospec=True, side_effect=capture_save
|
|
),
|
|
patch(
|
|
"api.routers.transformations.Model.get",
|
|
new_callable=AsyncMock,
|
|
return_value=object(),
|
|
) as mock_model_get,
|
|
):
|
|
response = client.post(
|
|
"/api/transformations", json=_create_payload("model:local")
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert saved_transformations[0].model_id == "model:local"
|
|
assert response.json()["model_id"] == "model:local"
|
|
mock_model_get.assert_awaited_once_with("model:local")
|
|
|
|
with patch(
|
|
"api.routers.transformations.Transformation.get",
|
|
new_callable=AsyncMock,
|
|
return_value=saved_transformations[0],
|
|
):
|
|
response = client.get("/api/transformations/transformation:created")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["model_id"] == "model:local"
|
|
|
|
|
|
def test_execute_without_request_model_uses_stored_model():
|
|
client = _client()
|
|
transformation = _transformation(model_id="model:stored")
|
|
|
|
with (
|
|
patch(
|
|
"api.routers.transformations.Transformation.get",
|
|
new_callable=AsyncMock,
|
|
return_value=transformation,
|
|
),
|
|
patch(
|
|
"api.routers.transformations.Model.get",
|
|
new_callable=AsyncMock,
|
|
return_value=object(),
|
|
) as mock_model_get,
|
|
patch(
|
|
"api.routers.transformations.transformation_graph.ainvoke",
|
|
new_callable=AsyncMock,
|
|
return_value={"output": "Stored model output"},
|
|
) as mock_ainvoke,
|
|
):
|
|
response = client.post(
|
|
"/api/transformations/execute",
|
|
json={
|
|
"transformation_id": transformation.id,
|
|
"input_text": "Input text",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["model_id"] == "model:stored"
|
|
mock_model_get.assert_awaited_once_with("model:stored")
|
|
assert (
|
|
mock_ainvoke.call_args.kwargs["config"]["configurable"]["model_id"]
|
|
== "model:stored"
|
|
)
|
|
|
|
|
|
def test_execute_without_any_model_uses_default_transformation_model_fallback():
|
|
client = _client()
|
|
transformation = _transformation(model_id=None)
|
|
|
|
with (
|
|
patch(
|
|
"api.routers.transformations.Transformation.get",
|
|
new_callable=AsyncMock,
|
|
return_value=transformation,
|
|
),
|
|
patch(
|
|
"api.routers.transformations.Model.get",
|
|
new_callable=AsyncMock,
|
|
) as mock_model_get,
|
|
patch(
|
|
"api.routers.transformations.transformation_graph.ainvoke",
|
|
new_callable=AsyncMock,
|
|
return_value={"output": "Default model output"},
|
|
) as mock_ainvoke,
|
|
):
|
|
response = client.post(
|
|
"/api/transformations/execute",
|
|
json={
|
|
"transformation_id": transformation.id,
|
|
"input_text": "Input text",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["model_id"] is None
|
|
mock_model_get.assert_not_awaited()
|
|
assert mock_ainvoke.call_args.kwargs["config"]["configurable"]["model_id"] is None
|
|
|
|
|
|
def test_update_transformation_model_id_is_used_by_subsequent_execution():
|
|
client = _client()
|
|
transformation = _transformation(model_id="model:old")
|
|
|
|
async def save_update(transformation: Transformation):
|
|
transformation.updated = datetime(2026, 1, 1, 12, 5, 0)
|
|
|
|
with (
|
|
patch(
|
|
"api.routers.transformations.Transformation.get",
|
|
new_callable=AsyncMock,
|
|
return_value=transformation,
|
|
),
|
|
patch.object(
|
|
Transformation, "save", autospec=True, side_effect=save_update
|
|
),
|
|
patch(
|
|
"api.routers.transformations.Model.get",
|
|
new_callable=AsyncMock,
|
|
return_value=object(),
|
|
) as mock_model_get,
|
|
patch(
|
|
"api.routers.transformations.transformation_graph.ainvoke",
|
|
new_callable=AsyncMock,
|
|
return_value={"output": "Updated model output"},
|
|
) as mock_ainvoke,
|
|
):
|
|
update_response = client.put(
|
|
f"/api/transformations/{transformation.id}",
|
|
json={"model_id": "model:new"},
|
|
)
|
|
execute_response = client.post(
|
|
"/api/transformations/execute",
|
|
json={
|
|
"transformation_id": transformation.id,
|
|
"input_text": "Input text",
|
|
},
|
|
)
|
|
|
|
assert update_response.status_code == 200
|
|
assert update_response.json()["model_id"] == "model:new"
|
|
assert execute_response.status_code == 200
|
|
assert execute_response.json()["model_id"] == "model:new"
|
|
# model:new is validated once at update time and looked up again at execute.
|
|
assert mock_model_get.await_count == 2
|
|
mock_model_get.assert_awaited_with("model:new")
|
|
assert (
|
|
mock_ainvoke.call_args.kwargs["config"]["configurable"]["model_id"]
|
|
== "model:new"
|
|
)
|
|
|
|
|
|
def test_execute_request_model_overrides_stored_model():
|
|
client = _client()
|
|
transformation = _transformation(model_id="model:stored")
|
|
|
|
with (
|
|
patch(
|
|
"api.routers.transformations.Transformation.get",
|
|
new_callable=AsyncMock,
|
|
return_value=transformation,
|
|
),
|
|
patch(
|
|
"api.routers.transformations.Model.get",
|
|
new_callable=AsyncMock,
|
|
return_value=object(),
|
|
) as mock_model_get,
|
|
patch(
|
|
"api.routers.transformations.transformation_graph.ainvoke",
|
|
new_callable=AsyncMock,
|
|
return_value={"output": "Override output"},
|
|
) as mock_ainvoke,
|
|
):
|
|
response = client.post(
|
|
"/api/transformations/execute",
|
|
json={
|
|
"transformation_id": transformation.id,
|
|
"input_text": "Input text",
|
|
"model_id": "model:override",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["model_id"] == "model:override"
|
|
mock_model_get.assert_awaited_once_with("model:override")
|
|
assert (
|
|
mock_ainvoke.call_args.kwargs["config"]["configurable"]["model_id"]
|
|
== "model:override"
|
|
)
|