"""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"