diff options
Diffstat (limited to 'test_llamachat.py')
| -rwxr-xr-x | test_llamachat.py | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/test_llamachat.py b/test_llamachat.py index 0eea9e1..ed50995 100755 --- a/test_llamachat.py +++ b/test_llamachat.py @@ -1003,6 +1003,96 @@ def test_provider_parsing(): print("ok provider config parsing") +def test_provider_malformed_shapes(): + """Config shapes that are not tables are skipped, never raised on. + + Every case here is valid TOML a hand-editing user can write, and every + one of them used to reach the GUI as a traceback before any window + existed. The invariant is that the local provider survives all of them. + """ + import io + from contextlib import redirect_stderr + + from llamachat import providers + + # `providers` itself is not a table. Three shapes, and note they used to + # raise two different exception types, which is why the guard is an + # isinstance test and not a try/except. + for bad in ("oops", ["a"], 5): + fallen_back = providers.parse( + {"base_url": "http://localhost:8181", "providers": bad} + ) + assert set(fallen_back) == {"local"}, bad + assert fallen_back["local"].base_url == "http://localhost:8181" + + # A single entry that is not a table. The list case is the one that + # matters: [[providers.a]] is a plausible slip for [providers.a]. + for bad in (5, "x", ["x"], [{"base_url": "http://y.example.org"}]): + mixed = providers.parse( + {"providers": {"local": {"base_url": "http://x.example.org"}, + "a": bad}} + ) + assert set(mixed) == {"local"}, bad + + # A None entry, which TOML cannot produce but callers can, is still + # skipped rather than crashing. + assert providers.parse({"providers": {"a": None}}) == {} + + # Each skip says which provider it dropped and why, so a user running + # from a terminal has something to act on. + err = io.StringIO() + with redirect_stderr(err): + providers.parse( + { + "providers": { + "listy": [{"base_url": "http://x.example.org"}], + "urlless": {"api_key": "env:SOME_VAR"}, + "a:b": {"base_url": "http://y.example.org"}, + } + } + ) + messages = err.getvalue() + assert "listy" in messages and "urlless" in messages and "a:b" in messages + assert "base_url" in messages # the missing-URL case names what is missing + print("ok malformed provider shapes are skipped, not raised") + + +def test_unusable_config_exits(): + """A config that cannot load exits 1 with a message, not a traceback. + + Runs the real entry point in a child process rather than calling main() + in-process. CONFIG_PATH is read from the environment at import time and + baked into load()'s default argument, so rebinding the constant after + import does nothing: only a fresh interpreter with a redirected + XDG_CONFIG_HOME actually moves the file the app reads. + """ + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "llamachat" / "config.toml" + path.parent.mkdir(parents=True) + # A plain syntax error, the commonest way a hand-edited file breaks. + path.write_text('base_url = "http://localhost:8181"\nsocket = = ""\n') + + env = dict(os.environ, XDG_CONFIG_HOME=tmp) + # --ping is the cheapest path that still loads the config, and it + # fails before it looks for a socket, so an instance actually running + # on this machine cannot turn this into a false pass. + proc = subprocess.run( + [sys.executable, "-m", "llamachat", "--ping"], + capture_output=True, text=True, env=env, + cwd=str(Path(__file__).resolve().parent), + ) + + assert proc.returncode == 1, proc.returncode + # A traceback here would mean the app died rather than reported. + assert "Traceback" not in proc.stderr, proc.stderr + # The path is what makes the message actionable: it says which file to fix. + assert str(path) in proc.stderr, proc.stderr + assert "unusable" in proc.stderr + # tomllib names the line, and that detail survives into the report. + assert "line" in proc.stderr, proc.stderr + print("ok unusable config exits without a traceback") + + def test_model_ids_and_filtering(): """Ids are provider:model, local stays bare, filters are substrings.""" from llamachat import providers @@ -1986,6 +2076,8 @@ if __name__ == "__main__": test_venv_discovery() test_config_defaults() test_provider_parsing() + test_provider_malformed_shapes() + test_unusable_config_exits() test_model_ids_and_filtering() test_key_resolution() test_config_providers() |
