feat(bot_cli): implement interactive TUI menu and add subparser entry
This commit is contained in:
parent
f472a94916
commit
7011c6bada
@ -8,8 +8,43 @@ import sys
|
|||||||
from .cm_bot_hal import CM_BOT_HAL
|
from .cm_bot_hal import CM_BOT_HAL
|
||||||
|
|
||||||
|
|
||||||
|
# Map TUI shortcuts to argparse subcommand names so the REPL reuses the
|
||||||
|
# same dispatch table as one-shot invocations.
|
||||||
|
_TUI_ALIASES = {"1": "register", "2": "set-pin", "3": "insert-user"}
|
||||||
|
|
||||||
|
|
||||||
def cmd_interactive(_args):
|
def cmd_interactive(_args):
|
||||||
raise NotImplementedError("cmd_interactive is implemented in a later task")
|
"""Telegram-style menu in a TTY loop. stdlib only."""
|
||||||
|
print("CM Bot CLI — interactive (type 'q' to quit, '?' for menu)")
|
||||||
|
while True:
|
||||||
|
print()
|
||||||
|
print(" 1 Register / get next account")
|
||||||
|
print(" 2 <whatsapp_link> Set security PIN")
|
||||||
|
print(" 3 <f_username> <t_username> Insert into user table")
|
||||||
|
print(" credit <username> <password> Read account credit")
|
||||||
|
print(" transfer <fu> <fp> <tu> <tp> One-shot credit transfer")
|
||||||
|
print(" monitor [N] Run monitor once (default 20)")
|
||||||
|
print(" q Quit")
|
||||||
|
try:
|
||||||
|
line = input("> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line in ("q", "quit", "exit"):
|
||||||
|
return
|
||||||
|
if line in ("?", "help", "menu"):
|
||||||
|
continue
|
||||||
|
argv = line.split()
|
||||||
|
argv[0] = _TUI_ALIASES.get(argv[0], argv[0])
|
||||||
|
try:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
args.func(args)
|
||||||
|
except SystemExit:
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"ERROR: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def _print_user(user: dict) -> None:
|
def _print_user(user: dict) -> None:
|
||||||
@ -118,6 +153,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
sp.add_argument("--target", type=int, default=20)
|
sp.add_argument("--target", type=int, default=20)
|
||||||
sp.set_defaults(func=cmd_monitor_once)
|
sp.set_defaults(func=cmd_monitor_once)
|
||||||
|
|
||||||
|
sp = sub.add_parser("interactive", help="Drop into the TUI menu.")
|
||||||
|
sp.set_defaults(func=cmd_interactive)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -241,5 +241,53 @@ class CmdMonitorOnceTests(unittest.TestCase):
|
|||||||
self.assertEqual(args.target, 20)
|
self.assertEqual(args.target, 20)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdInteractiveTests(unittest.TestCase):
|
||||||
|
@mock.patch("builtins.input", side_effect=["q"])
|
||||||
|
@mock.patch.object(bot_cli, "CM_BOT_HAL")
|
||||||
|
def test_q_exits_cleanly(self, mock_hal_class, mock_input):
|
||||||
|
out = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out):
|
||||||
|
bot_cli.cmd_interactive(argparse.Namespace())
|
||||||
|
self.assertIn("CM Bot CLI", out.getvalue())
|
||||||
|
|
||||||
|
@mock.patch("builtins.input", side_effect=["", "q"])
|
||||||
|
@mock.patch.object(bot_cli, "CM_BOT_HAL")
|
||||||
|
def test_blank_line_continues_loop(self, mock_hal_class, mock_input):
|
||||||
|
out = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out):
|
||||||
|
bot_cli.cmd_interactive(argparse.Namespace())
|
||||||
|
self.assertGreaterEqual(out.getvalue().count("Register / get next account"), 2)
|
||||||
|
|
||||||
|
@mock.patch("builtins.input", side_effect=EOFError)
|
||||||
|
@mock.patch.object(bot_cli, "CM_BOT_HAL")
|
||||||
|
def test_eof_exits_cleanly(self, mock_hal_class, mock_input):
|
||||||
|
out = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out):
|
||||||
|
bot_cli.cmd_interactive(argparse.Namespace())
|
||||||
|
self.assertIn("CM Bot CLI", out.getvalue())
|
||||||
|
|
||||||
|
@mock.patch("builtins.input", side_effect=["1", "q"])
|
||||||
|
@mock.patch.object(bot_cli, "CM_BOT_HAL")
|
||||||
|
def test_alias_1_dispatches_to_register(self, mock_hal_class, mock_input):
|
||||||
|
mock_hal = mock_hal_class.return_value
|
||||||
|
mock_hal.get_user_api.return_value = {
|
||||||
|
"username": "u", "password": "p", "link": "l",
|
||||||
|
}
|
||||||
|
out = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out):
|
||||||
|
bot_cli.cmd_interactive(argparse.Namespace())
|
||||||
|
self.assertIn("Username: u", out.getvalue())
|
||||||
|
mock_hal.get_user_api.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch("builtins.input", side_effect=["nonsense", "q"])
|
||||||
|
@mock.patch.object(bot_cli, "CM_BOT_HAL")
|
||||||
|
def test_unknown_subcommand_keeps_loop_alive(self, mock_hal_class, mock_input):
|
||||||
|
out = io.StringIO()
|
||||||
|
err = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||||
|
bot_cli.cmd_interactive(argparse.Namespace())
|
||||||
|
self.assertIn("CM Bot CLI", out.getvalue())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user