unsloth/studio/backend/tests/test_chat_history_routes.py
Daniel Han 3ce187da02
Formatting: ruff line-length 100, kwarg-spacing passes, drop blank after short local imports (#6079)
Raise ruff line-length to 100 and extend the local pre-commit format pipeline (def-signature magic-comma normalization, short multi-line assert collapse, kwarg '=' spacing, blank-line-after-short-import removal, adjacent string-literal / f-string+plain merge, redundant-pass pruning). Every transform re-checks the file AST and is dropped if it would differ; the whole-repo reformat is verified AST-identical per file and idempotent.
2026-06-08 04:24:13 -07:00

171 lines
5.5 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import asyncio
import os
import re
import sys
import pytest
from fastapi import HTTPException
_backend = os.path.join(os.path.dirname(__file__), "..")
sys.path.insert(0, _backend)
from routes import chat_history
def _message(message_id: str, thread_id: str) -> chat_history.ChatMessage:
return chat_history.ChatMessage(
id = message_id,
threadId = thread_id,
parentId = None,
role = "user",
content = [{"type": "text", "text": "hello"}],
createdAt = 1_700_000_000_000,
)
def test_replace_thread_messages_rejects_body_thread_mismatch(monkeypatch):
called = False
def fake_get_chat_thread(thread_id: str):
return {"id": thread_id}
def fake_sync_chat_messages(*args, **kwargs):
nonlocal called
called = True
return []
monkeypatch.setattr(chat_history, "get_chat_thread", fake_get_chat_thread)
monkeypatch.setattr(chat_history, "sync_chat_messages", fake_sync_chat_messages)
with pytest.raises(HTTPException) as exc_info:
asyncio.run(
chat_history.replace_thread_messages(
"thread-1",
chat_history.ChatMessageSyncRequest(
messages = [_message("msg-1", "thread-2")],
pruneMissing = True,
),
current_subject = "test-user",
)
)
assert exc_info.value.status_code == 400
assert "Message threadId mismatch" in str(exc_info.value.detail)
assert called is False
# ---------------------------------------------------------------------------
# /api/chat/settings
# ---------------------------------------------------------------------------
def test_chat_settings_payload_accepts_fast_mode_presets():
payload = chat_history.ChatSettingsPayload.model_validate(
{
"inferenceParams": {"fastMode": False},
"customPresets": [
{
"name": "Fast Opus",
"params": {
"temperature": 0.6,
"topP": 0.95,
"topK": 20,
"minP": 0.01,
"repetitionPenalty": 1.0,
"presencePenalty": 0.0,
"maxTokens": 8192,
"systemPrompt": "",
"trustRemoteCode": False,
"fastMode": True,
},
},
],
}
)
dumped = payload.model_dump(exclude_unset = True)
assert dumped["inferenceParams"]["fastMode"] is False
assert dumped["customPresets"][0]["params"]["fastMode"] is True
def test_chat_inference_settings_covers_frontend_persisted_fields():
# Drift guard: every InferenceParams field the UI persists (all but
# checkpoint) must exist on ChatInferenceSettings, else extra="forbid"
# 400s PUT /api/chat/settings on the next added field (issue #5862).
runtime_ts = os.path.join(
_backend,
"..",
"frontend",
"src",
"features",
"chat",
"types",
"runtime.ts",
)
if not os.path.exists(runtime_ts):
pytest.skip("frontend runtime.ts not present")
with open(runtime_ts, encoding = "utf-8") as fh:
block = re.search(r"interface InferenceParams \{(.*?)\n\}", fh.read(), re.DOTALL)
assert block, "InferenceParams interface not found in runtime.ts"
persisted = set(re.findall(r"^\s*(\w+)\??:", block.group(1), re.M)) - {"checkpoint"}
backend = set(chat_history.ChatInferenceSettings.model_fields)
assert persisted == backend, (
f"schema drift: frontend-only {persisted - backend}, " f"backend-only {backend - persisted}"
)
# ---------------------------------------------------------------------------
# /api/chat/import-ledger
# ---------------------------------------------------------------------------
def test_get_import_ledger_round_trips_through_storage(monkeypatch):
seen: list[str] = []
def fake_list():
return list(seen)
monkeypatch.setattr(chat_history, "list_chat_legacy_imports", fake_list)
response = asyncio.run(chat_history.get_import_ledger(current_subject = "test-user"))
assert response.threadIds == []
seen.extend(["legacy-a", "legacy-b"])
response = asyncio.run(chat_history.get_import_ledger(current_subject = "test-user"))
assert response.threadIds == ["legacy-a", "legacy-b"]
def test_record_import_ledger_returns_accepted_and_inserted(monkeypatch):
captured: list[list[str]] = []
def fake_upsert(thread_ids):
captured.append(list(thread_ids))
# Pretend two of the three were already in the ledger.
return (len(thread_ids), max(0, len(thread_ids) - 2))
monkeypatch.setattr(chat_history, "upsert_chat_legacy_imports", fake_upsert)
response = asyncio.run(
chat_history.record_import_ledger(
payload = chat_history.ChatImportLedgerRecordRequest(
threadIds = ["a", "b", "c"],
),
current_subject = "test-user",
)
)
assert response.accepted == 3
assert response.inserted == 1
assert captured == [["a", "b", "c"]]
def test_record_import_ledger_rejects_oversize_payload():
from pydantic import ValidationError
with pytest.raises(ValidationError):
chat_history.ChatImportLedgerRecordRequest(
threadIds = [f"id-{i}" for i in range(10_001)],
)