| 1 | """Host-contract tests for non-modal agent runtimes.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | ROOT = Path(__file__).resolve().parents[1] |
| 8 | SKILL_MD = ROOT / "skills" / "last30days" / "SKILL.md" |
| 9 | |
| 10 | |
| 11 | def _prose_flow() -> str: |
| 12 | text = SKILL_MD.read_text(encoding="utf-8") |
| 13 | start_marker = "### Non-Modal Prose Flow" |
| 14 | end_marker = "### Manual Setup Guide" |
| 15 | start = text.find(start_marker) |
| 16 | assert start != -1, f"missing section marker: {start_marker}" |
| 17 | end = text.find(end_marker, start) |
| 18 | assert end != -1, f"missing section marker: {end_marker}" |
| 19 | return text[start:end] |
| 20 | |
| 21 | |
| 22 | def test_non_modal_hosts_are_named(): |
| 23 | prose = _prose_flow() |
| 24 | for host in ("Codex", "Cursor", "Gemini CLI", "raw CLI"): |
| 25 | assert host in prose |
| 26 | |
| 27 | |
| 28 | def test_non_modal_cookie_consent_uses_engine_allow_flag(): |
| 29 | prose = _prose_flow() |
| 30 | consent = prose.index("Cookie consent") |
| 31 | allow = prose.index("setup --allow-browser-cookies") |
| 32 | decline = prose.index("FROM_BROWSER=off") |
| 33 | assert consent < allow |
| 34 | assert consent < decline |
| 35 | |
| 36 | |
| 37 | def test_non_modal_preflight_runs_before_cookie_consent(): |
| 38 | prose = _prose_flow() |
| 39 | preflight = prose.index("--preflight") |
| 40 | consent = prose.index("Cookie consent") |
| 41 | assert preflight < consent |
| 42 | assert "does not read browser-cookie values" in prose |
| 43 | assert "does not write setup/config/report files" in prose |
| 44 | assert "does not run research" in prose |
| 45 | |
| 46 | |
| 47 | def test_non_modal_completion_mentions_safe_diagnose_and_project_trust(): |
| 48 | prose = _prose_flow() |
| 49 | assert "safe `--diagnose`" in prose |
| 50 | assert "LAST30DAYS_TRUST_PROJECT_CONFIG=1" in prose |
| 51 | assert "Codex desktop" in prose |
| 52 | |
| 53 | |
| 54 | def _step0_search_contract() -> str: |
| 55 | text = SKILL_MD.read_text(encoding="utf-8") |
| 56 | start_marker = "**STEP 0 - RESOLVE HOST WEB SEARCH FIRST.**" |
| 57 | end_marker = "**FIRST-RUN GATE" |
| 58 | start = text.find(start_marker) |
| 59 | assert start != -1, f"missing section marker: {start_marker}" |
| 60 | end = text.find(end_marker, start) |
| 61 | assert end != -1, f"missing section marker: {end_marker}" |
| 62 | return text[start:end] |
| 63 | |
| 64 | |
| 65 | def test_host_web_search_uses_available_capability_not_specific_tool_name(): |
| 66 | step0 = _step0_search_contract() |
| 67 | assert "usable web-search tool" in step0 |
| 68 | assert "built in, exposed as a deferred tool, or provided by an installed connector" in step0 |
| 69 | assert "Brave, Firecrawl, Exa, Serper" in step0 |
| 70 | assert "If your host requires loading, selecting, or enabling the web-search tool" in step0 |
| 71 | assert "Do not fail the skill just because one particular schema lookup or tool name is unavailable" in step0 |
| 72 | |
| 73 | |
| 74 | def test_no_host_search_uses_auto_resolve_and_leaves_native_signal_unset(): |
| 75 | step0 = _step0_search_contract() |
| 76 | assert "If no web-search tool is available in the agent session" in step0 |
| 77 | assert "--auto-resolve" in step0 |
| 78 | assert "LAST30DAYS_NATIVE_SEARCH=1" in step0 |
| 79 | assert "Leave it unset when the agent session has no web-search tool" in step0 |
| 80 | |
| 81 | |
| 82 | def _law8_block() -> str: |
| 83 | text = SKILL_MD.read_text(encoding="utf-8") |
| 84 | start = text.find("**LAW 8 -") |
| 85 | assert start != -1, "missing LAW 8 marker" |
| 86 | end = text.find("**LAW 9 -", start) |
| 87 | assert end != -1, "missing LAW 9 marker (LAW 8 block end)" |
| 88 | return text[start:end] |
| 89 | |
| 90 | |
| 91 | def test_law8_is_renderer_aware_with_both_regimes(): |
| 92 | # LAW 8 must keep the inline-link default for hidden-link hosts AND carry a |
| 93 | # plain-label branch for visible-URL hosts. Codex rendered every inline link |
| 94 | # as `label (https://...)`, so a single-renderer LAW 8 produced URL soup. |
| 95 | law8 = _law8_block() |
| 96 | assert "Hidden-link hosts (Claude Code)" in law8 |
| 97 | assert "Visible-URL hosts (Codex" in law8 |
| 98 | assert "URL soup" in law8 |
| 99 | # Hidden-link default must remain inline `[name](url)` (no Claude Code regression). |
| 100 | assert "`[name](url)`" in law8 |
| 101 | |
| 102 | |
| 103 | def test_law8_host_detection_is_deterministic_via_claudecode(): |
| 104 | law8 = _law8_block() |
| 105 | assert "CLAUDECODE" in law8 |
| 106 | # The detection must be stated as deterministic, not left to the model guessing. |
| 107 | assert "do not guess" in law8 |
| 108 | |
| 109 | |
| 110 | def test_plan_invocation_warns_against_bash_lc_apostrophe_wrapper(): |
| 111 | # Codex aborted its first engine run by wrapping the query-plan heredoc in |
| 112 | # `bash -lc '...'`; the outer single quote ended at the first apostrophe in a |
| 113 | # ranking string. The guidance must steer off that wrapper explicitly. |
| 114 | text = SKILL_MD.read_text(encoding="utf-8") |
| 115 | assert "bash -lc '...'" in text |
| 116 | assert "unmatched" in text |
| 117 | |
| 118 | |
| 119 | def test_step055_documents_dedicated_vs_broad_subreddits(): |
| 120 | # Step 0.55 must instruct the model to split entity-home (dedicated) subs from |
| 121 | # broad subs and pass them via --dedicated-subreddits, which the engine pulls |
| 122 | # in full and exempts from the relevance floor. |
| 123 | text = SKILL_MD.read_text(encoding="utf-8") |
| 124 | assert "RESOLVED_DEDICATED_SUBREDDITS" in text |
| 125 | assert "--dedicated-subreddits" in text |
| 126 | assert "relevance floor" in text |
| 127 |