stackchain-dashboard/tests/test_search_batch_plan.py
timmy c3d58d7111
All checks were successful
CI / lint (pull_request) Successful in 1m43s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped
feat: fit search batches to Today capacity (Closes #811)
2026-08-14 08:28:49 +00:00

177 lines
8.2 KiB
Python

import json
import subprocess
from pathlib import Path
SEARCH_BATCH_PLAN = Path(__file__).parents[1] / "frontend" / "search-batch-plan.js"
HTML = Path(__file__).parents[1] / "frontend" / "index.html"
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css"
def run_node(script):
return json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout)
def test_search_batch_selection_keeps_open_issues_across_pages_and_rejects_ineligible_results():
script = f"""
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
const states=[];
const flow=createSearchBatchPlan({{onChange:state=>states.push(state)}});
const first={{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,title:'First'}};
const second={{kind:'issue',state:'open',repository:'stackchain/api',number:17,title:'Second'}};
const pull={{kind:'pull',state:'open',repository:'stackchain/dashboard',number:810}};
const closed={{kind:'issue',state:'closed',repository:'stackchain/dashboard',number:808}};
flow.start();
const outcomes=[flow.toggle(first),flow.toggle(pull),flow.toggle(closed),flow.toggle(second)];
process.stdout.write(JSON.stringify({{outcomes,snapshot:flow.snapshot(),states}}));
"""
result = run_node(script)
assert result["outcomes"] == ["selected", "ineligible", "ineligible", "selected"]
assert result["snapshot"] == {
"active": True,
"count": 2,
"items": [
{"kind": "issue", "state": "open", "repository": "stackchain/dashboard", "number": 809, "title": "First"},
{"kind": "issue", "state": "open", "repository": "stackchain/api", "number": 17, "title": "Second"},
],
}
assert result["states"][-1]["count"] == 2
def test_search_batch_prepares_owned_issues_without_reclaiming_and_claims_unassigned_issues():
script = f"""
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
const calls=[];
const details={{
809:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:809,assigned_to_me:true}},
810:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:810,claimable:true}},
811:{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:811,claimable:false}},
}};
const flow=createSearchBatchPlan({{
resolve:item=>{{calls.push('resolve:'+item.number);return Promise.resolve(details[item.number]);}},
claim:detail=>{{calls.push('claim:'+detail.number);return Promise.resolve({{...detail,assigned_to_me:true}});}},
}});
Promise.all([
flow.prepare({{number:809}}),
flow.prepare({{number:810}}),
flow.prepare({{number:811}}).catch(error=>error.message),
]).then(results=>process.stdout.write(JSON.stringify({{results,calls}})));
"""
result = run_node(script)
assert result["calls"] == ["resolve:809", "resolve:810", "resolve:811", "claim:810"]
assert result["results"][0]["assigned_to_me"] is True
assert result["results"][1]["assigned_to_me"] is True
assert result["results"][2] == "This issue is no longer available to assign."
def test_mobile_search_exposes_touch_safe_durable_batch_planning_controls():
html = HTML.read_text()
dashboard = DASHBOARD.read_text()
css = CSS.read_text()
worker = (HTML.parent / "service-worker.js").read_text()
controller = SEARCH_BATCH_PLAN.read_text()
assert 'id="select-search-results"' in html
assert 'id="search-batch-actions"' in html
assert 'id="queue-selected-search-results"' in html
assert 'id="resume-search-batch"' in html
assert 'src="static/search-batch-plan.js"' in html
assert "mountSearchBatchPlanning(" in dashboard
assert "journalName:'search-today-batch'" in controller
assert "autoMount:false" in controller
assert "plan.toggle(result)" in controller
assert "processor.run(plan.snapshot().items, estimateValues())" in controller
assert "timeBudget:()" in controller
assert "persistEstimate" in controller
assert 'id="search-batch-estimate-review"' in html
assert 'data-search-batch-estimate' in controller
assert "processor.resume()" in controller
assert 'aria-label="Select ' in controller
assert "result.kind === 'issue' && result.state === 'open'" in controller
assert ".search-batch-actions" in css
assert "position:sticky" in css
assert "env(safe-area-inset-bottom)" in css
assert ".cmd-select-result" in css and "min-height:44px" in css
assert "BASE + 'static/search-batch-plan.js'" in worker
def test_search_batch_reviews_time_estimates_before_any_assignment_and_persists_queued_estimates():
script = f"""
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
const listeners={{}};
const elements={{}};
for (const id of [
'search-batch-actions','select-search-results','queue-selected-search-results',
'search-selection-status','cmd-search-action-status','resume-search-batch',
'open-palette','cmd-results','cancel-search-selection','search-batch-estimate-review',
'search-batch-estimate-summary','search-batch-estimate-list',
'cancel-search-batch-estimates','confirm-search-batch-estimates'
]) elements['#'+id]={{hidden:false,disabled:false,textContent:'',innerHTML:'',
addEventListener:(name,fn)=>listeners[id+':'+name]=fn,focus:()=>{{}}}};
const estimateInputs=[
{{dataset:{{searchBatchEstimate:'stackchain/dashboard#811'}},value:'25'}},
{{dataset:{{searchBatchEstimate:'stackchain/api#12'}},value:'30'}},
];
const document={{
querySelector:selector=>elements[selector] || null,
querySelectorAll:selector=>selector==='[data-search-batch-estimate]' ? estimateInputs : [],
}};
const calls=[];
let processorOptions;
const processor={{
run:(items,estimates)=>{{calls.push({{type:'run',items,estimates}});return Promise.resolve();}},
resume:()=>Promise.resolve(),pending:()=>0,
}};
const batchFactory=options=>{{processorOptions=options;return processor;}};
const planning={{capacity_minutes:90,estimates:{{'stackchain/existing#1':20}}}};
const todayWork={{limit:8,read:()=>[],planning:()=>planning,identity:item=>item.repository+'#'+item.number,
replacePlanning:value=>Object.assign(planning,value)}};
const todaySync={{enqueueConfiguration:()=>{{}},flush:()=>{{}}}};
const items=[
{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:811,title:'Capacity'}},
{{kind:'issue',state:'open',repository:'stackchain/api',number:12,title:'API'}},
];
let selected=items;
const mounted=createSearchBatchPlan.mount(
document,batchFactory,todayWork,()=> 'timmy',()=>Promise.resolve(),()=>'',()=>Promise.resolve('queued'),
todaySync,item=>item,index=>selected[index],()=>{{}},value=>value,value=>value
);
mounted.plan.start();
items.forEach(item=>mounted.plan.toggle(item));
listeners['queue-selected-search-results:click']();
const before={{
calls:calls.slice(),
reviewHidden:elements['#search-batch-estimate-review'].hidden,
actionsHidden:elements['#search-batch-actions'].hidden,
markup:elements['#search-batch-estimate-list'].innerHTML,
budget:processorOptions.timeBudget(),
}};
listeners['confirm-search-batch-estimates:click']();
processorOptions.persistEstimate(items[0],25);
processorOptions.onProgress({{status:'complete',queued:['stackchain/dashboard#811','stackchain/api#12'],failed:[]}});
const afterCompleteHidden=elements['#search-batch-estimate-review'].hidden;
process.stdout.write(JSON.stringify({{before,calls,afterCompleteHidden}}));
"""
result = run_node(script)
assert result["before"]["calls"] == []
assert result["before"]["reviewHidden"] is False
assert result["before"]["actionsHidden"] is True
assert "Capacity" in result["before"]["markup"]
assert "data-search-batch-estimate" in result["before"]["markup"]
assert result["before"]["budget"] == {"capacity_minutes": 90, "planned_minutes": 20}
assert result["calls"] == [{
"type": "run",
"items": [
{"kind": "issue", "state": "open", "repository": "stackchain/dashboard", "number": 811, "title": "Capacity"},
{"kind": "issue", "state": "open", "repository": "stackchain/api", "number": 12, "title": "API"},
],
"estimates": {"stackchain/dashboard#811": 25, "stackchain/api#12": 30},
}]
assert result["afterCompleteHidden"] is True