core/config+cli: recover empty-models providers via models()

Keep empty models as recoverable; promote to ready after remote list or --model.
This commit is contained in:
2026-07-15 17:32:10 +08:00
parent 6bd80cb915
commit 611147b0a5
5 changed files with 314 additions and 23 deletions
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
from pathlib import Path
import pytest
import plyngent.config
from plyngent.cli.provider_recovery import ensure_provider_ready, try_promote_provider
from plyngent.config.models import ModelConfig, OpenAICompatibleProvider
def _hollow_store(tmp_path: Path):
path = tmp_path / "cfg.toml"
_ = path.write_text(
"""
[providers.hollow]
preset = "openai-compatible"
url = "https://example.com/v1"
access_key_or_token = "sk-test"
models = {}
""",
encoding="utf-8",
)
return plyngent.config.load(path)
@pytest.mark.asyncio
async def test_try_promote_with_seed(tmp_path: Path) -> None:
store = _hollow_store(tmp_path)
promoted = await try_promote_provider(store, "hollow", seed_model_ids=["gpt-x"])
assert promoted is not None
assert "gpt-x" in promoted.models
assert "hollow" in store.providers
assert "hollow" not in store.recoverable_providers
@pytest.mark.asyncio
async def test_try_promote_via_remote(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
store = _hollow_store(tmp_path)
async def fake_discover(provider: object) -> list[str]:
del provider
return ["remote-a", "remote-b"]
monkeypatch.setattr("plyngent.cli.provider_recovery.discover_model_ids", fake_discover)
promoted = await try_promote_provider(store, "hollow")
assert promoted is not None
assert set(promoted.models) == {"remote-a", "remote-b"}
@pytest.mark.asyncio
async def test_ensure_provider_ready_already_ready(tmp_path: Path) -> None:
store = _hollow_store(tmp_path)
ready = OpenAICompatibleProvider(
access_key_or_token="sk",
url="https://x/v1",
models={"m": ModelConfig()},
)
store.providers = {"ready": ready}
out = await ensure_provider_ready(store, "ready", ready, interactive=False)
assert out is ready
@pytest.mark.asyncio
async def test_ensure_provider_ready_seed_model(tmp_path: Path) -> None:
store = _hollow_store(tmp_path)
provider = store.recoverable_providers["hollow"]
out = await ensure_provider_ready(
store,
"hollow",
provider,
preferred_model="explicit",
interactive=False,
)
assert "explicit" in out.models
+24 -2
View File
@@ -81,7 +81,7 @@ def test_read_bad_config() -> None:
assert isinstance(config.bad_providers, Mapping)
def test_provider_with_empty_models_is_bad(tmp_path: Path) -> None:
def test_provider_with_empty_models_is_recoverable(tmp_path: Path) -> None:
path = tmp_path / "empty-models.toml"
_ = path.write_text(
"""
@@ -95,7 +95,29 @@ models = {}
)
config = plyngent.config.load(path)
assert "hollow" not in config.providers
assert "hollow" in config.bad_providers
assert "hollow" not in config.bad_providers
assert "hollow" in config.recoverable_providers
promoted = config.promote_provider("hollow", ["m1", "m2"])
assert "hollow" in config.providers
assert "hollow" not in config.recoverable_providers
assert set(promoted.models) == {"m1", "m2"}
def test_promote_provider_requires_ids(tmp_path: Path) -> None:
path = tmp_path / "empty-models.toml"
_ = path.write_text(
"""
[providers.hollow]
preset = "openai-compatible"
url = "https://example.com/v1"
access_key_or_token = "sk-test"
models = {}
""",
encoding="utf-8",
)
config = plyngent.config.load(path)
with pytest.raises(ValueError, match="no model ids"):
_ = config.promote_provider("hollow", [])
def test_read_invalid_config() -> None: