* Harden interrupted recovery control filtering * Redesign live-to-final assistant replies * Fix live activity anchor test fixture * Fix CI lint issues for live reply tests * Strengthen live progress prompt contract * Recover PR #3401 refresh on origin/master * Repair live-to-final refresh regressions * Fix live worklog refresh regressions * Show live footer timer on initial stream start * Restore live stream shell after reload * Preserve per-frame live SSE replay cursors * Preserve reasoning as Worklog Thinking cards * Quiet Worklog Thinking card styling * Align Worklog Thinking card styling * Scope live Worklog Thinking cards by segment * Suppress exact duplicate settled Thinking * Close #3401 merge review test gaps * fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog) Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor surfaced 4 regressions vs master that the rewritten suite no longer guarded: 1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText used $-anchored regexes so a leading think block + visible answer extracted nothing and the Thinking card never rendered. Removed the 3 $ anchors to match the (non-anchored) display stripper. Live: inline-think thinking-only turn now renders. 2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the cursor onto INFLIGHT + schedules a throttled persist. 3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in the picker. Restored the neon CSS block from master. 4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled turn's worklog when renderMessages re-ran during an active stream (switch-back to an in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length). Live: busy re-render now preserves tool cards (4→4, was 4→0). Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7). * test(#3401): realign 3 stale source-shape assertions to the deep-review fixes Fix commit changed two source literals that existing stage tests scanned for: - test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored 'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4). - test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'. Intent of all three assertions unchanged; only the matched string updated. No code behavior change. * docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign) --------- Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: nesquena-hermes <[email protected]>
189 lines
4.8 KiB
Python
189 lines
4.8 KiB
Python
"""Regression tests for #1527/#1530 LM Studio base_url ownership.
|
|
|
|
When a local OpenAI-compatible endpoint is configured as LM Studio, model
|
|
discovery must trust the configured provider before guessing from the URL host.
|
|
LAN IPs, Tailscale names, and reverse proxies do not contain "lmstudio" in the
|
|
hostname, but the config block already says which provider owns that base_url.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import socket
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
import api.config as config
|
|
import api.profiles as profiles
|
|
|
|
|
|
_API_KEY_ENV_VARS = (
|
|
"ANTHROPIC_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"GOOGLE_API_KEY",
|
|
"GEMINI_API_KEY",
|
|
"GLM_API_KEY",
|
|
"KIMI_API_KEY",
|
|
"DEEPSEEK_API_KEY",
|
|
"OPENCODE_ZEN_API_KEY",
|
|
"OPENCODE_GO_API_KEY",
|
|
"MINIMAX_API_KEY",
|
|
"MINIMAX_CN_API_KEY",
|
|
"XAI_API_KEY",
|
|
"MISTRAL_API_KEY",
|
|
"LM_API_KEY",
|
|
"LMSTUDIO_API_KEY",
|
|
"OLLAMA_API_KEY",
|
|
"LOCAL_API_KEY",
|
|
"API_KEY",
|
|
)
|
|
|
|
|
|
class _ModelsResponse:
|
|
def __init__(self, model_ids: list[str]):
|
|
self._model_ids = model_ids
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return None
|
|
|
|
def read(self) -> bytes:
|
|
return json.dumps({"data": [{"id": mid} for mid in self._model_ids]}).encode()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_config(monkeypatch, tmp_path):
|
|
old_cfg = dict(config.cfg)
|
|
old_mtime = config._cfg_mtime
|
|
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path)
|
|
for var in _API_KEY_ENV_VARS:
|
|
monkeypatch.delenv(var, raising=False)
|
|
config.invalidate_models_cache()
|
|
yield
|
|
config.cfg.clear()
|
|
config.cfg.update(old_cfg)
|
|
config._cfg_mtime = old_mtime
|
|
config.invalidate_models_cache()
|
|
|
|
|
|
def _write_config(tmp_path, monkeypatch, text: str) -> None:
|
|
cfgfile = tmp_path / "config.yaml"
|
|
cfgfile.write_text(text, encoding="utf-8")
|
|
monkeypatch.setattr(config, "_get_config_path", lambda: cfgfile)
|
|
config.reload_config()
|
|
config.invalidate_models_cache()
|
|
|
|
|
|
def _mock_model_discovery(monkeypatch, model_ids: list[str], resolved_ip: str) -> None:
|
|
monkeypatch.setattr(
|
|
urllib.request,
|
|
"urlopen",
|
|
lambda *_args, **_kwargs: _ModelsResponse(model_ids),
|
|
)
|
|
monkeypatch.setattr(
|
|
socket,
|
|
"getaddrinfo",
|
|
lambda *_args, **_kwargs: [
|
|
(socket.AF_INET, socket.SOCK_STREAM, 6, "", (resolved_ip, 0))
|
|
],
|
|
)
|
|
|
|
|
|
def _groups_by_id() -> dict[str, dict]:
|
|
return {
|
|
group["provider_id"]: group
|
|
for group in config.get_available_models()["groups"]
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("base_url", "resolved_ip"),
|
|
[
|
|
("http://192.168.1.22:1234/v1", "192.168.1.22"),
|
|
("http://my-mac.tailnet.example:1234/v1", "192.168.1.22"),
|
|
("https://lm.internal.example.com/v1", "192.168.1.22"),
|
|
],
|
|
)
|
|
def test_lmstudio_configured_base_url_keeps_discovered_models(
|
|
tmp_path,
|
|
monkeypatch,
|
|
base_url: str,
|
|
resolved_ip: str,
|
|
):
|
|
_write_config(
|
|
tmp_path,
|
|
monkeypatch,
|
|
f"""
|
|
model:
|
|
provider: lmstudio
|
|
default: qwen3.6-35b-a3b@q6_k
|
|
base_url: {base_url}
|
|
providers:
|
|
lmstudio:
|
|
api_key: local-key
|
|
""",
|
|
)
|
|
_mock_model_discovery(
|
|
monkeypatch,
|
|
["qwen3.6-35b-a3b@q6_k", "second-lmstudio-model"],
|
|
resolved_ip,
|
|
)
|
|
|
|
groups = _groups_by_id()
|
|
assert "custom" not in groups
|
|
assert "lmstudio" in groups
|
|
model_ids = {model["id"] for model in groups["lmstudio"]["models"]}
|
|
bare_model_ids = {mid.removeprefix("@lmstudio:") for mid in model_ids}
|
|
assert {"qwen3.6-35b-a3b@q6_k", "second-lmstudio-model"} <= bare_model_ids
|
|
|
|
|
|
def test_custom_configured_base_url_is_not_reclassified_as_ollama(tmp_path, monkeypatch):
|
|
_write_config(
|
|
tmp_path,
|
|
monkeypatch,
|
|
"""
|
|
model:
|
|
provider: custom
|
|
default: custom-model
|
|
base_url: http://localhost:4000/v1
|
|
providers:
|
|
custom:
|
|
api_key: local-key
|
|
""",
|
|
)
|
|
_mock_model_discovery(monkeypatch, ["custom-model", "custom-extra"], "127.0.0.1")
|
|
|
|
groups = _groups_by_id()
|
|
assert "ollama" not in groups
|
|
assert "custom" in groups
|
|
model_ids = {model["id"] for model in groups["custom"]["models"]}
|
|
assert {"custom-model", "custom-extra"} <= model_ids
|
|
|
|
|
|
def test_lmstudio_session_model_resolves_to_configured_base_url(tmp_path, monkeypatch):
|
|
_write_config(
|
|
tmp_path,
|
|
monkeypatch,
|
|
"""
|
|
model:
|
|
provider: lmstudio
|
|
default: qwen3.6-35b-a3b@q6_k
|
|
base_url: http://192.168.1.22:1234/v1
|
|
providers:
|
|
lmstudio:
|
|
api_key: local-key
|
|
""",
|
|
)
|
|
|
|
model, provider, base_url = config.resolve_model_provider(
|
|
"qwen3.6-35b-a3b@q6_k"
|
|
)
|
|
|
|
assert model == "qwen3.6-35b-a3b@q6_k"
|
|
assert provider == "lmstudio"
|
|
assert base_url == "http://192.168.1.22:1234/v1"
|