From f472a949160a1d3478d69a7cfb744a75f5185540 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sat, 2 May 2026 16:59:55 +0800 Subject: [PATCH] feat(bot_cli): add monitor-once subcommand --- app/bot_cli.py | 20 ++++++++++++++++ tests/test_bot_cli.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/app/bot_cli.py b/app/bot_cli.py index ef788c3..5c9ee01 100644 --- a/app/bot_cli.py +++ b/app/bot_cli.py @@ -67,6 +67,22 @@ def cmd_transfer(args): )) +def cmd_monitor_once(args): + bot = CM_BOT_HAL() + available = bot.get_all_available_acc() + print(f"Available accounts: {len(available)} (target: {args.target})") + if len(available) >= args.target: + print("Already at target; nothing to do.") + return + for _ in range(len(available), args.target): + try: + user = bot.create_new_acc() + print(f"Created: {user['username']}") + except Exception as exc: + print(f"ERROR creating account: {exc}", file=sys.stderr) + sys.exit(1) + + def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="bot_cli", @@ -98,6 +114,10 @@ def build_parser() -> argparse.ArgumentParser: sp.add_argument("t_password") sp.set_defaults(func=cmd_transfer) + sp = sub.add_parser("monitor-once", aliases=["monitor"], help="One iteration of the auto-create monitor.") + sp.add_argument("--target", type=int, default=20) + sp.set_defaults(func=cmd_monitor_once) + return p diff --git a/tests/test_bot_cli.py b/tests/test_bot_cli.py index d268fa1..8e56770 100644 --- a/tests/test_bot_cli.py +++ b/tests/test_bot_cli.py @@ -188,5 +188,58 @@ class CmdTransferTests(unittest.TestCase): self.assertIs(args.func, bot_cli.cmd_transfer) +class CmdMonitorOnceTests(unittest.TestCase): + @mock.patch.object(bot_cli, "CM_BOT_HAL") + def test_does_nothing_when_already_at_target(self, mock_hal_class): + mock_hal = mock_hal_class.return_value + mock_hal.get_all_available_acc.return_value = [{"username": f"u{i}"} for i in range(20)] + out = io.StringIO() + with contextlib.redirect_stdout(out): + bot_cli.cmd_monitor_once(argparse.Namespace(target=20)) + text = out.getvalue() + self.assertIn("Available accounts: 20", text) + self.assertIn("Already at target", text) + mock_hal.create_new_acc.assert_not_called() + + @mock.patch.object(bot_cli, "CM_BOT_HAL") + def test_creates_accounts_until_target(self, mock_hal_class): + mock_hal = mock_hal_class.return_value + mock_hal.get_all_available_acc.return_value = [{"username": "u1"}, {"username": "u2"}] + mock_hal.create_new_acc.side_effect = [ + {"username": "u3", "password": "p3", "link": "l3"}, + {"username": "u4", "password": "p4", "link": "l4"}, + {"username": "u5", "password": "p5", "link": "l5"}, + ] + out = io.StringIO() + with contextlib.redirect_stdout(out): + bot_cli.cmd_monitor_once(argparse.Namespace(target=5)) + text = out.getvalue() + self.assertEqual(mock_hal.create_new_acc.call_count, 3) + self.assertIn("Created: u3", text) + self.assertIn("Created: u4", text) + self.assertIn("Created: u5", text) + + @mock.patch.object(bot_cli, "CM_BOT_HAL") + def test_create_failure_exits_1(self, mock_hal_class): + mock_hal = mock_hal_class.return_value + mock_hal.get_all_available_acc.return_value = [] + mock_hal.create_new_acc.side_effect = RuntimeError("fail login") + with self.assertRaises(SystemExit) as cm: + bot_cli.cmd_monitor_once(argparse.Namespace(target=1)) + self.assertEqual(cm.exception.code, 1) + + def test_monitor_once_subparser_dispatches(self): + parser = bot_cli.build_parser() + args = parser.parse_args(["monitor-once", "--target", "7"]) + self.assertIs(args.func, bot_cli.cmd_monitor_once) + self.assertEqual(args.target, 7) + + def test_monitor_alias_dispatches(self): + parser = bot_cli.build_parser() + args = parser.parse_args(["monitor"]) + self.assertIs(args.func, bot_cli.cmd_monitor_once) + self.assertEqual(args.target, 20) + + if __name__ == "__main__": unittest.main()