Files
Octop/tests/unit/api/test_chat_serialize.py
53fab69f4f feat: add a decision model for skills, tools, and teams (#2098)
* feat: add a decision model for skills, tools, and teams

Let a small model shortlist what the chat model sees, including a local Laya install, and show the result on the turn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: stop Laya through the POSIX-safe kill helper

Windows mypy does not see os.killpg, so process-group shutdown goes through the shared helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: compare the Laya install command without a POSIX path

Windows quotes the interpreter, so the displayed command is checked against the same argv list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: show Laya readiness and wait for local decisions

Pin the local-CPU timeout fix and distinguish a ready Laya service from the service currently enabled for decisions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: save each decision service and pick the default on the card

Clicking a card selects the default. Endpoint and key stay in the drawer
so Jev, Laya, and custom can be kept at once.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 16:42:26 +08:00

110 lines
3.8 KiB
Python

"""Unit tests for chat SSE / WebSocket chunk serialization."""
from __future__ import annotations
import json
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from octop.api.routers.chat.serialize import _serialize_history_message
from octop.api.routers.chat.sse import json_chunk_default
def test_json_chunk_default_serializes_langchain_messages() -> None:
chunk = {
"type": "state_update",
"node": "BootstrapMiddleware.before_agent",
"data": {
"messages": [
SystemMessage(content="bootstrap"),
HumanMessage(content="hi"),
AIMessage(content="hello"),
],
},
}
payload = json.dumps(chunk, default=json_chunk_default)
parsed = json.loads(payload)
assert parsed["type"] == "state_update"
assert len(parsed["data"]["messages"]) == 3
assert parsed["data"]["messages"][0]["content"] == "bootstrap"
def test_json_chunk_default_keeps_tool_message_artifact() -> None:
"""Live WS/SSE frames must carry the offloaded octop_ui payload."""
msg = ToolMessage(
content='{"octop_ui": {"renderer": "bilibili_player"}, "data_ref": "artifact"}',
tool_call_id="call_1",
name="bilibili_search_anime",
artifact={"results": [{"season_id": 1, "episodes": [{"index": 1}]}]},
)
payload = json.dumps({"type": "tool_result", "messages": [msg]}, default=json_chunk_default)
parsed = json.loads(payload)
wire_msg = parsed["messages"][0]
assert wire_msg["artifact"]["results"][0]["season_id"] == 1
assert "episodes" not in wire_msg["content"]
def test_serialize_history_message_includes_tool_artifact() -> None:
msg = ToolMessage(
content='{"octop_ui": {"renderer": "bilibili_player"}, "data_ref": "artifact"}',
tool_call_id="call_1",
name="bilibili_search_anime",
artifact={"results": [{"season_id": 1}]},
)
entry = _serialize_history_message(msg)
assert entry is not None
block = entry["content"][0]
assert block["type"] == "tool_result"
assert block["artifact"]["results"][0]["season_id"] == 1
def test_serialize_history_message_includes_decision_savings() -> None:
msg = AIMessage(
content="done",
usage_metadata={"input_tokens": 100, "output_tokens": 4, "total_tokens": 104},
additional_kwargs={"context_usage": {"input_tokens": 100, "decision_saved": 5972}},
)
entry = _serialize_history_message(msg)
assert entry is not None
assert entry["usage"]["decision_saved"] == 5972
def test_serialize_history_message_keeps_decision_savings_without_provider_usage() -> None:
msg = AIMessage(
content="done",
additional_kwargs={"context_usage": {"decision_saved": 5972}},
)
entry = _serialize_history_message(msg)
assert entry is not None
assert entry["usage"] == {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"decision_saved": 5972,
}
def test_serialize_history_message_includes_decision() -> None:
msg = AIMessage(
content="done",
additional_kwargs={
"octop_decision": {
"kind": "decision",
"model": "typesafe/jev-1.13",
"empty": False,
"parts": [{"kind": "skill", "kept": [{"name": "pdf", "score": 0.9}]}],
}
},
)
entry = _serialize_history_message(msg)
assert entry is not None
assert entry["decision"]["model"] == "typesafe/jev-1.13"
assert entry["decision"]["parts"][0]["kept"][0]["name"] == "pdf"
def test_serialize_history_message_omits_artifact_key_when_absent() -> None:
msg = ToolMessage(content="found", tool_call_id="call_1", name="search")
entry = _serialize_history_message(msg)
assert entry is not None
assert "artifact" not in entry["content"][0]