Merge #5289 into stage-b1
This commit is contained in:
commit
7df031592c
4 files changed
+149
-14
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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"] == []
|
||||
@@ -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"]]
|
||||
|
||||
Reference in new issue
Block a user