Files
acdl/tests/test_mode_resolver.py
T
Jon Chery 6a8267e13f test(P01): tests/test_mode_resolver.py — hypothesis properties (REQ-349, cli-engineer)
Property tests (hypothesis):
- deterministic (same inputs → same output)
- flag wins (flag in {agent,interactive} → mode==flag, reason=="flag")
- invalid env ignored (env in {auto,""} → credential-or-tty result)
- no silent fallback (every result has non-empty selection_reason)
- credential+TTY → interactive, credential+no-TTY → agent

Edge cases (explicit):
- stdin TTY + credential → interactive (Edge 3 analog)
- missing credential → falls to TTY
- conflicting flag/env → flag wins
- env wins over credential
- invalid env warns + falls through
- resolve_mode_from_env reads --mode from sys.argv + NOVA_CLIENT_MODE

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:30:08 +00:00

117 lines
4.8 KiB
Python

"""Property + edge-case tests for core.mode_resolver (P1, REQ-349).
Hypothesis-driven: deterministic, flag-wins, invalid-env-ignored,
no-silent-fallback, credential+TTY semantics. Edge cases as explicit
tests (TTY + piped-stdout analog, missing credential, conflicting
flag/env).
"""
from __future__ import annotations
import logging
import pytest
from hypothesis import given, strategies as st, settings, HealthCheck
from core.mode_resolver import resolve_mode
flag_st = st.sampled_from(["agent", "interactive", None])
env_st = st.sampled_from(["agent", "interactive", "auto", "", None])
cred_st = st.sampled_from(["developer_pat", "nova_oidc_token", None])
tty_st = st.booleans()
@given(flag=flag_st, env=env_st, cred=cred_st, tty=tty_st)
@settings(max_examples=200)
def test_deterministic(flag, env, cred, tty):
a = resolve_mode(flag=flag, env_var=env, credential_type=cred, stdin_isatty=tty)
b = resolve_mode(flag=flag, env_var=env, credential_type=cred, stdin_isatty=tty)
assert a == b
@given(flag=flag_st, env=env_st, cred=cred_st, tty=tty_st)
@settings(max_examples=200)
def test_flag_wins(flag, env, cred, tty):
mode, reason = resolve_mode(flag=flag, env_var=env, credential_type=cred, stdin_isatty=tty)
if flag in ("agent", "interactive"):
assert mode == flag
assert reason == "flag"
@given(env=env_st, cred=cred_st, tty=tty_st)
@settings(max_examples=200, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_invalid_env_ignored(env, cred, tty, caplog):
with caplog.at_level(logging.WARNING, logger="nova.mode_resolver"):
mode, reason = resolve_mode(flag=None, env_var=env, credential_type=cred, stdin_isatty=tty)
if env in ("auto", ""):
# invalid/empty env must fall through to credential-or-tty result
expected_mode, expected_reason = resolve_mode(flag=None, env_var=None, credential_type=cred, stdin_isatty=tty)
assert (mode, reason) == (expected_mode, expected_reason)
@given(flag=flag_st, env=env_st, cred=cred_st, tty=tty_st)
@settings(max_examples=200)
def test_no_silent_fallback(flag, env, cred, tty):
_, reason = resolve_mode(flag=flag, env_var=env, credential_type=cred, stdin_isatty=tty)
assert reason and reason.strip() != ""
@given(cred=st.sampled_from(["developer_pat", "nova_oidc_token"]), tty=tty_st)
@settings(max_examples=100)
def test_credential_tty_semantics(cred, tty):
mode, reason = resolve_mode(flag=None, env_var=None, credential_type=cred, stdin_isatty=tty)
if tty:
assert mode == "interactive"
else:
assert mode == "agent"
assert reason == f"credential:{cred}"
# --- Edge cases (explicit) ---
def test_edge_stdin_tty_true_with_credential_is_interactive():
"""Edge 3 analog: stdin is a TTY (even if stdout piped) → interactive."""
mode, reason = resolve_mode(flag=None, env_var=None, credential_type="developer_pat", stdin_isatty=True)
assert mode == "interactive"
assert reason == "credential:developer_pat"
def test_edge_missing_credential_falls_to_tty():
mode_no_tty, reason_no = resolve_mode(flag=None, env_var=None, credential_type=None, stdin_isatty=False)
mode_tty, reason_tty = resolve_mode(flag=None, env_var=None, credential_type=None, stdin_isatty=True)
assert mode_no_tty == "agent" and reason_no == "tty"
assert mode_tty == "interactive" and reason_tty == "tty"
def test_edge_conflicting_flag_env_flag_wins():
mode, reason = resolve_mode(flag="agent", env_var="interactive", credential_type="developer_pat", stdin_isatty=True)
assert mode == "agent" and reason == "flag"
def test_edge_env_wins_over_credential():
mode, reason = resolve_mode(flag=None, env_var="agent", credential_type="developer_pat", stdin_isatty=True)
assert mode == "agent" and reason == "env"
def test_edge_invalid_env_warns_and_falls_through(caplog):
with caplog.at_level(logging.WARNING, logger="nova.mode_resolver"):
mode, reason = resolve_mode(flag=None, env_var="auto", credential_type=None, stdin_isatty=False)
assert mode == "agent" and reason == "tty"
assert any("invalid" in rec.message.lower() for rec in caplog.records)
def test_resolve_mode_from_env_uses_argv_flag(monkeypatch):
monkeypatch.setattr("sys.argv", ["nova", "--mode", "interactive", "policy"])
monkeypatch.setenv("NOVA_CLIENT_MODE", "agent")
from core.mode_resolver import resolve_mode_from_env
mode, reason = resolve_mode_from_env(credential_type=None)
assert mode == "interactive" and reason == "flag"
def test_resolve_mode_from_env_uses_env_when_no_flag(monkeypatch):
monkeypatch.setattr("sys.argv", ["nova", "policy"])
monkeypatch.setenv("NOVA_CLIENT_MODE", "interactive")
from core.mode_resolver import resolve_mode_from_env
mode, reason = resolve_mode_from_env(credential_type=None)
assert mode == "interactive" and reason == "env"