From 4023cd48112aa8cb5f89a32c7031239988219d59 Mon Sep 17 00:00:00 2001 From: Derek Wisong Date: Wed, 13 May 2026 22:35:06 -0400 Subject: [PATCH] =?UTF-8?q?Make=20tests=20pytest-runnable;=20finish=20Smok?= =?UTF-8?q?e=E2=86=92Test=20scrub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Append a one-line `def test_main(): main()` pytest entry point to each tests/test_*.py file that didn't already expose a discoverable test_* function. `python -m tests.test_` keeps working unchanged. - Rename custom-arg helper functions in test_roles_md.py, test_py_dev_toolkit.py, and test_roles.py from `test_*` to `_check_*` so pytest doesn't try to inject fixtures for their positional params. `main()` still calls them, so the standalone path still exercises them; pytest collects each file's `test_main`. - CI: replace the per-file bash loop with `pytest tests/`. - README: drop the "no pytest" framing and show `pytest tests/` as the primary invocation. - Scrub the remaining capital-S `Smoke` mentions the earlier sed pass missed (README, CI step name, ~35 test docstrings). Verified: black + ruff clean; pytest collects 135 tests, all pass. --- .github/workflows/ci.yml | 23 +------ README.md | 14 +++-- tests/test_agent_label.py | 7 ++- tests/test_arg_scrubbing.py | 2 +- tests/test_ask_parent.py | 7 ++- tests/test_async_subagent.py | 7 ++- tests/test_attachment_lru.py | 7 ++- tests/test_auto_venv.py | 7 ++- tests/test_background_exec.py | 5 ++ tests/test_bench_defaults.py | 7 ++- tests/test_call_tool.py | 2 +- tests/test_checklist.py | 7 ++- tests/test_claude_code_cli.py | 2 +- tests/test_cli_render.py | 5 ++ tests/test_code_mapper.py | 5 ++ tests/test_context_window.py | 5 ++ tests/test_controlling_hooks.py | 2 +- tests/test_ctrlc.py | 7 ++- tests/test_doc_tools.py | 5 ++ tests/test_edit_file.py | 7 ++- tests/test_glob.py | 7 ++- tests/test_grep.py | 7 ++- tests/test_hn_search.py | 5 ++ tests/test_html_tools.py | 5 ++ tests/test_kill_active.py | 5 ++ tests/test_library_usage.py | 7 ++- tests/test_list_models.py | 5 ++ tests/test_memory_category_drift.py | 7 ++- tests/test_memory_recall_improvements.py | 7 ++- tests/test_notify.py | 7 ++- tests/test_notify_surface.py | 7 ++- tests/test_ollama_plugin.py | 5 ++ tests/test_permission_handler.py | 5 ++ tests/test_pip_safety.py | 7 ++- tests/test_plugin_provider.py | 2 +- tests/test_plugins.py | 6 +- tests/test_prompt_environment.py | 7 ++- tests/test_prompt_toolkit.py | 7 ++- tests/test_py_dev_toolkit.py | 69 +++++++++++---------- tests/test_read_file_ceiling.py | 7 ++- tests/test_recursive_subagent.py | 5 ++ tests/test_reddit_search.py | 5 ++ tests/test_roles.py | 25 +++++--- tests/test_roles_md.py | 77 +++++++++++++----------- tests/test_session_audit.py | 7 ++- tests/test_session_replay.py | 5 ++ tests/test_skill_eviction.py | 7 ++- tests/test_status_footer.py | 7 ++- tests/test_streaming.py | 5 ++ tests/test_subagent.py | 5 ++ tests/test_subagent_caps.py | 7 ++- tests/test_subagent_routing.py | 7 ++- tests/test_submit_handler.py | 7 ++- tests/test_subprocess.py | 5 ++ tests/test_token_meter.py | 7 ++- tests/test_web_search.py | 5 ++ tests/test_write_file_append.py | 7 ++- 57 files changed, 369 insertions(+), 141 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03e1c0b..14d28bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,24 +30,5 @@ jobs: - name: Ruff run: python -m ruff check pyagent tests - - name: Smoke tests - run: | - set +e - fail=0 - failed="" - for t in tests/test_*.py; do - name=$(basename "$t" .py) - echo "::group::$name" - python -m "tests.$name" - rc=$? - echo "::endgroup::" - if [ $rc -ne 0 ]; then - fail=$((fail+1)) - failed="$failed $name" - fi - done - if [ $fail -gt 0 ]; then - echo "FAILED ($fail tests):$failed" - exit 1 - fi - echo "All tests passed" + - name: Tests + run: python -m pytest tests/ diff --git a/README.md b/README.md index 34b1128..00e6d88 100644 --- a/README.md +++ b/README.md @@ -85,18 +85,20 @@ Local Ollama needs no key — just the daemon running and a model pulled. ## Tests -Smoke tests live under `tests/` as standalone scripts (no pytest): - ```bash pip install -e '.[dev]' -pre-commit install # one-time: run black + ruff on every commit -python -m tests.test_token_meter # run one -for f in tests/test_*.py; do python -m tests.$(basename "$f" .py); done +pre-commit install # one-time: run black + ruff on every commit +pytest tests/ # run everything +pytest tests/test_token_meter.py # run one file ``` +Each `tests/test_*.py` also runs standalone via +`python -m tests.test_token_meter`, but pytest is the default +runner and what CI uses. + Three recall sub-tests in `test_plugins.py` are gated on `PYAGENT_HEAVY_TESTS=1` because they download a ~130MB embedding -model. CI runs the same loop on every push and pull request. +model. ## License diff --git a/tests/test_agent_label.py b/tests/test_agent_label.py index dc48b3b..72a95ca 100644 --- a/tests/test_agent_label.py +++ b/tests/test_agent_label.py @@ -1,4 +1,4 @@ -"""Smoke for the CLI's _agent_label rendering. +"""Test of the CLI's _agent_label rendering. The label has to survive rich's markup parser and produce `[] ` in cyan around the brackets. A previous version used @@ -51,3 +51,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_arg_scrubbing.py b/tests/test_arg_scrubbing.py index 5e77b94..c14bbbc 100644 --- a/tests/test_arg_scrubbing.py +++ b/tests/test_arg_scrubbing.py @@ -1,4 +1,4 @@ -"""Smoke for tool-call arg scrubbing. +"""Test of tool-call arg scrubbing. Large string args in tool_call dicts get replaced with a short marker after the tool runs, so a write_file with 50KB of content diff --git a/tests/test_ask_parent.py b/tests/test_ask_parent.py index 6abd549..9fc3cc4 100644 --- a/tests/test_ask_parent.py +++ b/tests/test_ask_parent.py @@ -1,4 +1,4 @@ -"""Smoke for mid-task subagent ↔ parent conversation (issue #47). +"""Test of mid-task subagent ↔ parent conversation (issue #47). Drives both sides of `ask_parent` / `reply_to_subagent` against real `_ChildState` IO threads without spawning subagent @@ -325,3 +325,8 @@ def asker(question: str) -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_async_subagent.py b/tests/test_async_subagent.py index e5b8cc7..4e8680e 100644 --- a/tests/test_async_subagent.py +++ b/tests/test_async_subagent.py @@ -1,4 +1,4 @@ -"""Smoke for async subagent dispatch. +"""Test of async subagent dispatch. Covers: 1. `_drain_pending_async()` — appends queued replies as user-role @@ -183,3 +183,8 @@ def runner(): if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_attachment_lru.py b/tests/test_attachment_lru.py index 7aff625..970aaf1 100644 --- a/tests/test_attachment_lru.py +++ b/tests/test_attachment_lru.py @@ -1,4 +1,4 @@ -"""Smoke for the per-session attachments LRU cap (issue #86). +"""Test of the per-session attachments LRU cap (issue #86). Locks these behaviors: @@ -287,3 +287,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_auto_venv.py b/tests/test_auto_venv.py index 0962866..19a6018 100644 --- a/tests/test_auto_venv.py +++ b/tests/test_auto_venv.py @@ -1,4 +1,4 @@ -"""Smoke for venv discovery / creation + the `python_env` tool. +"""Test of venv discovery / creation + the `python_env` tool. Locks: 1. `discover` finds nothing in an empty workspace. @@ -242,3 +242,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_background_exec.py b/tests/test_background_exec.py index d065c9c..98d2cd1 100644 --- a/tests/test_background_exec.py +++ b/tests/test_background_exec.py @@ -301,3 +301,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_bench_defaults.py b/tests/test_bench_defaults.py index 8163540..6420ebb 100644 --- a/tests/test_bench_defaults.py +++ b/tests/test_bench_defaults.py @@ -1,4 +1,4 @@ -"""Smoke for `pyagent-bench`'s default-budget-by-model table. +"""Test of `pyagent-bench`'s default-budget-by-model table. Locks the per-model defaults so a future model rename / pricing-table update doesn't silently make Opus runs halt at the Sonnet budget (or @@ -53,3 +53,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_call_tool.py b/tests/test_call_tool.py index 2c5de3b..744c8f6 100644 --- a/tests/test_call_tool.py +++ b/tests/test_call_tool.py @@ -1,4 +1,4 @@ -"""Smoke tests for `PluginAPI.call_tool` — cross-plugin composition. +"""Tests for `PluginAPI.call_tool` — cross-plugin composition. Covers: - Plugin B's tool calls plugin A's tool through `api.call_tool`. diff --git a/tests/test_checklist.py b/tests/test_checklist.py index 4dd3d9a..6882b49 100644 --- a/tests/test_checklist.py +++ b/tests/test_checklist.py @@ -1,4 +1,4 @@ -"""Smoke for the agent-managed checklist (issue #38). +"""Test of the agent-managed checklist (issue #38). Locks the contract for `pyagent.checklist.Checklist`, the three tool factories that bind to it, the `checklist` event flowing into @@ -307,3 +307,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_claude_code_cli.py b/tests/test_claude_code_cli.py index fc3ad82..218d6a2 100644 --- a/tests/test_claude_code_cli.py +++ b/tests/test_claude_code_cli.py @@ -1,4 +1,4 @@ -"""Smoke tests for the claude-code-cli plugin's early-return error paths. +"""Tests for the claude-code-cli plugin's early-return error paths. All cases exercised here return before subprocess is invoked, so the tests don't require the `claude` binary to be present on PATH. The diff --git a/tests/test_cli_render.py b/tests/test_cli_render.py index 320f18f..8a181b0 100644 --- a/tests/test_cli_render.py +++ b/tests/test_cli_render.py @@ -437,3 +437,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_code_mapper.py b/tests/test_code_mapper.py index 4803267..285adc5 100644 --- a/tests/test_code_mapper.py +++ b/tests/test_code_mapper.py @@ -1069,3 +1069,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_context_window.py b/tests/test_context_window.py index 4897579..df9348c 100644 --- a/tests/test_context_window.py +++ b/tests/test_context_window.py @@ -374,3 +374,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_controlling_hooks.py b/tests/test_controlling_hooks.py index 96d75b5..485c1dc 100644 --- a/tests/test_controlling_hooks.py +++ b/tests/test_controlling_hooks.py @@ -1,4 +1,4 @@ -"""Smoke tests for v2 controlling hooks (issue #66). +"""Tests for v2 controlling hooks (issue #66). Covers every acceptance-criteria bullet: diff --git a/tests/test_ctrlc.py b/tests/test_ctrlc.py index b94cf40..e60398e 100644 --- a/tests/test_ctrlc.py +++ b/tests/test_ctrlc.py @@ -1,4 +1,4 @@ -"""Smoke for clean Ctrl+C behavior. +"""Test of clean Ctrl+C behavior. Subprocess-launches the full `pyagent` CLI in its own process group (simulating an interactive terminal), waits for the agent to become @@ -93,3 +93,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_doc_tools.py b/tests/test_doc_tools.py index c6ec7f0..58d8c68 100644 --- a/tests/test_doc_tools.py +++ b/tests/test_doc_tools.py @@ -710,3 +710,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_edit_file.py b/tests/test_edit_file.py index 7a7f962..fd84356 100644 --- a/tests/test_edit_file.py +++ b/tests/test_edit_file.py @@ -1,4 +1,4 @@ -"""Smoke for edit_file. +"""Test of edit_file. Exercises (in-process): 1. Single-match replace, with correct 1-indexed line number in @@ -134,3 +134,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_glob.py b/tests/test_glob.py index c43f94b..0f055a7 100644 --- a/tests/test_glob.py +++ b/tests/test_glob.py @@ -1,4 +1,4 @@ -"""Smoke for glob. +"""Test of glob. Exercises (in-process): 1. Single-pattern recursive match returns sorted relative paths. @@ -150,3 +150,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_grep.py b/tests/test_grep.py index 94d65cb..81f3d58 100644 --- a/tests/test_grep.py +++ b/tests/test_grep.py @@ -1,4 +1,4 @@ -"""Smoke for grep. +"""Test of grep. Exercises (in-process): 1. Default behavior: bare pattern/path returns `path:lineno:line`, @@ -217,3 +217,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_hn_search.py b/tests/test_hn_search.py index 4029bf1..2dc579a 100644 --- a/tests/test_hn_search.py +++ b/tests/test_hn_search.py @@ -409,3 +409,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_html_tools.py b/tests/test_html_tools.py index 8c4e93c..2efd2e8 100644 --- a/tests/test_html_tools.py +++ b/tests/test_html_tools.py @@ -240,3 +240,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_kill_active.py b/tests/test_kill_active.py index 46ada55..856847d 100644 --- a/tests/test_kill_active.py +++ b/tests/test_kill_active.py @@ -72,3 +72,8 @@ def runner() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_library_usage.py b/tests/test_library_usage.py index 0fc49b8..09116bf 100644 --- a/tests/test_library_usage.py +++ b/tests/test_library_usage.py @@ -1,4 +1,4 @@ -"""Smoke for the pyagent top-level library surface. +"""Test of the pyagent top-level library surface. Locks the public re-exports + auto_client behavior. These are the names the README/library-usage.md teach; if they go missing, the @@ -158,3 +158,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_list_models.py b/tests/test_list_models.py index cce5aec..84fa8d0 100644 --- a/tests/test_list_models.py +++ b/tests/test_list_models.py @@ -334,3 +334,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_memory_category_drift.py b/tests/test_memory_category_drift.py index 53d7737..9392061 100644 --- a/tests/test_memory_category_drift.py +++ b/tests/test_memory_category_drift.py @@ -1,4 +1,4 @@ -"""Smoke for the Tier-2 memory category-drift improvements. +"""Test of the Tier-2 memory category-drift improvements. Locks four behaviors: @@ -256,3 +256,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_memory_recall_improvements.py b/tests/test_memory_recall_improvements.py index 1bebe5c..1c2cee4 100644 --- a/tests/test_memory_recall_improvements.py +++ b/tests/test_memory_recall_improvements.py @@ -1,4 +1,4 @@ -"""Smoke for the Tier-1 memory recall improvements. +"""Test of the Tier-1 memory recall improvements. Locks three behaviors: @@ -247,3 +247,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_notify.py b/tests/test_notify.py index bdd13ce..b08351c 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -1,4 +1,4 @@ -"""Smoke for the subagent ↔ parent notification protocol (issue #64). +"""Test of the subagent ↔ parent notification protocol (issue #64). Drives both directions of the new `subagent_note` / `parent_note` pipe events against real `_ChildState` IO threads without spawning @@ -341,3 +341,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_notify_surface.py b/tests/test_notify_surface.py index 55d03b6..fc44d82 100644 --- a/tests/test_notify_surface.py +++ b/tests/test_notify_surface.py @@ -1,4 +1,4 @@ -"""Smoke for the parent-side notification surface (issue #65). +"""Test of the parent-side notification surface (issue #65). Drives `tell_subagent` and `peek_subagent` against real `_ChildState` IO threads with fake subagent pipes — the same @@ -496,3 +496,8 @@ def _capture_unread(count: int, by_sev: dict[str, int]) -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_ollama_plugin.py b/tests/test_ollama_plugin.py index 688e55e..1284b91 100644 --- a/tests/test_ollama_plugin.py +++ b/tests/test_ollama_plugin.py @@ -1210,3 +1210,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_permission_handler.py b/tests/test_permission_handler.py index b7f4fd6..e5736ae 100644 --- a/tests/test_permission_handler.py +++ b/tests/test_permission_handler.py @@ -245,3 +245,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_pip_safety.py b/tests/test_pip_safety.py index e495c0e..3752017 100644 --- a/tests/test_pip_safety.py +++ b/tests/test_pip_safety.py @@ -1,4 +1,4 @@ -"""Smoke test for pip-pollution danger patterns. +"""Test for pip-pollution danger patterns. Asserts `_safety_check` flags installs that would pollute system or user-level Python and that benign pip use still passes. @@ -45,3 +45,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_plugin_provider.py b/tests/test_plugin_provider.py index 99124d1..4020e97 100644 --- a/tests/test_plugin_provider.py +++ b/tests/test_plugin_provider.py @@ -1,4 +1,4 @@ -"""Smoke tests for the plugin-provider surface (PluginAPI.register_provider +"""Tests for the plugin-provider surface (PluginAPI.register_provider + pyagent.llms plugin-router integration). What's covered: diff --git a/tests/test_plugins.py b/tests/test_plugins.py index e8656f1..c6b22fd 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,4 +1,4 @@ -"""Smoke tests for the plugin loader. +"""Tests for the plugin loader. Covers: - Discover and load a drop-in plugin with manifest + plugin.py. @@ -1570,7 +1570,7 @@ def test_write_session_attachment_no_session() -> None: "def get_state(): return _state\n" "def register(api):\n" " def go() -> str:\n" - ' """Smoke: try to write."""\n' + ' """Test: try to write."""\n' " path = api.write_session_attachment(\n" ' "go", "side-data", suffix=".json"\n' " )\n" @@ -1616,7 +1616,7 @@ def test_write_session_attachment_with_session() -> None: "def get_state(): return _state\n" "def register(api):\n" " def go() -> str:\n" - ' """Smoke: write side-data."""\n' + ' """Test: write side-data."""\n' " p = api.write_session_attachment(\n" ' "go", \'{"k": 1}\', suffix=".json"\n' " )\n" diff --git a/tests/test_prompt_environment.py b/tests/test_prompt_environment.py index 9a1dad6..ea00110 100644 --- a/tests/test_prompt_environment.py +++ b/tests/test_prompt_environment.py @@ -1,4 +1,4 @@ -"""Smoke test for the system-prompt environment footer. +"""Test for the system-prompt environment footer. Asserts the persona footer surfaces enough host context (OS, shell, python) for the agent to pick the right shell idioms. @@ -90,3 +90,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_prompt_toolkit.py b/tests/test_prompt_toolkit.py index f922b6c..c189493 100644 --- a/tests/test_prompt_toolkit.py +++ b/tests/test_prompt_toolkit.py @@ -1,4 +1,4 @@ -"""Smoke for the prompt_toolkit-backed REPL input. +"""Test of the prompt_toolkit-backed REPL input. Runs pyagent under a real PTY so prompt_toolkit's interactive path is exercised (the PIPE-based test_ctrlc falls back to a non-tty @@ -156,3 +156,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_py_dev_toolkit.py b/tests/test_py_dev_toolkit.py index 1429252..fb75545 100644 --- a/tests/test_py_dev_toolkit.py +++ b/tests/test_py_dev_toolkit.py @@ -1,4 +1,4 @@ -"""Smoke tests for the py-dev-toolkit plugin (lint / typecheck / run_pytest). +"""Tests for the py-dev-toolkit plugin (lint / typecheck / run_pytest). Each test is gated on the relevant binary being installed in the runtime environment. CI hosts without ruff / mypy / pytest will skip @@ -36,12 +36,12 @@ def _setup() -> tuple[dict, Path]: return tools, workdir -def test_plugin_registers_three_tools(tools: dict) -> None: +def _check_plugin_registers_three_tools(tools: dict) -> None: for name in ("lint", "typecheck", "run_pytest"): _check(f"{name!r} registered", name in tools, f"have: {sorted(tools)}") -def test_lint_findings_and_clean(tools: dict, workdir: Path) -> None: +def _check_lint_findings_and_clean(tools: dict, workdir: Path) -> None: if shutil.which("ruff") is None: _check("ruff test skipped (binary missing)", True) return @@ -62,7 +62,7 @@ def test_lint_findings_and_clean(tools: dict, workdir: Path) -> None: _check("lint clean run", out_clean.startswith("ruff: clean"), out_clean) -def test_lint_input_validation(tools: dict, workdir: Path) -> None: +def _check_lint_input_validation(tools: dict, workdir: Path) -> None: out = tools["lint"]("") _check("empty path → error", out.startswith(" None: ) -def test_typecheck_mypy(tools: dict, workdir: Path) -> None: +def _check_typecheck_mypy(tools: dict, workdir: Path) -> None: if shutil.which("mypy") is None: _check("mypy test skipped (binary missing)", True) return @@ -96,7 +96,7 @@ def test_typecheck_mypy(tools: dict, workdir: Path) -> None: _check("mypy clean run", out_clean.startswith("mypy: clean"), out_clean) -def test_typecheck_input_validation(tools: dict) -> None: +def _check_typecheck_input_validation(tools: dict) -> None: out = tools["typecheck"]("/some/path", tool="pyflakes") _check( "unsupported typecheck tool → error", @@ -105,19 +105,19 @@ def test_typecheck_input_validation(tools: dict) -> None: ) -def test_run_pytest_basic(tools: dict, workdir: Path) -> None: +def _check_run_pytest_basic(tools: dict, workdir: Path) -> None: if shutil.which("pytest") is None: _check("pytest test skipped (binary missing)", True) return test_file = workdir / "test_demo.py" test_file.write_text( - "def test_pass():\n" + "def _check_pass():\n" " assert 1 + 1 == 2\n" "\n" - "def test_fail():\n" + "def _check_fail():\n" " assert 1 + 1 == 3, 'math broken'\n" "\n" - "def test_skip():\n" + "def _check_skip():\n" " import pytest; pytest.skip('not relevant')\n" ) out = tools["run_pytest"](str(test_file)) @@ -136,7 +136,7 @@ def test_run_pytest_basic(tools: dict, workdir: Path) -> None: ) -def test_run_pytest_k_filter(tools: dict, workdir: Path) -> None: +def _check_run_pytest_k_filter(tools: dict, workdir: Path) -> None: if shutil.which("pytest") is None: return test_file = workdir / "test_demo.py" @@ -155,7 +155,7 @@ def test_run_pytest_k_filter(tools: dict, workdir: Path) -> None: ) -def test_mypy_text_parser_handles_windows_paths() -> None: +def _check_mypy_text_parser_handles_windows_paths() -> None: """Reviewer-found bug: previous regex used `[^:]+` for the file path, so `C:\\foo\\bar.py:3:1: error: …` never matched and Windows hosts got a false-clean result. New regex uses a @@ -187,7 +187,7 @@ def test_mypy_text_parser_handles_windows_paths() -> None: ) -def test_mypy_text_parser_filters_notes() -> None: +def _check_mypy_text_parser_filters_notes() -> None: """Reviewer-found bug: `note:` lines were being counted as findings, inflating the summary (e.g. `1 error, 4 notes` shown as 5 findings). They're context for the adjacent error, not @@ -209,7 +209,7 @@ def test_mypy_text_parser_filters_notes() -> None: ) -def test_mypy_json_parser_filters_notes() -> None: +def _check_mypy_json_parser_filters_notes() -> None: """Same notes-filtering applied to mypy's JSONL output.""" from pyagent.plugins.py_dev_toolkit.typecheck import parse_mypy_json @@ -229,7 +229,7 @@ def test_mypy_json_parser_filters_notes() -> None: ) -def test_lint_on_directory(tools: dict, workdir: Path) -> None: +def _check_lint_on_directory(tools: dict, workdir: Path) -> None: if shutil.which("ruff") is None: return sub = workdir / "lint_dir" @@ -245,7 +245,7 @@ def test_lint_on_directory(tools: dict, workdir: Path) -> None: _check("finding cites file in directory", "a.py" in out, out[:300]) -def test_pathutil_shorten() -> None: +def _check_pathutil_shorten() -> None: """`_pathutil.shorten` normalizes path output across tools so `lint` (ruff resolves to absolute) and `typecheck` (mypy preserves caller input) emit the same shape: relative when @@ -269,7 +269,7 @@ def test_pathutil_shorten() -> None: _check("empty input returns unchanged", shorten("") == "") -def test_pytest_permission_gate_runs_for_nonexistent_paths( +def _check_pytest_permission_gate_runs_for_nonexistent_paths( tools: dict, ) -> None: """Reviewer-found bug: `if target_path.exists() and not @@ -295,7 +295,7 @@ def test_pytest_permission_gate_runs_for_nonexistent_paths( permissions.set_prompt_handler(saved_handler) -def test_missing_binary_path(workdir: Path) -> None: +def _check_missing_binary_path(workdir: Path) -> None: # Spoof PATH to confirm clean error when binary missing. Need a # real-on-disk file because the existence check runs before the # binary check. @@ -318,22 +318,27 @@ def test_missing_binary_path(workdir: Path) -> None: def main() -> None: tools, workdir = _setup() - test_plugin_registers_three_tools(tools) - test_lint_findings_and_clean(tools, workdir) - test_lint_input_validation(tools, workdir) - test_typecheck_mypy(tools, workdir) - test_typecheck_input_validation(tools) - test_run_pytest_basic(tools, workdir) - test_run_pytest_k_filter(tools, workdir) - test_mypy_text_parser_handles_windows_paths() - test_mypy_text_parser_filters_notes() - test_mypy_json_parser_filters_notes() - test_lint_on_directory(tools, workdir) - test_pathutil_shorten() - test_pytest_permission_gate_runs_for_nonexistent_paths(tools) - test_missing_binary_path(workdir) + _check_plugin_registers_three_tools(tools) + _check_lint_findings_and_clean(tools, workdir) + _check_lint_input_validation(tools, workdir) + _check_typecheck_mypy(tools, workdir) + _check_typecheck_input_validation(tools) + _check_run_pytest_basic(tools, workdir) + _check_run_pytest_k_filter(tools, workdir) + _check_mypy_text_parser_handles_windows_paths() + _check_mypy_text_parser_filters_notes() + _check_mypy_json_parser_filters_notes() + _check_lint_on_directory(tools, workdir) + _check_pathutil_shorten() + _check_pytest_permission_gate_runs_for_nonexistent_paths(tools) + _check_missing_binary_path(workdir) print("\nALL CHECKS PASSED") if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_read_file_ceiling.py b/tests/test_read_file_ceiling.py index 738f648..8f28a25 100644 --- a/tests/test_read_file_ceiling.py +++ b/tests/test_read_file_ceiling.py @@ -1,4 +1,4 @@ -"""Smoke for the read_file soft-threshold ceiling (issue #9). +"""Test of the read_file soft-threshold ceiling (issue #9). Locks four behaviors: 1. Small read_file output (< attachment_threshold) returns inline. @@ -129,3 +129,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_recursive_subagent.py b/tests/test_recursive_subagent.py index befd905..ba5d71f 100644 --- a/tests/test_recursive_subagent.py +++ b/tests/test_recursive_subagent.py @@ -148,3 +148,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_reddit_search.py b/tests/test_reddit_search.py index 35ee055..7f2fbf8 100644 --- a/tests/test_reddit_search.py +++ b/tests/test_reddit_search.py @@ -423,3 +423,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_roles.py b/tests/test_roles.py index a613b57..b70ae20 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -48,7 +48,7 @@ def _write_config(tmp: Path) -> None: """) -def test_role_load_and_resolve(tmp: Path) -> None: +def _check_role_load_and_resolve(tmp: Path) -> None: loaded = roles.load() # Bundled roles may also be present; assert legacy entries loaded. assert {"skim", "cheap"} <= set(loaded), loaded @@ -82,7 +82,7 @@ def test_role_load_and_resolve(tmp: Path) -> None: print("✓ resolve('') → ('', None) — caller inherits parent's model") -def test_build_subagent_config_with_role(tmp: Path) -> None: +def _check_build_subagent_config_with_role(tmp: Path) -> None: parent_session = MagicMock() parent_session.dir = tmp / "fake-session" parent_session.dir.mkdir(exist_ok=True) @@ -115,7 +115,7 @@ def test_build_subagent_config_with_role(tmp: Path) -> None: print(f"✓ _build_subagent_config: role data threaded into cfg ({sid})") -def test_register_tools_allowlist() -> None: +def _check_register_tools_allowlist() -> None: a = Agent(client=EchoClient()) agent_proc._register_tools(a, allow_meta=False, allowlist=["read_file", "grep"]) assert sorted(a.tools) == ["grep", "read_file"], sorted(a.tools) @@ -132,7 +132,7 @@ def test_register_tools_allowlist() -> None: print("✓ _register_tools allowlist=None → full default set, no meta") -def test_end_to_end_role_spawn(tmp: Path) -> None: +def _check_end_to_end_role_spawn(tmp: Path) -> None: """Spawn an actual subprocess subagent with a role and round-trip a turn.""" soul = paths.resolve("SOUL.md", seed="SOUL.md") tools_md = paths.resolve("TOOLS.md", seed="TOOLS.md") @@ -198,7 +198,7 @@ def test_end_to_end_role_spawn(tmp: Path) -> None: io_thread.join(timeout=2) -def test_set_model_handler() -> None: +def _check_set_model_handler() -> None: ctx = multiprocessing.get_context("spawn") parent_end, child_end = ctx.Pipe(duplex=True) state = agent_proc._ChildState(conn=child_end) @@ -234,14 +234,19 @@ def main() -> None: _write_config(tmp) - test_role_load_and_resolve(tmp) - test_build_subagent_config_with_role(tmp) - test_register_tools_allowlist() - test_end_to_end_role_spawn(tmp) - test_set_model_handler() + _check_role_load_and_resolve(tmp) + _check_build_subagent_config_with_role(tmp) + _check_register_tools_allowlist() + _check_end_to_end_role_spawn(tmp) + _check_set_model_handler() print("\nALL CHECKS PASSED") if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_roles_md.py b/tests/test_roles_md.py index 75c77ca..88236db 100644 --- a/tests/test_roles_md.py +++ b/tests/test_roles_md.py @@ -39,7 +39,7 @@ def _write(p: Path, text: str) -> None: p.write_text(text) -def test_frontmatter_full(tmp: Path) -> None: +def _check_frontmatter_full(tmp: Path) -> None: """A role with every frontmatter field populated.""" _write( tmp / ".pyagent" / "roles" / "alpha.md", @@ -61,7 +61,7 @@ def test_frontmatter_full(tmp: Path) -> None: print("✓ frontmatter (full): all fields applied") -def test_frontmatter_none(tmp: Path) -> None: +def _check_frontmatter_none(tmp: Path) -> None: """A role with no frontmatter at all — defaults + auto-description.""" _write( tmp / ".pyagent" / "roles" / "bare.md", @@ -77,7 +77,7 @@ def test_frontmatter_none(tmp: Path) -> None: print("✓ frontmatter (none): defaults applied + description derived") -def test_frontmatter_partial(tmp: Path) -> None: +def _check_frontmatter_partial(tmp: Path) -> None: """Partial frontmatter — only `tools`, no model/description.""" _write( tmp / ".pyagent" / "roles" / "partial.md", @@ -94,7 +94,7 @@ def test_frontmatter_partial(tmp: Path) -> None: print("✓ frontmatter (partial): selective override + auto description") -def test_frontmatter_malformed(tmp: Path) -> None: +def _check_frontmatter_malformed(tmp: Path) -> None: """Bad TOML in frontmatter falls back to no frontmatter.""" _write( tmp / ".pyagent" / "roles" / "broken.md", @@ -111,7 +111,7 @@ def test_frontmatter_malformed(tmp: Path) -> None: print("✓ frontmatter (malformed): role still loads with defaults") -def test_description_derivation_skips_heading(tmp: Path) -> None: +def _check_description_derivation_skips_heading(tmp: Path) -> None: """Description should skip the leading heading line.""" _write( tmp / ".pyagent" / "roles" / "headed.md", @@ -124,7 +124,7 @@ def test_description_derivation_skips_heading(tmp: Path) -> None: print("✓ description: leading heading skipped, first paragraph used") -def test_case_insensitive_lookup(tmp: Path) -> None: +def _check_case_insensitive_lookup(tmp: Path) -> None: """File `RESEARCHER.md` callable as `researcher` and any case variant.""" _write( tmp / ".pyagent" / "roles" / "BIG.md", @@ -139,7 +139,7 @@ def test_case_insensitive_lookup(tmp: Path) -> None: print("✓ lookup: case-insensitive (BIG / Big / big all resolve)") -def test_dash_underscore_normalization(tmp: Path) -> None: +def _check_dash_underscore_normalization(tmp: Path) -> None: """`software-engineer.md` → role name `software_engineer`.""" _write( tmp / ".pyagent" / "roles" / "field-tester.md", @@ -155,7 +155,7 @@ def test_dash_underscore_normalization(tmp: Path) -> None: print("✓ filename: dash/underscore both normalize to underscores") -def test_tier_precedence(tmp: Path, user_dir: Path) -> None: +def _check_tier_precedence(tmp: Path, user_dir: Path) -> None: """Project > user > bundled on canonical-name collision. `RESEARCHER.md` ships bundled. We add a user-tier file and a @@ -186,7 +186,7 @@ def test_tier_precedence(tmp: Path, user_dir: Path) -> None: print("✓ tier: project beats user beats bundled") -def test_legacy_models_table_still_resolves_and_warns(tmp: Path, caplog) -> None: +def _check_legacy_models_table_still_resolves_and_warns(tmp: Path, caplog) -> None: """[models.] in config.toml still loads + emits one-time warning.""" (tmp / ".pyagent" / "config.toml").write_text( "[models.legacyrole]\n" @@ -214,7 +214,7 @@ def test_legacy_models_table_still_resolves_and_warns(tmp: Path, caplog) -> None print("✓ legacy: deprecation warning does not spam (one-time)") -def test_file_based_role_shadows_legacy(tmp: Path) -> None: +def _check_file_based_role_shadows_legacy(tmp: Path) -> None: """If both an .md role and a [models.] entry exist with the same canonical name, the file-based form wins (newer authoring tool).""" @@ -236,7 +236,7 @@ def test_file_based_role_shadows_legacy(tmp: Path) -> None: print("✓ collision: file-based role shadows legacy [models.shared]") -def test_per_tier_disk_collision(tmp: Path) -> None: +def _check_per_tier_disk_collision(tmp: Path) -> None: """Two files in the same tier that normalize to the same name — warn and keep the lexicographically-first.""" _write( @@ -254,7 +254,7 @@ def test_per_tier_disk_collision(tmp: Path) -> None: print("✓ per-tier collision: lexicographically-first wins") -def test_catalog_reflects_file_based_roles(tmp: Path) -> None: +def _check_catalog_reflects_file_based_roles(tmp: Path) -> None: _write( tmp / ".pyagent" / "roles" / "catalog_role.md", '+++\nmodel = "pyagent/echo"\n+++\n\nA cataloged role.\n', @@ -265,7 +265,7 @@ def test_catalog_reflects_file_based_roles(tmp: Path) -> None: print("✓ catalog: file-based roles render in the system-prompt block") -def test_cli_list_runs(tmp: Path, user_dir: Path) -> None: +def _check_cli_list_runs(tmp: Path, user_dir: Path) -> None: """`pyagent-roles list` finds bundled + user + project roles.""" # User-tier _write(user_dir / "roles" / "user_only.md", "# Role: User\n\nUser role.\n") @@ -283,7 +283,7 @@ def test_cli_list_runs(tmp: Path, user_dir: Path) -> None: print("✓ pyagent-roles list: shows bundled + user + project tiers") -def test_cli_show_and_path(tmp: Path) -> None: +def _check_cli_show_and_path(tmp: Path) -> None: _write( tmp / ".pyagent" / "roles" / "shown.md", '+++\nmodel = "pyagent/echo"\n+++\n\n# Role: Shown\n\nShown body.\n', @@ -301,7 +301,7 @@ def test_cli_show_and_path(tmp: Path) -> None: print("✓ pyagent-roles show / path: emit role content + resolved path") -def test_cli_init_idempotent(tmp: Path, user_dir: Path) -> None: +def _check_cli_init_idempotent(tmp: Path, user_dir: Path) -> None: """`pyagent-roles init` seeds config-dir/roles, idempotently.""" runner = CliRunner() first = runner.invoke(roles_cli.main, ["init"]) @@ -322,7 +322,7 @@ def test_cli_init_idempotent(tmp: Path, user_dir: Path) -> None: print("✓ pyagent-roles init: idempotent (skips existing files)") -def test_cli_migrate(tmp: Path, user_dir: Path) -> None: +def _check_cli_migrate(tmp: Path, user_dir: Path) -> None: """`pyagent-roles migrate` synthesizes .md files from [models.].""" (tmp / ".pyagent" / "config.toml").write_text( "[models.tomigrate]\n" @@ -362,7 +362,7 @@ def test_cli_migrate(tmp: Path, user_dir: Path) -> None: print("✓ pyagent-roles migrate: writes .md and roles.load() picks it up") -def test_cli_migrate_dashed_name(tmp: Path, user_dir: Path) -> None: +def _check_cli_migrate_dashed_name(tmp: Path, user_dir: Path) -> None: """Legacy role names with dashes (`[models.deep-thought]`) migrate to canonical `DEEP_THOUGHT.md` — uppercase + underscores, matching the bundled-roles convention. Lookup still works either way via @@ -394,7 +394,7 @@ def test_cli_migrate_dashed_name(tmp: Path, user_dir: Path) -> None: print("✓ pyagent-roles migrate: dashed names → underscored filenames") -def test_cli_migrate_no_body_keeps_description(tmp: Path, user_dir: Path) -> None: +def _check_cli_migrate_no_body_keeps_description(tmp: Path, user_dir: Path) -> None: """When the legacy [models.] entry has no `system_prompt`, the migrated file must keep `description` in the frontmatter — the auto-derive has nothing to pull from.""" @@ -459,24 +459,24 @@ def main() -> None: try: caplog = _CapLog() - test_frontmatter_full(tmp) - test_frontmatter_none(tmp) - test_frontmatter_partial(tmp) - test_frontmatter_malformed(tmp) - test_description_derivation_skips_heading(tmp) - test_case_insensitive_lookup(tmp) - test_dash_underscore_normalization(tmp) - test_tier_precedence(tmp, user_dir) - test_legacy_models_table_still_resolves_and_warns(tmp, caplog) - test_file_based_role_shadows_legacy(tmp) - test_per_tier_disk_collision(tmp) - test_catalog_reflects_file_based_roles(tmp) - test_cli_list_runs(tmp, user_dir) - test_cli_show_and_path(tmp) - test_cli_init_idempotent(tmp, user_dir) - test_cli_migrate(tmp, user_dir) - test_cli_migrate_dashed_name(tmp, user_dir) - test_cli_migrate_no_body_keeps_description(tmp, user_dir) + _check_frontmatter_full(tmp) + _check_frontmatter_none(tmp) + _check_frontmatter_partial(tmp) + _check_frontmatter_malformed(tmp) + _check_description_derivation_skips_heading(tmp) + _check_case_insensitive_lookup(tmp) + _check_dash_underscore_normalization(tmp) + _check_tier_precedence(tmp, user_dir) + _check_legacy_models_table_still_resolves_and_warns(tmp, caplog) + _check_file_based_role_shadows_legacy(tmp) + _check_per_tier_disk_collision(tmp) + _check_catalog_reflects_file_based_roles(tmp) + _check_cli_list_runs(tmp, user_dir) + _check_cli_show_and_path(tmp) + _check_cli_init_idempotent(tmp, user_dir) + _check_cli_migrate(tmp, user_dir) + _check_cli_migrate_dashed_name(tmp, user_dir) + _check_cli_migrate_no_body_keeps_description(tmp, user_dir) print("\nALL CHECKS PASSED") finally: @@ -485,3 +485,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_session_audit.py b/tests/test_session_audit.py index a82b925..3fb5ac1 100644 --- a/tests/test_session_audit.py +++ b/tests/test_session_audit.py @@ -1,4 +1,4 @@ -"""Smoke for `pyagent-sessions audit` (issue #14, Part B). +"""Test of `pyagent-sessions audit` (issue #14, Part B). Locks four behaviors: 1. audit_session aggregates four-key usage correctly across mixed @@ -512,3 +512,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_session_replay.py b/tests/test_session_replay.py index 93fbbe8..cc29ed0 100644 --- a/tests/test_session_replay.py +++ b/tests/test_session_replay.py @@ -465,3 +465,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_skill_eviction.py b/tests/test_skill_eviction.py index dde9e9e..59f54f1 100644 --- a/tests/test_skill_eviction.py +++ b/tests/test_skill_eviction.py @@ -1,4 +1,4 @@ -"""Smoke for `read_skill` body eviction (issue #10). +"""Test of `read_skill` body eviction (issue #10). Locks six behaviors of `Agent._apply_eviction` and the `evict_after_use=True` flag on `Agent.add_tool`: @@ -304,3 +304,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_status_footer.py b/tests/test_status_footer.py index 84a9ea0..105e3c7 100644 --- a/tests/test_status_footer.py +++ b/tests/test_status_footer.py @@ -1,4 +1,4 @@ -"""Smoke for the CLI status footer. +"""Test of the CLI status footer. Drives `_update_agents_state`, `_render_status`, and `_compose_footer` directly with synthetic events and asserts the rendered footer @@ -536,3 +536,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 4796fdb..221abc1 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -575,3 +575,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_subagent.py b/tests/test_subagent.py index a20044c..b93fb27 100644 --- a/tests/test_subagent.py +++ b/tests/test_subagent.py @@ -134,3 +134,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_subagent_caps.py b/tests/test_subagent_caps.py index 1c800c9..4a074d7 100644 --- a/tests/test_subagent_caps.py +++ b/tests/test_subagent_caps.py @@ -1,4 +1,4 @@ -"""Smoke for spawn_subagent depth and fan-out caps. +"""Test of spawn_subagent depth and fan-out caps. Both caps come from /config.toml; we install a temporary config that sets them low (max_fanout=1, max_depth=1) and verify @@ -118,3 +118,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_subagent_routing.py b/tests/test_subagent_routing.py index 7601d2d..50dc39a 100644 --- a/tests/test_subagent_routing.py +++ b/tests/test_subagent_routing.py @@ -1,4 +1,4 @@ -"""Smoke for permission_response routing root → subagent. +"""Test of permission_response routing root → subagent. Drives `_ChildState._handle_parent_event` directly with a fake subagent pipe and verifies that: @@ -97,3 +97,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_submit_handler.py b/tests/test_submit_handler.py index 7cd7fa9..1cf3542 100644 --- a/tests/test_submit_handler.py +++ b/tests/test_submit_handler.py @@ -1,4 +1,4 @@ -"""Smoke for the CLI submit-handler state machine (issues #68 + #69). +"""Test of the CLI submit-handler state machine (issues #68 + #69). Replaces the old `test_input_queue.py` — issue #68 deletes the local input queue and replaces it with mid-turn `user_note` @@ -254,3 +254,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 3431adb..8d7b64e 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -124,3 +124,8 @@ def _recv(timeout: float = 10.0) -> dict: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_token_meter.py b/tests/test_token_meter.py index 677b12a..6f6479b 100644 --- a/tests/test_token_meter.py +++ b/tests/test_token_meter.py @@ -1,4 +1,4 @@ -"""Smoke for the token / cost meter. +"""Test of the token / cost meter. Exercises (in-process): 1. Agent.token_usage accumulates across multiple LLM calls. @@ -390,3 +390,8 @@ def _check_format_right_zone() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_web_search.py b/tests/test_web_search.py index e1c7f59..f6f86bd 100644 --- a/tests/test_web_search.py +++ b/tests/test_web_search.py @@ -585,3 +585,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main() diff --git a/tests/test_write_file_append.py b/tests/test_write_file_append.py index c651b80..4c1565d 100644 --- a/tests/test_write_file_append.py +++ b/tests/test_write_file_append.py @@ -1,4 +1,4 @@ -"""Smoke for write_file's append mode. +"""Test of write_file's append mode. Exercises (in-process): 1. append=True creates a fresh file when one is missing. @@ -71,3 +71,8 @@ def main() -> None: if __name__ == "__main__": main() + + +def test_main() -> None: + """Entry point for pytest; runs the standalone main().""" + main()