diff --git a/app/bot_cli.py b/app/bot_cli.py new file mode 100644 index 0000000..8d1ec11 --- /dev/null +++ b/app/bot_cli.py @@ -0,0 +1,33 @@ +"""Local dev CLI for the CM bot. Mirrors Telegram /1, /2, /3 plus +operational commands. No-arg invocation drops into a stdlib TUI menu. +""" + +import argparse +import sys + +from .cm_bot_hal import CM_BOT_HAL + + +def cmd_interactive(_args): + raise NotImplementedError("cmd_interactive is implemented in a later task") + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="bot_cli", + description="CM Bot dev CLI (mirrors Telegram triggers).", + ) + p.add_subparsers(dest="command") + return p + + +def main(argv=None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args.command is None: + return cmd_interactive(args) or 0 + return args.func(args) or 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_bot_cli.py b/tests/test_bot_cli.py new file mode 100644 index 0000000..f5a23a4 --- /dev/null +++ b/tests/test_bot_cli.py @@ -0,0 +1,40 @@ +"""Tests for the bot CLI (app.bot_cli). + +The CLI mirrors the Telegram bot's manual-trigger surface (Telegram +handlers /1, /2, /3) plus the operational ops (credit, transfer, +monitor-once). With no args, it drops into a stdlib TUI menu. + +These tests mock app.bot_cli.CM_BOT_HAL so they never touch the database +or cm99.net. The HAL class is imported at module load (which is a pure +import — no env reads), so we can patch the symbol bound on app.bot_cli +without affecting other tests. +""" + +import argparse +import contextlib +import io +import os +import sys +import unittest +from unittest import mock + +import app.bot_cli as bot_cli + + +class ParserSanityTests(unittest.TestCase): + def test_build_parser_returns_argument_parser(self): + parser = bot_cli.build_parser() + self.assertIsInstance(parser, argparse.ArgumentParser) + + def test_main_with_no_args_dispatches_to_interactive(self): + # When invoked with no subcommand, main() should drop into the + # TUI loop. We verify the dispatch by patching cmd_interactive to + # a no-op recorder. + with mock.patch.object(bot_cli, "cmd_interactive", return_value=0) as mocked: + rc = bot_cli.main([]) + mocked.assert_called_once() + self.assertEqual(rc, 0) + + +if __name__ == "__main__": + unittest.main()