156 lines
6.3 KiB
Python
156 lines
6.3 KiB
Python
"""Tests for the TUI ``/model reset`` handler (NS-563).
|
|
|
|
A session-scoped ``/model`` switch pins ``session["model_override"]``, which
|
|
persists to the session DB row and is restored on resume. Before ``/model
|
|
reset`` the only escape from a stale pin was ``/new`` (losing history).
|
|
``_reset_session_model_override`` clears the pin, returns the live agent to
|
|
the config.yaml default, and persists the cleared state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from tui_gateway.server import _reset_session_model_override
|
|
|
|
|
|
def _session(**extra) -> dict:
|
|
base = {
|
|
"session_key": "tui-reset-test",
|
|
"history": [],
|
|
"agent": SimpleNamespace(provider="openrouter", model="deepseek/deepseek-v4"),
|
|
"model_override": {
|
|
"model": "deepseek/deepseek-v4",
|
|
"provider": "openrouter",
|
|
"base_url": None,
|
|
"api_mode": None,
|
|
},
|
|
}
|
|
base.update(extra)
|
|
return base
|
|
|
|
|
|
class TestResetClearsPin:
|
|
def test_override_and_one_turn_restore_removed(self) -> None:
|
|
session = _session(one_turn_model_restore={"model": "old"})
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("deepseek/deepseek-v4", "openrouter"),
|
|
), patch("tui_gateway.server._persist_live_session_runtime") as persist:
|
|
result = _reset_session_model_override("sid", session)
|
|
assert "model_override" not in session
|
|
assert "one_turn_model_restore" not in session
|
|
persist.assert_called_once_with(session)
|
|
assert result["confirm_required"] is False
|
|
assert result["scope"] == "session"
|
|
|
|
def test_config_model_seen_cleared_so_adopt_sync_reruns(self) -> None:
|
|
session = _session(config_model_seen=("some/model", "prov"))
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("deepseek/deepseek-v4", "openrouter"),
|
|
), patch("tui_gateway.server._persist_live_session_runtime"):
|
|
_reset_session_model_override("sid", session)
|
|
assert "config_model_seen" not in session
|
|
|
|
|
|
class TestResetSwitchesAgentBack:
|
|
def test_switches_when_agent_differs_from_default(self) -> None:
|
|
session = _session()
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("anthropic/claude-sonnet-4.6", "anthropic"),
|
|
), patch("tui_gateway.server._apply_model_switch") as apply_switch, patch(
|
|
"tui_gateway.server._persist_live_session_runtime"
|
|
):
|
|
result = _reset_session_model_override("sid", session)
|
|
apply_switch.assert_called_once()
|
|
args, kwargs = apply_switch.call_args
|
|
assert args[2] == "anthropic/claude-sonnet-4.6 --provider anthropic"
|
|
# Restoring the default must not create a fresh pin or write config.
|
|
assert kwargs["pin_session_override"] is False
|
|
assert kwargs["persist_override"] is False
|
|
assert result["value"] == "anthropic/claude-sonnet-4.6"
|
|
assert "cleared" in result["warning"]
|
|
|
|
def test_no_switch_when_agent_already_on_default(self) -> None:
|
|
session = _session()
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("deepseek/deepseek-v4", "openrouter"),
|
|
), patch("tui_gateway.server._apply_model_switch") as apply_switch, patch(
|
|
"tui_gateway.server._persist_live_session_runtime"
|
|
):
|
|
result = _reset_session_model_override("sid", session)
|
|
apply_switch.assert_not_called()
|
|
# The pin is still cleared and persisted even without a live switch.
|
|
assert "model_override" not in session
|
|
assert "cleared" in result["warning"]
|
|
|
|
def test_no_switch_without_agent(self) -> None:
|
|
session = _session(agent=None)
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("anthropic/claude-sonnet-4.6", "anthropic"),
|
|
), patch("tui_gateway.server._apply_model_switch") as apply_switch, patch(
|
|
"tui_gateway.server._persist_live_session_runtime"
|
|
):
|
|
_reset_session_model_override("sid", session)
|
|
apply_switch.assert_not_called()
|
|
|
|
|
|
class TestResetWithoutActivePin:
|
|
def test_reports_no_override_active(self) -> None:
|
|
session = _session()
|
|
session.pop("model_override")
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("deepseek/deepseek-v4", "openrouter"),
|
|
), patch("tui_gateway.server._persist_live_session_runtime"):
|
|
result = _reset_session_model_override("sid", session)
|
|
assert "No session model override" in result["warning"]
|
|
|
|
def test_provider_only_difference_triggers_switch(self) -> None:
|
|
session = _session(
|
|
agent=SimpleNamespace(provider="openrouter", model="deepseek/deepseek-v4")
|
|
)
|
|
with patch(
|
|
"tui_gateway.server._config_model_target",
|
|
return_value=("deepseek/deepseek-v4", "nousresearch"),
|
|
), patch("tui_gateway.server._apply_model_switch") as apply_switch, patch(
|
|
"tui_gateway.server._persist_live_session_runtime"
|
|
):
|
|
_reset_session_model_override("sid", session)
|
|
apply_switch.assert_called_once()
|
|
assert (
|
|
apply_switch.call_args[0][2]
|
|
== "deepseek/deepseek-v4 --provider nousresearch"
|
|
)
|
|
|
|
|
|
class TestResetDispatch:
|
|
"""/model reset routed through _apply_model_switch reaches the handler."""
|
|
|
|
def test_apply_model_switch_dispatches_reset(self) -> None:
|
|
session = _session()
|
|
from tui_gateway.server import _apply_model_switch
|
|
|
|
with patch(
|
|
"tui_gateway.server._reset_session_model_override",
|
|
return_value={"value": "x", "warning": "", "confirm_required": False,
|
|
"scope": "session"},
|
|
) as reset:
|
|
result = _apply_model_switch("sid", session, "reset")
|
|
reset.assert_called_once_with("sid", session)
|
|
assert result["scope"] == "session"
|
|
|
|
def test_reset_with_flags_raises_value_error(self) -> None:
|
|
import pytest
|
|
|
|
session = _session()
|
|
from tui_gateway.server import _apply_model_switch
|
|
|
|
with pytest.raises(ValueError, match="reset"):
|
|
_apply_model_switch("sid", session, "reset --global")
|