Files
hermes-webui/tests/test_issue5231_default_kanban_dispatch.py
T

119 lines
3.4 KiB
Python

import json
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
from tests.js_source_extract import extract_function
ROOT = Path(__file__).resolve().parents[1]
PANELS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
def _run_node(js: str) -> dict:
with tempfile.NamedTemporaryFile("w", suffix=".cjs", encoding="utf-8", dir=ROOT, delete=False) as script:
script.write(js)
script_path = Path(script.name)
try:
proc = subprocess.run(
[NODE, str(script_path)],
cwd=ROOT,
capture_output=True,
text=True,
timeout=30,
)
finally:
script_path.unlink(missing_ok=True)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip())
return json.loads(proc.stdout.strip())
def _run_dispatcher_case(function_name: str, current_board, confirm: bool) -> dict:
fn_source = extract_function(PANELS, function_name, prefix="async function")
source = f"""
const fnSource = {json.dumps(fn_source)};
eval(fnSource);
let _kanbanCurrentBoard = {json.dumps(current_board)};
let _kanbanIsDispatching = false;
const apiCalls = [];
const confirmCalls = [];
const toastCalls = [];
const showConfirmDialog = async (opts) => {{
confirmCalls.push(opts);
return {str(confirm).lower()};
}};
const api = async (path, opts = {{}}) => {{
apiCalls.push({{ path, method: opts.method || "GET", body: ("body" in opts ? opts.body : null) }});
return {{
spawned: 1,
skipped_unassigned: 0,
skipped_nonspawnable: 0,
promoted: 0,
auto_blocked: 0,
}};
}};
const showToast = (...args) => {{
toastCalls.push(args);
}};
const t = (key) => key;
const _setKanbanDispatcherButtonsDisabled = () => {{}};
const loadKanban = async () => {{}};
const _kanbanFormatDispatchResult = () => "ok";
const document = {{
querySelectorAll: () => [],
}};
(async () => {{
try {{
await {function_name}();
console.log(JSON.stringify({{
apiCalls,
confirmCalls,
toastCalls,
ok: true,
}}));
}} catch (err) {{
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
}}
}})();
"""
return _run_node(source)
def test_default_board_real_dispatch_sends_boardless_post_when_confirmed():
result = _run_dispatcher_case("runKanbanDispatcher", None, True)
assert result["confirmCalls"], "expected confirmation dialog"
assert result["apiCalls"] == [
{"path": "/api/kanban/dispatch?max=8", "method": "POST", "body": None},
]
def test_default_board_preview_dispatch_stays_dry_run_and_boardless():
result = _run_dispatcher_case("nudgeKanbanDispatcher", None, True)
assert result["confirmCalls"] == []
assert result["apiCalls"] == [
{"path": "/api/kanban/dispatch?dry_run=1&max=8", "method": "POST", "body": None},
]
def test_named_board_real_dispatch_preserves_board_parameter():
result = _run_dispatcher_case("runKanbanDispatcher", "qa", True)
assert result["apiCalls"] == [
{"path": "/api/kanban/dispatch?max=8&board=qa", "method": "POST", "body": None},
]
def test_confirmation_decline_prevents_real_dispatch_post():
result = _run_dispatcher_case("runKanbanDispatcher", None, False)
assert result["confirmCalls"], "expected confirmation attempt before dispatch"
assert result["apiCalls"] == []