119 lines
3.4 KiB
Python
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"] == []
|