From 847840dfb05fb26d40d134217d023c769c755f27 Mon Sep 17 00:00:00 2001 From: Rasmus Ros Date: Fri, 10 Jul 2026 14:14:52 +0200 Subject: [PATCH] feat: clearer fatal error messages for missing API keys and images --- src/vectrify/main.py | 17 ++++++++-- src/vectrify/tests/test_main.py | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/vectrify/tests/test_main.py diff --git a/src/vectrify/main.py b/src/vectrify/main.py index 8b1ddc0..42c9e4e 100755 --- a/src/vectrify/main.py +++ b/src/vectrify/main.py @@ -31,12 +31,20 @@ def determine_provider_and_model(args) -> tuple[str, str]: elif os.getenv("GEMINI_API_KEY"): provider = "gemini" else: - print("CRITICAL: No API key found.", file=sys.stderr) + print( + "Error: no LLM API key found. Set one of OPENAI_API_KEY, " + "ANTHROPIC_API_KEY, or GEMINI_API_KEY in your environment.", + file=sys.stderr, + ) sys.exit(1) else: env_var = f"{provider.upper()}_API_KEY" if not os.getenv(env_var): - print(f"CRITICAL: {env_var} not set.", file=sys.stderr) + print( + f"Error: --provider {provider} was selected but {env_var} is " + f"not set. Export {env_var} or pick a provider whose key is set.", + file=sys.stderr, + ) sys.exit(1) if not model: @@ -115,8 +123,11 @@ def main(): except KeyboardInterrupt: print("\nSearch interrupted by user. Exiting safely...", file=sys.stderr) sys.exit(130) + except FileNotFoundError: + print(f"Error: input image not found: {args.image}", file=sys.stderr) + sys.exit(1) except Exception as e: - print(f"FATAL: {e}", file=sys.stderr) + print(f"Error: {e}", file=sys.stderr) sys.exit(1) diff --git a/src/vectrify/tests/test_main.py b/src/vectrify/tests/test_main.py new file mode 100644 index 0000000..6b13e79 --- /dev/null +++ b/src/vectrify/tests/test_main.py @@ -0,0 +1,56 @@ +import argparse + +import pytest + +from vectrify.main import determine_provider_and_model + +_KEYS = ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY") + + +@pytest.fixture +def clear_keys(monkeypatch): + for key in _KEYS: + monkeypatch.delenv(key, raising=False) + + +def _args(**kwargs): + defaults = {"provider": "auto", "model": None} + defaults.update(kwargs) + return argparse.Namespace(**defaults) + + +@pytest.mark.usefixtures("clear_keys") +def test_auto_no_key_errors_and_names_env_vars(capsys): + with pytest.raises(SystemExit) as exc: + determine_provider_and_model(_args()) + assert exc.value.code == 1 + err = capsys.readouterr().err + assert all(name in err for name in _KEYS) + assert err.startswith("Error:") + + +@pytest.mark.usefixtures("clear_keys") +def test_explicit_provider_missing_key_names_var(capsys): + with pytest.raises(SystemExit) as exc: + determine_provider_and_model(_args(provider="anthropic")) + assert exc.value.code == 1 + err = capsys.readouterr().err + assert "ANTHROPIC_API_KEY" in err + assert err.startswith("Error:") + + +@pytest.mark.usefixtures("clear_keys") +def test_auto_selects_provider_by_priority(monkeypatch): + monkeypatch.setenv("ANTHROPIC_API_KEY", "x") + monkeypatch.setenv("GEMINI_API_KEY", "y") + provider, model = determine_provider_and_model(_args()) + assert provider == "anthropic" + assert model == "claude-4-6-sonnet" + + +@pytest.mark.usefixtures("clear_keys") +def test_explicit_model_is_preserved(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "x") + provider, model = determine_provider_and_model(_args(model="custom-model")) + assert provider == "openai" + assert model == "custom-model"