| 1 | """Regression tests for agent-host local-read boundaries.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import importlib |
| 6 | import io |
| 7 | import json |
| 8 | import os |
| 9 | import sys |
| 10 | from contextlib import redirect_stderr, redirect_stdout |
| 11 | from unittest import mock |
| 12 | |
| 13 | import last30days as cli |
| 14 | from lib import env |
| 15 | |
| 16 | |
| 17 | def test_importing_cli_does_not_load_config_or_propagate_endpoints(monkeypatch): |
| 18 | monkeypatch.delenv("OPENAI_BASE_URL", raising=False) |
| 19 | monkeypatch.delenv("XAI_BASE_URL", raising=False) |
| 20 | with mock.patch("lib.env.get_config", side_effect=AssertionError("import loaded config")): |
| 21 | importlib.reload(cli) |
| 22 | assert os.environ.get("OPENAI_BASE_URL") is None |
| 23 | assert os.environ.get("XAI_BASE_URL") is None |
| 24 | |
| 25 | |
| 26 | def test_diagnose_uses_plan_only_cookie_policy_and_safe_pipeline(monkeypatch): |
| 27 | seen: dict[str, object] = {} |
| 28 | |
| 29 | def fake_get_config(*, policy): |
| 30 | seen["policy"] = policy |
| 31 | return {"_BROWSER_COOKIE_MODE": policy.browser_cookies, "_BROWSER_COOKIE_BROWSERS": ["firefox"]} |
| 32 | |
| 33 | with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \ |
| 34 | mock.patch.object(cli.pipeline, "diagnose", return_value={"ok": True}) as diagnose, \ |
| 35 | mock.patch.object(sys, "argv", ["last30days.py", "--diagnose"]): |
| 36 | stdout = io.StringIO() |
| 37 | stderr = io.StringIO() |
| 38 | with redirect_stdout(stdout), redirect_stderr(stderr): |
| 39 | assert cli.main() == 0 |
| 40 | |
| 41 | assert seen["policy"].browser_cookies == "plan_only" |
| 42 | diagnose.assert_called_once_with( |
| 43 | {"_BROWSER_COOKIE_MODE": "plan_only", "_BROWSER_COOKIE_BROWSERS": ["firefox"]}, |
| 44 | None, |
| 45 | safe=True, |
| 46 | ) |
| 47 | assert json.loads(stdout.getvalue()) == {"ok": True} |
| 48 | |
| 49 | |
| 50 | def test_setup_without_cookie_flag_disables_browser_cookie_setup(monkeypatch): |
| 51 | with mock.patch.object(cli.env, "get_config", return_value={}), \ |
| 52 | mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \ |
| 53 | mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \ |
| 54 | mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \ |
| 55 | mock.patch.object(sys, "argv", ["last30days.py", "setup"]): |
| 56 | stdout = io.StringIO() |
| 57 | stderr = io.StringIO() |
| 58 | with redirect_stdout(stdout), redirect_stderr(stderr): |
| 59 | assert cli.main() == 0 |
| 60 | |
| 61 | assert setup.call_args.kwargs["allow_browser_cookies"] is False |
| 62 | |
| 63 | |
| 64 | def test_setup_cookie_flag_allows_browser_cookie_setup(monkeypatch): |
| 65 | with mock.patch.object(cli.env, "get_config", return_value={}), \ |
| 66 | mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \ |
| 67 | mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \ |
| 68 | mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \ |
| 69 | mock.patch.object(sys, "argv", ["last30days.py", "setup", "--allow-browser-cookies"]): |
| 70 | stdout = io.StringIO() |
| 71 | stderr = io.StringIO() |
| 72 | with redirect_stdout(stdout), redirect_stderr(stderr): |
| 73 | assert cli.main() == 0 |
| 74 | |
| 75 | assert setup.call_args.kwargs["allow_browser_cookies"] is True |
| 76 | |
| 77 | |
| 78 | def test_no_browser_cookies_overrides_setup_cookie_flag(monkeypatch): |
| 79 | seen: dict[str, object] = {} |
| 80 | |
| 81 | def fake_get_config(*, policy): |
| 82 | seen["policy"] = policy |
| 83 | return {} |
| 84 | |
| 85 | with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \ |
| 86 | mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \ |
| 87 | mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \ |
| 88 | mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \ |
| 89 | mock.patch.object( |
| 90 | sys, |
| 91 | "argv", |
| 92 | ["last30days.py", "--no-browser-cookies", "setup", "--allow-browser-cookies"], |
| 93 | ): |
| 94 | stdout = io.StringIO() |
| 95 | stderr = io.StringIO() |
| 96 | with redirect_stdout(stdout), redirect_stderr(stderr): |
| 97 | assert cli.main() == 0 |
| 98 | |
| 99 | assert seen["policy"].browser_cookies == "off" |
| 100 | assert setup.call_args.kwargs["allow_browser_cookies"] is False |
| 101 | |
| 102 | |
| 103 | def test_diagnose_overrides_setup_cookie_flag(monkeypatch): |
| 104 | seen: dict[str, object] = {} |
| 105 | |
| 106 | def fake_get_config(*, policy): |
| 107 | seen["policy"] = policy |
| 108 | return {} |
| 109 | |
| 110 | with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \ |
| 111 | mock.patch("lib.setup_wizard.run_auto_setup", return_value={"cookies_found": {}}) as setup, \ |
| 112 | mock.patch("lib.setup_wizard.write_setup_config", return_value=True), \ |
| 113 | mock.patch("lib.setup_wizard.get_setup_status_text", return_value="ok"), \ |
| 114 | mock.patch.object( |
| 115 | sys, |
| 116 | "argv", |
| 117 | ["last30days.py", "--diagnose", "setup", "--allow-browser-cookies"], |
| 118 | ): |
| 119 | stdout = io.StringIO() |
| 120 | stderr = io.StringIO() |
| 121 | with redirect_stdout(stdout), redirect_stderr(stderr): |
| 122 | assert cli.main() == 0 |
| 123 | |
| 124 | assert seen["policy"].browser_cookies == "plan_only" |
| 125 | assert setup.call_args.kwargs["allow_browser_cookies"] is False |
| 126 | |
| 127 | |
| 128 | def test_research_run_defaults_to_browser_cookie_read(): |
| 129 | """A plain research run reads cookies (the path that powers X auth).""" |
| 130 | parser = cli.build_parser() |
| 131 | args, extra = parser.parse_known_args(["some topic"]) |
| 132 | policy = cli._config_policy_for_args(args, "some topic", extra) |
| 133 | assert policy.browser_cookies == "read" |
| 134 | |
| 135 | |
| 136 | def test_no_browser_cookies_flag_disables_research_run_cookie_read(): |
| 137 | """--no-browser-cookies flips a research run to the no-read policy.""" |
| 138 | parser = cli.build_parser() |
| 139 | args, extra = parser.parse_known_args(["--no-browser-cookies", "some topic"]) |
| 140 | policy = cli._config_policy_for_args(args, "some topic", extra) |
| 141 | assert policy.browser_cookies == "off" |
| 142 | |
| 143 | |
| 144 | def test_watchlist_subprocess_disables_browser_cookies(): |
| 145 | """The unattended watchlist cron must never probe browser cookies.""" |
| 146 | import watchlist |
| 147 | |
| 148 | fake_result = mock.Mock(returncode=1, stdout="", stderr="boom") |
| 149 | with mock.patch.object(watchlist, "store") as store, \ |
| 150 | mock.patch.object(watchlist.subprocess, "run", return_value=fake_result) as run: |
| 151 | store.record_run.return_value = 1 |
| 152 | watchlist._run_topic({"id": 1, "name": "test topic", "search_queries": None}) |
| 153 | |
| 154 | argv = run.call_args.args[0] |
| 155 | assert "--no-browser-cookies" in argv |
| 156 | |
| 157 | |
| 158 | def test_project_config_ignored_by_default_and_cannot_self_trust(tmp_path, monkeypatch): |
| 159 | project_env = tmp_path / ".claude" / "last30days.env" |
| 160 | project_env.parent.mkdir() |
| 161 | project_env.write_text( |
| 162 | "LAST30DAYS_TRUST_PROJECT_CONFIG=1\nOPENAI_BASE_URL=https://example.invalid\n", |
| 163 | encoding="utf-8", |
| 164 | ) |
| 165 | monkeypatch.chdir(tmp_path) |
| 166 | monkeypatch.setattr(env, "CONFIG_FILE", None) |
| 167 | monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False) |
| 168 | monkeypatch.delenv("OPENAI_BASE_URL", raising=False) |
| 169 | |
| 170 | with mock.patch.object(env, "_load_keychain", return_value={}), \ |
| 171 | mock.patch.object(env, "_load_pass", return_value={}): |
| 172 | cfg = env.get_config() |
| 173 | |
| 174 | assert cfg["OPENAI_BASE_URL"] is None |
| 175 | assert cfg["_CONFIG_SOURCE"] == "env_only" |
| 176 | |
| 177 | |
| 178 | def test_project_config_loads_with_process_trust_signal(tmp_path, monkeypatch): |
| 179 | project_env = tmp_path / ".claude" / "last30days.env" |
| 180 | project_env.parent.mkdir() |
| 181 | project_env.write_text("OPENAI_BASE_URL=https://trusted.example\n", encoding="utf-8") |
| 182 | monkeypatch.chdir(tmp_path) |
| 183 | monkeypatch.setattr(env, "CONFIG_FILE", None) |
| 184 | monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "1") |
| 185 | monkeypatch.delenv("OPENAI_BASE_URL", raising=False) |
| 186 | |
| 187 | with mock.patch.object(env, "_load_keychain", return_value={}), \ |
| 188 | mock.patch.object(env, "_load_pass", return_value={}): |
| 189 | cfg = env.get_config() |
| 190 | |
| 191 | assert cfg["OPENAI_BASE_URL"] == "https://trusted.example" |
| 192 | assert cfg["_CONFIG_SOURCE"].startswith(f"project:{project_env}") |
| 193 |