6a8267e13f
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---
117 lines
4.8 KiB
Python
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" |