Files
openworker/tests/test_settings.py
Devika Verma f574c41454 fix(settings): no Remove key for a provider key set by an environment variable (#742)
Every API-key provider can take its key from an environment variable
(OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, TOGETHER_API_KEY and
so on). When the key came from there, the Models & Keys card still showed the
provider as Connected and offered "Remove key…". Clicking it deleted nothing:
the variable still configured the provider, the card stayed Connected, and the
key field was filled again on the next page load.

Provider rows now carry key_source (store, env or none) and env_key, the name
of the variable when one is set. The card hides Remove key for a key that only
comes from the environment and shows a note naming the variable; when a key
saved in Settings sits on top of the variable, the note says that removing the
saved key falls back to it. remove_provider returns ok: false with the
variable's name when there is nothing stored to remove, and reports
configured: true when a saved key is removed but the variable remains. The
OpenAI-only environment note is replaced by this general one.

Tests: backend cases for the env-only, saved-over-env and plain stored key in
test_settings.py; a GUI test for the Remove key footer; locale parity with the
two new strings in en and zh.
2026-10-07 17:42:34 +05:30

291 lines
12 KiB
Python

"""Tests for the model API-key settings path (Tauri desktop Phase 2).
A Tauri-launched sidecar doesn't inherit the shell env, so the key may live only in the
SecretStore. These cover: the env→store resolver, the status shape (never leaks the key),
and the REST round-trip. No network, no model calls.
"""
from __future__ import annotations
from pathlib import Path
from coworker.providers import resolve_api_key
from coworker.secrets import SecretStore
def test_resolve_api_key_prefers_env(monkeypatch, tmp_path):
monkeypatch.setenv("OPENAI_API_KEY", "sk-env-123")
secrets = SecretStore(path=tmp_path / "secrets.json")
secrets.put("provider:openai", {"type": "api_key", "api_key": "sk-store-999"})
assert resolve_api_key(secrets) == "sk-env-123"
def test_resolve_api_key_falls_back_to_store(monkeypatch, tmp_path):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
secrets = SecretStore(path=tmp_path / "secrets.json")
assert resolve_api_key(secrets) is None
secrets.put("provider:openai", {"type": "api_key", "api_key": "sk-store-999"})
assert resolve_api_key(secrets) == "sk-store-999"
def test_settings_rest_roundtrip(tmp_path, monkeypatch):
from fastapi.testclient import TestClient
from coworker.server.app import create_app
from coworker.server.manager import SessionManager
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
manager = SessionManager(data_dir=tmp_path / "data")
client = TestClient(create_app(manager))
before = client.get("/v1/settings").json()
assert (
before["has_key"] is False
and before["source"] is None
and before["provider"] == "openai"
)
assert before["onboarded"] is False and before["model"] in before["models"]
set_resp = client.post(
"/v1/settings/model-key", json={"api_key": "sk-secret-xyz"}
).json()
assert (
set_resp["ok"] is True
and set_resp["has_key"] is True
and set_resp["source"] == "store"
)
after = client.get("/v1/settings").json()
assert after["has_key"] is True
# the key value is never returned by either endpoint
assert "sk-secret-xyz" not in str(set_resp) and "api_key" not in after
# empty key is rejected
assert (
client.post("/v1/settings/model-key", json={"api_key": " "}).json()["ok"]
is False
)
def test_default_model_and_onboarding_persist(tmp_path, monkeypatch):
from fastapi.testclient import TestClient
from coworker.server.app import create_app
from coworker.server.manager import SessionManager
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
data_dir = tmp_path / "data"
client = TestClient(create_app(SessionManager(data_dir=data_dir)))
# set a default model + mark onboarded
assert (
client.post("/v1/settings/default-model", json={"model": "gpt-4o"}).json()[
"model"
]
== "gpt-4o"
)
assert (
client.post("/v1/settings/onboarded", json={"value": True}).json()["onboarded"]
is True
)
assert (
client.post("/v1/settings/default-model", json={"model": " "}).json()["ok"]
is False
)
# a fresh manager over the same data dir restores both from prefs.json
reborn = SessionManager(data_dir=data_dir)
assert reborn.model == "gpt-4o"
s = reborn.get_settings()
assert s["onboarded"] is True and s["model"] == "gpt-4o"
def test_nav_layout_setting_roundtrips(tmp_path, monkeypatch):
from fastapi.testclient import TestClient
from coworker.server.app import create_app
from coworker.server.manager import SessionManager
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
data_dir = tmp_path / "data"
client = TestClient(create_app(SessionManager(data_dir=data_dir)))
# defaults to "flat"
assert client.get("/v1/settings").json()["nav_layout"] == "flat"
resp = client.post("/v1/settings/nav-layout", json={"nav_layout": "grouped"}).json()
assert resp == {"ok": True, "nav_layout": "grouped"}
assert client.get("/v1/settings").json()["nav_layout"] == "grouped"
# unknown value falls back to flat; persists across a restart
assert (
client.post("/v1/settings/nav-layout", json={"nav_layout": "bogus"}).json()[
"nav_layout"
]
== "flat"
)
client.post("/v1/settings/nav-layout", json={"nav_layout": "grouped"})
reborn = SessionManager(data_dir=data_dir)
assert reborn.get_settings()["nav_layout"] == "grouped"
def test_scratch_base_setting_persists_and_drives_provisioning(tmp_path, monkeypatch):
from fastapi.testclient import TestClient
from coworker.server.app import create_app
from coworker.server.manager import SessionManager
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
data_dir = tmp_path / "data"
client = TestClient(create_app(SessionManager(data_dir=data_dir)))
# defaults to ~/OpenWorker
assert client.get("/v1/settings").json()["scratch_base"] == "~/OpenWorker"
base = tmp_path / "my coworker files"
resp = client.post("/v1/settings/scratch-base", json={"path": str(base)}).json()
assert resp["ok"] is True and resp["scratch_base"] == str(base)
assert base.is_dir() # created on set
assert (
client.post("/v1/settings/scratch-base", json={"path": " "}).json()["ok"]
is False
)
# persists across a restart and actually drives where scratch dirs are provisioned
reborn = SessionManager(data_dir=data_dir)
assert reborn.get_settings()["scratch_base"] == str(base)
scratch = reborn._provision_scratch("sess-xyz")
assert Path(scratch) == (base / "sess-xyz").resolve() and Path(scratch).is_dir()
def test_ollama_models_gated_on_liveness(tmp_path, monkeypatch):
"""`ollama:*` entries show only while a local Ollama answers — keyless must not mean
always-present (a stray ollama:<junk> pref would otherwise render forever)."""
from coworker.server.manager import SessionManager
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
manager = SessionManager(data_dir=tmp_path / "data")
manager.add_model("ollama:llama3.3")
monkeypatch.setattr(SessionManager, "_ollama_alive", lambda self: False)
assert "ollama:llama3.3" not in manager.get_settings()["models"]
monkeypatch.setattr(SessionManager, "_ollama_models", lambda self: [])
monkeypatch.setattr(SessionManager, "_ollama_alive", lambda self: True)
assert "ollama:llama3.3" in manager.get_settings()["models"]
def _ollama_manager(tmp_path, monkeypatch, pulled=("qwen2.5:7b-instruct",)):
"""A fresh manager (no keys, no saved Ollama profile) whose local Ollama answers
`/api/tags` with `pulled` — none of them the recommended model."""
import httpx
from coworker.server.manager import SessionManager
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
from coworker.providers import ollama_facts
class _Tags:
status_code = 200
def json(self):
return {"models": [{"name": n} for n in pulled]}
class _Show:
status_code = 200
def json(self):
return {"capabilities": ["completion", "tools"]}
monkeypatch.setattr(httpx, "get", lambda url, timeout=None: _Tags())
monkeypatch.setattr(httpx, "post", lambda url, json=None, timeout=None: _Show())
ollama_facts.forget_all() # answers are cached per server; never across tests
return SessionManager(data_dir=tmp_path / "data")
def test_ollama_models_without_saved_profile(tmp_path, monkeypatch):
"""Keyless Ollama needs no saved form: pulled models list from the localhost default."""
manager = _ollama_manager(tmp_path, monkeypatch)
assert manager.secrets.get("provider:ollama") is None
assert manager._ollama_models() == ["ollama:qwen2.5:7b-instruct"]
def test_pulled_ollama_models_reach_the_picker(tmp_path, monkeypatch):
manager = _ollama_manager(tmp_path, monkeypatch)
assert "ollama:qwen2.5:7b-instruct" in manager.get_settings()["models"]
def test_ollama_default_ready_tracks_liveness(tmp_path, monkeypatch):
from coworker.server.manager import SessionManager
manager = _ollama_manager(tmp_path, monkeypatch)
manager.set_default_model("ollama:qwen2.5:7b-instruct")
assert manager.get_settings()["model_ready"] is True
monkeypatch.setattr(SessionManager, "_ollama_alive", lambda self: False)
assert manager.get_settings()["model_ready"] is False
def test_ollama_adopts_first_pulled_model(tmp_path, monkeypatch):
"""Saving Ollama with only a non-recommended model pulled still yields a runnable default."""
manager = _ollama_manager(tmp_path, monkeypatch)
assert manager.get_settings()["model_ready"] is False
assert manager.adopt_provider_default("ollama") == "ollama:qwen2.5:7b-instruct"
settings = manager.get_settings()
assert settings["model"] == "ollama:qwen2.5:7b-instruct"
assert settings["model_ready"] is True
def test_ollama_adoption_never_steals_a_working_default(tmp_path, monkeypatch):
manager = _ollama_manager(tmp_path, monkeypatch)
monkeypatch.setenv("OPENAI_API_KEY", "sk-env-123")
before = manager.get_settings()["model"]
assert manager.adopt_provider_default("ollama") is None
assert manager.get_settings()["model"] == before
def test_env_key_provider_reports_its_source_and_refuses_removal(tmp_path, monkeypatch):
"""#742: a key that comes from the provider's environment variable is not ours to
remove. The provider row says where the key comes from, and remove_provider answers
truthfully instead of reporting a removal that changes nothing."""
from coworker.server.manager import SessionManager
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-from-env")
mgr = SessionManager(data_dir=tmp_path)
row = {p["name"]: p for p in mgr.get_providers()}["anthropic"]
assert row["configured"] is True
assert row["key_source"] == "env" and row["env_key"] == "ANTHROPIC_API_KEY"
res = mgr.remove_provider("anthropic")
assert res["ok"] is False and res["env_key"] == "ANTHROPIC_API_KEY"
assert "ANTHROPIC_API_KEY" in res["error"]
assert {p["name"]: p for p in mgr.get_providers()}["anthropic"]["configured"] is True
# A key saved in Settings wins over the variable and can be removed; the provider
# then stays configured by the variable, and the answer says so.
mgr.set_provider("anthropic", {"api_key": "sk-ant-stored"})
row = {p["name"]: p for p in mgr.get_providers()}["anthropic"]
assert row["key_source"] == "store" and row["env_key"] == "ANTHROPIC_API_KEY"
res = mgr.remove_provider("anthropic")
assert res == {"ok": True, "provider": "anthropic", "env_key": "ANTHROPIC_API_KEY", "configured": True}
row = {p["name"]: p for p in mgr.get_providers()}["anthropic"]
assert row["key_source"] == "env" and row["configured"] is True
def test_stored_key_provider_removes_cleanly(tmp_path, monkeypatch):
from coworker.server.manager import SessionManager
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
mgr = SessionManager(data_dir=tmp_path)
assert {p["name"]: p for p in mgr.get_providers()}["anthropic"]["key_source"] is None
mgr.set_provider("anthropic", {"api_key": "sk-ant-stored"})
row = {p["name"]: p for p in mgr.get_providers()}["anthropic"]
assert row["key_source"] == "store" and row["env_key"] is None
assert mgr.remove_provider("anthropic") == {"ok": True, "provider": "anthropic"}
row = {p["name"]: p for p in mgr.get_providers()}["anthropic"]
assert row["configured"] is False and row["key_source"] is None