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.
171 lines
5.5 KiB
Python
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)],
|
|
)
|