Files
strix/tests/test_auth_cli.py
T
Jonathan SingerandClaude Fable 5 30390628da auth: make STRIX_LLM=openai/subscription the single switch
Replace the separate STRIX_AUTH_MODE flag with a sentinel model value:
STRIX_LLM=openai/subscription selects the authenticated ChatGPT subscription,
and any other value is a normal API-key model. The env vars that already run
Strix are now the single source of truth — no second mode to keep in sync.

Encapsulate the behavior instead of branching everywhere:
- StrixProvider.get_model routes the sentinel to a _CodexResponsesModel backed
  by a cached OAuth client (no global default-client mutation, no per-call
  client churn).
- _CodexResponsesModel self-enforces the backend's requirements — streaming,
  store=false, encrypted reasoning, and the configured reasoning effort — so the
  runner, warm-up, and make_model_settings no longer special-case subscription.

Remove now-unneeded machinery: STRIX_AUTH_MODE/AuthMode, the
"incompatible model" warning, the non-OpenAI model coercion, the
make_model_settings codex flag, and the global set_default_openai_client wiring.
run.json still records a derived auth_mode so the viewer/telemetry/cost display
are unchanged. Switching modes is now just editing STRIX_LLM.

Sentinel-only (no per-model override): a subscription run uses gpt-5.4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:10:57 -04:00

104 lines
3.7 KiB
Python

"""Tests for the `strix auth` CLI: subcommand routing and provider naming."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pytest
from strix.auth import codex, store
from strix.interface import auth_cli
if TYPE_CHECKING:
from pathlib import Path
@pytest.fixture(autouse=True)
def _tmp_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(store, "AUTH_PATH", tmp_path / "home" / ".strix" / "subscription-auth.json")
def test_login_provider_is_chatgpt() -> None:
assert auth_cli.LOGIN_PROVIDER == "chatgpt"
assert codex.PROVIDER in auth_cli._ACCEPTED_PROVIDERS
assert "chatgpt" in auth_cli._ACCEPTED_PROVIDERS
def test_unknown_subcommand_returns_usage_error() -> None:
assert auth_cli.run_auth(["bogus"]) == 2
def test_help_returns_zero() -> None:
assert auth_cli.run_auth(["--help"]) == 0
def test_status_not_signed_in() -> None:
assert auth_cli.run_auth(["status"]) == 1
def test_login_rejects_unsupported_provider(monkeypatch: pytest.MonkeyPatch) -> None:
def _should_not_run(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
msg = "OAuth flow must not start for an unsupported provider"
raise AssertionError(msg)
monkeypatch.setattr(auth_cli, "_run_oauth_flow", _should_not_run)
assert auth_cli.run_auth(["login", "gemini"]) == 2
def test_finish_requires_state_on_loopback(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
# Loopback (require_state=True): missing or mismatched state is rejected.
with pytest.raises(codex.CodexAuthError) as missing:
auth_cli._finish("code", None, "verifier", "expected", require_state=True)
assert missing.value.code == "state_mismatch"
with pytest.raises(codex.CodexAuthError) as mismatch:
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=True)
assert mismatch.value.code == "state_mismatch"
# Matching state proceeds to the exchange.
assert auth_cli._finish("code", "expected", "verifier", "expected", require_state=True) == {
"ok": True
}
def test_finish_manual_paste_allows_absent_state(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
# Manual paste (require_state=False): a bare code with no state is accepted,
# but a present-and-wrong state is still rejected.
assert auth_cli._finish("code", None, "verifier", "expected", require_state=False) == {
"ok": True
}
with pytest.raises(codex.CodexAuthError):
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=False)
def test_finish_rejects_missing_code() -> None:
with pytest.raises(codex.CodexAuthError) as exc:
auth_cli._finish(None, "expected", "verifier", "expected", require_state=True)
assert exc.value.code == "no_code"
@pytest.mark.parametrize("provider", ["chatgpt", "codex", "ChatGPT"])
def test_login_accepts_provider_aliases(provider: str, monkeypatch: pytest.MonkeyPatch) -> None:
reached = {"flow": False}
def _fake_flow(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
reached["flow"] = True
return {
"type": "oauth",
"provider": "codex",
"access": "a",
"refresh": "r",
"account_id": "acct",
"expires_at": 0,
}
monkeypatch.setattr(auth_cli, "_run_oauth_flow", _fake_flow)
monkeypatch.setattr(codex, "save_record", lambda _record: None)
monkeypatch.setattr(auth_cli, "_persist_subscription_config", lambda: None)
assert auth_cli.run_auth(["login", provider]) == 0
assert reached["flow"] is True