113 lines
5.0 KiB
Python
113 lines
5.0 KiB
Python
"""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] / "skills" / "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"})
|