Files
bot-bottle/tests/unit/test_cli_dispatch.py
didericis 632ab002ed
lint / lint (push) Successful in 1m52s
test / unit (pull_request) Successful in 46s
test / integration (pull_request) Successful in 16s
test / coverage (pull_request) Successful in 1m2s
ci(coverage): risk-weighted coverage policy + diff-coverage gate
Adopt ADR 0004: stop chasing a single global coverage number and
measure what matters instead.

- Omit the genuinely-interactive `cli/init.py` shell (read_tty_line
  prompt loops) alongside the existing `cli/tui.py`, with a rationale
  comment in .coveragerc. Subprocess/backend orchestration is NOT
  omitted — it stays visible and is scored via the integration suite.
- scripts/coverage.sh runs unit + integration under one coverage
  measurement (the policy's yardstick) and can report the critical
  security/logic core held to the >=90% target.
- scripts/diff_coverage.py is a stdlib-only gate (no diff-cover dep):
  new/changed executable lines must be >=90% covered. This is the
  enforced regression guard; the global number is informational.
- CI gains a `coverage` job: combined report + the diff-coverage gate.
- Unit-test `cli/__init__.py` dispatch/exit-code mapping (it's logic,
  not I/O, so it earns tests rather than an omit).

Combined unit+integration coverage now reports 83% global / 87% across
the critical modules; per-module ratcheting toward 90% is the ongoing
work this policy frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkwFXLFff9PYPy4wgVBJp9
2026-06-25 21:29:08 -04:00

83 lines
2.6 KiB
Python

"""Unit: top-level CLI dispatch in bot_bottle.cli.main (ADR 0004).
`cli/__init__.py` is dispatch + exit-code mapping, not interactive I/O,
so it carries real unit tests rather than being omitted like the
`cli/init` / `cli/tui` shells."""
from __future__ import annotations
import io
import unittest
from unittest.mock import patch
import bot_bottle.cli as climod
from bot_bottle.cli import main
from bot_bottle.log import Die
from bot_bottle.manifest import ManifestError
class TestMainDispatch(unittest.TestCase):
def test_no_args_prints_usage_returns_2(self) -> None:
with patch("sys.stderr", io.StringIO()):
self.assertEqual(2, main([]))
def test_help_flags_return_0(self) -> None:
with patch("sys.stderr", io.StringIO()):
self.assertEqual(0, main(["-h"]))
self.assertEqual(0, main(["--help"]))
def test_unknown_command_dies(self) -> None:
with patch("sys.stderr", io.StringIO()):
with self.assertRaises(Die):
main(["definitely-not-a-command"])
def test_handler_return_code_passthrough(self) -> None:
def handler(_rest: list[str]) -> int:
return 7
with patch.dict(climod.COMMANDS, {"x": handler}):
self.assertEqual(7, main(["x"]))
def test_handler_none_return_becomes_0(self) -> None:
def handler(_rest: list[str]) -> int | None:
return None
with patch.dict(climod.COMMANDS, {"x": handler}):
self.assertEqual(0, main(["x"]))
def test_args_forwarded_to_handler(self) -> None:
seen: list[list[str]] = []
def handler(rest: list[str]) -> int:
seen.append(rest)
return 0
with patch.dict(climod.COMMANDS, {"x": handler}):
main(["x", "a", "b"])
self.assertEqual([["a", "b"]], seen)
def test_manifest_error_maps_to_1(self) -> None:
def boom(_rest: list[str]) -> int:
raise ManifestError("bad manifest")
with patch.dict(climod.COMMANDS, {"x": boom}), patch("sys.stderr", io.StringIO()):
self.assertEqual(1, main(["x"]))
def test_die_maps_to_its_code(self) -> None:
def boom(_rest: list[str]) -> int:
raise Die(3)
with patch.dict(climod.COMMANDS, {"x": boom}):
self.assertEqual(3, main(["x"]))
def test_keyboard_interrupt_maps_to_130(self) -> None:
def boom(_rest: list[str]) -> int:
raise KeyboardInterrupt()
with patch.dict(climod.COMMANDS, {"x": boom}):
self.assertEqual(130, main(["x"]))
if __name__ == "__main__":
unittest.main()