"""Bot wiring self-check (templates/bot_runner.py): which page events each raw bot line makes.""" import importlib.util import os import sys import time from pathlib import Path import pytest RUNNER = Path(__file__).resolve().parents[1] / "rpa-dashboard-no-ai" / "templates" / "bot_runner.py" @pytest.fixture def runner(): spec = importlib.util.spec_from_file_location("bot_runner", RUNNER) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) mod.STEP_MARKERS = [("[step 1]", "Initializing", 10), ("[step 2]", "Login", 40), ("[step 3]", "Process items", 70), ("[all done]", "Completed", 100)] mod.FRIENDLY_RULES = [("[login] done", "Logged in.", "success"), ("[login]", "Logging in...", "info"), ("could not save receipt", "Could not save the receipt.", "error")] return mod def steps(runner, line): return [d for e, d in runner.events_for(line) if e == "step"] def test_step_marker_moves_progress(runner): assert steps(runner, "[step 2] Opening the login page") == [{"label": "Login", "pct": 40}] assert steps(runner, "[STEP 3] Processing 12 items") == [{"label": "Process items", "pct": 70}] def test_common_word_does_not_move_progress(runner): assert steps(runner, "Login page loaded slowly") == [] assert steps(runner, "Completed loading the config") == [] def test_first_step_marker_wins(runner): assert steps(runner, "[step 1] retry after [step 3] timed out") == [{"label": "Initializing", "pct": 10}] def logs(runner, line): return [d for e, d in runner.events_for(line) if e == "log"] def test_friendly_rule_rewrites_the_line_and_first_rule_wins(runner): assert logs(runner, "[login] done in 2.1s") == [{"msg": "Logged in.", "type": "success"}] assert logs(runner, "[LOGIN] sending credentials") == [{"msg": "Logging in...", "type": "info"}] def test_unmatched_non_error_lines_are_hidden(runner): for line in ["Loading config from settings.ini", "[step 2] Opening the login page", "warning: slow page"]: assert logs(runner, line) == [], line @pytest.mark.parametrize("line", [ "ERROR: portal timed out", "Traceback (most recent call last):", "ValueError: bad date", "Exception: boom", "FATAL disk full", "Login failed for user 3"]) def test_unmatched_error_line_is_always_shown_raw(runner, line): assert logs(runner, line) == [{"msg": line, "type": "error"}] def test_matched_error_line_shows_its_friendly_message(runner): assert logs(runner, "ERROR could not save receipt 42") == [{"msg": "Could not save the receipt.", "type": "error"}] def test_error_on_a_step_line_moves_progress_and_is_shown(runner): line = "[step 3] ERROR item 2 rejected" assert runner.events_for(line) == [("step", {"label": "Process items", "pct": 70}), ("log", {"msg": line, "type": "error"})] def run_bot(runner, tmp_path, script): """Run a fake bot through runner.run(); return [(seconds since start, event, data)].""" bot = tmp_path / "fake_bot.py" bot.write_text(script, encoding="utf-8") runner.BOT_CMD, runner.BOT_CWD, runner.RAW_LOG = [sys.executable, str(bot)], tmp_path, tmp_path / "raw.log" t0, sent = time.monotonic(), [] runner.run(lambda event, data: sent.append((time.monotonic() - t0, event, data))) return sent def test_run_streams_bot_output_live_from_a_child_process(runner, tmp_path): sent = run_bot(runner, tmp_path, "import os, time\n" "print('[step 1] pid', os.getpid())\n" "print('[login] done')\n" "time.sleep(1)\n" "print('[all done] ✓ 3 items')\n") events = [(e, d) for _, e, d in sent] assert events == [("status", {"state": "running", "msg": "Running…"}), ("step", {"label": "Initializing", "pct": 10}), ("log", {"msg": "Logged in.", "type": "success"}), ("step", {"label": "Completed", "pct": 100}), ("status", {"state": "done", "msg": "Completed"})] assert sent[3][0] - sent[1][0] > 0.5, "lines must reach the page while the bot runs, not at exit" raw = (tmp_path / "raw.log").read_text(encoding="utf-8") assert "[login] done" in raw and "✓ 3 items" in raw # raw lines kept for audit assert f"pid {os.getpid()}" not in raw # the bot ran in its own process def test_run_ends_in_error_when_the_bot_fails(runner, tmp_path): sent = run_bot(runner, tmp_path, "print('[step 1] start')\nraise SystemExit(3)\n") assert sent[-1][1:] == ("status", {"state": "error", "msg": "Failed (exit code 3)"}) def test_run_ends_in_error_when_the_bot_cannot_start(runner, tmp_path): runner.BOT_CMD, runner.BOT_CWD, runner.RAW_LOG = ["no-such-bot-xyz"], tmp_path, tmp_path / "raw.log" sent = [] runner.run(lambda event, data: sent.append((event, data))) assert sent[-2][0] == "log" and sent[-2][1]["type"] == "error" assert sent[-1] == ("status", {"state": "error", "msg": "Could not start the bot"})