From 6a8267e13f20de1ae3f33014232f8275ea4661da Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 19 Aug 2026 22:30:08 +0000 Subject: [PATCH] =?UTF-8?q?test(P01):=20tests/test=5Fmode=5Fresolver.py=20?= =?UTF-8?q?=E2=80=94=20hypothesis=20properties=20(REQ-349,=20cli-engineer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--- --- tests/test_mode_resolver.py | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/test_mode_resolver.py diff --git a/tests/test_mode_resolver.py b/tests/test_mode_resolver.py new file mode 100644 index 0000000..50e4d97 --- /dev/null +++ b/tests/test_mode_resolver.py @@ -0,0 +1,117 @@ +"""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" \ No newline at end of file