Merge #5289 into stage-b1

This commit is contained in:
nesquena-hermes committed 2026-06-30 22:45:06 +00:00
commit 7df031592c
4 files changed
+149 -14

No files matched your search

-4
View File
@@ -2380,10 +2380,6 @@ async function runKanbanDispatcher(){
// Confirmation dialog first because this actually consumes API budget on
// each spawned worker. Result toast surfaces what happened so users see
// the dispatcher actually doing work.
if (!_kanbanCurrentBoard) {
showToast(t('kanban_unavailable') || 'Kanban unavailable', 'error');
return;
}
_kanbanIsDispatching = true;
_setKanbanDispatcherButtonsDisabled(true);
+16
View File
@@ -0,0 +1,16 @@
def extract_function(js: str, name: str, prefix: str = "function") -> str:
marker = f"{prefix} {name}("
start = js.find(marker)
assert start >= 0, f"{name} function not found in static/panels.js"
brace = js.find("{", start)
assert brace >= 0, f"{name} opening brace not found"
depth = 1
i = brace + 1
while i < len(js) and depth > 0:
if js[i] == "{":
depth += 1
elif js[i] == "}":
depth -= 1
i += 1
assert depth == 0, f"{name} function braces unbalanced"
return js[start:i]
@@ -0,0 +1,118 @@
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"] == []
+15 -10
View File
@@ -1,6 +1,8 @@
from pathlib import Path
import re
from tests.js_source_extract import extract_function
ROOT = Path(__file__).resolve().parents[1]
INDEX = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
PANELS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
@@ -526,6 +528,15 @@ def test_kanban_dispatcher_inflight_guard_prevents_double_click_toast_confusion(
assert 'btnKanbanPreviewDispatcher' in INDEX
def test_kanban_dispatcher_no_longer_blocks_default_board():
"""Pin removal of the previous null-board guard in runKanbanDispatcher()."""
run_source = extract_function(PANELS, "runKanbanDispatcher", prefix="async function")
assert "if (!_kanbanCurrentBoard)" not in run_source, (
"runKanbanDispatcher() must not block when _kanbanCurrentBoard is null; "
"default board should dispatch through a board-less path."
)
def test_kanban_board_has_native_css_classes():
for selector in (
".kanban-board",
@@ -1107,17 +1118,11 @@ def test_kanban_board_color_is_validated_against_css_injection():
"""
import json
import subprocess
fn_source = extract_function(PANELS, "_kanbanSafeColor")
script = """
const fs = require('fs');
const src = fs.readFileSync('static/panels.js', 'utf8');
const start = src.indexOf('function _kanbanSafeColor');
if (start < 0) { console.error('_kanbanSafeColor missing'); process.exit(2); }
// Grab the function body up to and including the closing `}` line.
const tail = src.slice(start);
const end = tail.indexOf('\\n}\\n') + 2;
const fn = tail.slice(0, end);
const fnSource = __FN__;
const ctx = {};
new Function('out', fn + '; out.fn = _kanbanSafeColor;')(ctx);
new Function('out', fnSource + '; out.fn = _kanbanSafeColor;')(ctx);
const cases = [
['#fff', '#fff'],
['#3b82f6', '#3b82f6'],
@@ -1137,7 +1142,7 @@ const results = cases.map(([input, expected]) => ({
input, expected, actual: ctx.fn(input)
}));
console.log(JSON.stringify(results));
"""
""".replace("__FN__", json.dumps(fn_source))
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
results = json.loads(result.stdout)
failures = [r for r in results if r["actual"] != r["expected"]]