460 lines
22 KiB
Python
460 lines
22 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_release_planning_requires_selected_issues_from_one_repository():
|
|
script = f"""
|
|
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
|
const flow=createSearchBatchPlan();
|
|
flow.start();
|
|
flow.toggle({{kind:'issue',state:'open',repository:'stackchain/dashboard',number:815}});
|
|
const one=flow.releaseRepository();
|
|
flow.toggle({{kind:'issue',state:'open',repository:'stackchain/api',number:17}});
|
|
process.stdout.write(JSON.stringify({{one,mixed:flow.releaseRepository()}}));
|
|
"""
|
|
|
|
assert run_node(script) == {"one": "stackchain/dashboard", "mixed": None}
|
|
|
|
|
|
def test_week_batch_validates_every_row_before_claiming_and_flushes_one_staged_week():
|
|
script = f"""
|
|
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
|
const createWeekBatchPlan=createSearchBatchPlan.createWeekBatchPlan;
|
|
const calls=[];
|
|
const days=[
|
|
{{date:'2026-08-21',label:'Fri, Aug 21'}},
|
|
{{date:'2026-08-22',label:'Sat, Aug 22'}},
|
|
];
|
|
const state={{
|
|
'2026-08-21':{{ids:['stackchain/dashboard#1'],capacity_minutes:90,estimates:{{'stackchain/dashboard#1':30}}}},
|
|
'2026-08-22':{{ids:[],capacity_minutes:60,estimates:{{}}}},
|
|
}};
|
|
const week={{
|
|
load:async()=>{{calls.push('load');}},dates:()=>days,
|
|
day:date=>state[date],placement:id=>null,
|
|
place:(id,date,estimate,options)=>{{calls.push(['place',id,date,estimate,options.move]);state[date].ids.push(id);state[date].estimates[id]=estimate;return true;}},
|
|
flush:async()=>{{calls.push('flush');}},
|
|
}};
|
|
const items=[
|
|
{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:2,title:'Two'}},
|
|
{{kind:'issue',state:'open',repository:'stackchain/api',number:3,title:'Three'}},
|
|
];
|
|
const flow=createWeekBatchPlan({{
|
|
week,identity:item=>item.repository+'#'+item.number,
|
|
prepare:async item=>{{calls.push(['claim',item.number]);return {{...item,assigned_to_me:true}};}},
|
|
}});
|
|
(async()=>{{
|
|
const invalid=await flow.run(items,{{
|
|
'stackchain/dashboard#2':{{date:'2026-08-21',estimate:25}},
|
|
'stackchain/api#3':{{date:'2026-08-22',estimate:0}},
|
|
}});
|
|
const before=calls.slice();
|
|
const planned=await flow.run(items,{{
|
|
'stackchain/dashboard#2':{{date:'2026-08-21',estimate:25}},
|
|
'stackchain/api#3':{{date:'2026-08-22',estimate:20}},
|
|
}});
|
|
process.stdout.write(JSON.stringify({{invalid,before,planned,calls}}));
|
|
}})();
|
|
"""
|
|
|
|
result = run_node(script)
|
|
assert result["invalid"]["status"] == "invalid"
|
|
assert result["invalid"]["errors"] == [{"id": "stackchain/api#3", "reason": "estimate-required"}]
|
|
assert result["before"] == ["load"]
|
|
assert result["planned"]["status"] == "planned"
|
|
assert result["planned"]["planned"] == 2
|
|
assert result["planned"]["failed"] == []
|
|
assert result["calls"].count("flush") == 1
|
|
assert [call for call in result["calls"] if isinstance(call, list) and call[0] == "claim"] == [
|
|
["claim", 2], ["claim", 3],
|
|
]
|
|
|
|
|
|
def test_week_batch_stages_details_only_for_successfully_placed_canonical_items():
|
|
script = f"""
|
|
const createSearchBatchPlan=require({json.dumps(str(SEARCH_BATCH_PLAN))});
|
|
const flow=createSearchBatchPlan.createWeekBatchPlan({{
|
|
week:{{load:async()=>{{}},dates:()=>[{{date:'2026-08-21'}}],day:()=>({{ids:[],estimates:{{}}}}),
|
|
placement:()=>null,place:id=>!id.endsWith('#2'),
|
|
rememberPendingItem:(id,item)=>calls.push(['details',id,item.title]),flush:async()=>calls.push(['flush'])}},
|
|
prepare:async item=>({{...item,title:'Canonical '+item.number}}),
|
|
identity:item=>item.repository+'#'+item.number,
|
|
}});
|
|
const calls=[];const items=[
|
|
{{repository:'r',number:1,title:'Search one'}},{{repository:'r',number:2,title:'Search two'}}];
|
|
(async()=>{{const outcome=await flow.run(items,{{'r#1':{{date:'2026-08-21',estimate:20}},'r#2':{{date:'2026-08-21',estimate:30}}}});
|
|
process.stdout.write(JSON.stringify({{outcome,calls}}));}})();
|
|
"""
|
|
|
|
result = run_node(script)
|
|
assert result["outcome"] == {
|
|
"status": "partial", "planned": 1,
|
|
"failed": [{"id": "r#2", "reason": "assigned-not-planned"}],
|
|
}
|
|
assert result["calls"] == [["details", "r#1", "Canonical 1"], ["flush"]]
|
|
|
|
|
|
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="defer-selected-search-results"' in html
|
|
assert 'id="plan-selected-search-results"' in html
|
|
assert 'id="search-release-review"' in html
|
|
assert 'id="search-release-milestone"' in html
|
|
assert 'id="search-release-due-date"' in html
|
|
assert 'id="resume-search-batch"' in html
|
|
assert 'src="static/search-batch-plan.js"' in html
|
|
assert "mountSearchBatchPlanning(" in dashboard
|
|
assert "escapeHtml, escAttr, laterWork, laterPicker" in dashboard
|
|
assert "journalName:'search-today-batch'" in controller
|
|
assert "journalName:'search-later-batch'" in controller
|
|
assert "journalName:'search-release-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 ".search-release-review" in css and "100dvh" in css
|
|
assert ".search-release-actions" in css and "env(safe-area-inset-bottom)" in css
|
|
assert "BASE + 'static/search-batch-plan.js'" in worker
|
|
|
|
|
|
def test_mobile_search_exposes_batch_week_review_with_per_issue_day_and_estimate_controls():
|
|
html = HTML.read_text()
|
|
dashboard = DASHBOARD.read_text()
|
|
css = CSS.read_text()
|
|
controller = SEARCH_BATCH_PLAN.read_text()
|
|
|
|
assert 'id="week-selected-search-results"' in html
|
|
assert 'id="search-week-batch-review" role="dialog" aria-modal="true"' in html
|
|
assert 'id="search-week-batch-list"' in html
|
|
assert 'id="search-week-batch-summary" aria-live="polite"' in html
|
|
assert 'id="cancel-search-week-batch"' in html
|
|
assert 'id="confirm-search-week-batch"' in html
|
|
assert "createWeekBatchPlan" in controller
|
|
assert "data-search-week-batch-date" in controller
|
|
assert "data-search-week-batch-estimate" in controller
|
|
assert "overload-confirmation-required" in controller
|
|
assert "weekPlan, localStorage" in dashboard
|
|
assert ".search-week-batch-review" in css
|
|
assert "max-height:calc(100dvh - 48px)" in css
|
|
assert "min-height:44px" in css
|
|
assert "env(safe-area-inset-bottom)" in css
|
|
|
|
|
|
def test_selected_search_issues_choose_one_future_time_before_durable_batch_defer():
|
|
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',
|
|
'defer-selected-search-results','plan-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','search-release-review',
|
|
'search-release-summary','search-release-milestone','search-release-due-date','search-release-list',
|
|
'cancel-search-release','confirm-search-release'
|
|
]) elements['#'+id]={{hidden:false,disabled:false,textContent:'',innerHTML:'',
|
|
addEventListener:(name,fn)=>listeners[id+':'+name]=fn,focus:()=>{{}}}};
|
|
const document={{querySelector:selector=>elements[selector] || null,querySelectorAll:()=>[]}};
|
|
const processorOptions=[];
|
|
const runs=[];
|
|
const processors=[];
|
|
const batchFactory=options=>{{
|
|
processorOptions.push(options);
|
|
const processor={{
|
|
run:(items,estimates,context)=>{{runs.push({{items,estimates,context}});return Promise.resolve();}},
|
|
resume:()=>Promise.resolve(),pending:()=>0,
|
|
}};
|
|
processors.push(processor);
|
|
return processor;
|
|
}};
|
|
const planning={{capacity_minutes:null,estimates:{{}}}};
|
|
const todayWork={{limit:8,read:()=>[],planning:()=>planning,identity:item=>item.repository+'#'+item.number,
|
|
replacePlanning:()=>{{}}}};
|
|
const laterCalls=[];
|
|
const laterWork={{defer:(item,until)=>{{laterCalls.push([item.number,until]);return 'deferred';}}}};
|
|
let pickerRequest=null;
|
|
const laterPicker={{open:(payload,trigger,context)=>{{pickerRequest={{payload,trigger,context}};return true;}}}};
|
|
const item={{kind:'issue',state:'open',repository:'stackchain/dashboard',number:813,title:'Batch defer'}};
|
|
const mounted=createSearchBatchPlan.mount(
|
|
document,batchFactory,todayWork,()=> 'timmy',()=>Promise.resolve(),()=>'',()=>Promise.resolve('queued'),
|
|
{{enqueueConfiguration:()=>{{}},flush:()=>{{}}}},value=>({{...value,accepted:true}}),()=>item,()=>{{}},
|
|
value=>value,value=>value,laterWork,laterPicker
|
|
);
|
|
mounted.plan.start();
|
|
mounted.plan.toggle(item);
|
|
listeners['defer-selected-search-results:click']({{currentTarget:elements['#defer-selected-search-results']}});
|
|
const before={{runs:runs.slice(),count:pickerRequest.payload.items.length,context:pickerRequest.context,journals:processorOptions.map(value=>value.journalName)}};
|
|
pickerRequest.payload.confirm(new Date('2026-08-15T09:00:00.000Z'))();
|
|
const laterResult=processorOptions[1].queue({{...item,assigned_to_me:true}},{{until:'2026-08-15T09:00:00.000Z'}});
|
|
process.stdout.write(JSON.stringify({{before,runs,laterResult,laterCalls}}));
|
|
"""
|
|
|
|
result = run_node(script)
|
|
assert result["before"] == {
|
|
"runs": [],
|
|
"count": 1,
|
|
"context": "search-batch",
|
|
"journals": ["search-today-batch", "search-later-batch", "search-release-batch"],
|
|
}
|
|
assert result["runs"] == [{
|
|
"items": [{
|
|
"kind": "issue", "state": "open", "repository": "stackchain/dashboard",
|
|
"number": 813, "title": "Batch defer",
|
|
}],
|
|
"estimates": {},
|
|
"context": {"until": "2026-08-15T09:00:00.000Z"},
|
|
}]
|
|
assert result["laterResult"] == "queued"
|
|
assert result["laterCalls"] == [[813, "2026-08-15T09:00:00.000Z"]]
|
|
|
|
|
|
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',
|
|
'defer-selected-search-results','plan-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','search-release-review',
|
|
'search-release-summary','search-release-milestone','search-release-due-date','search-release-list',
|
|
'cancel-search-release','confirm-search-release'
|
|
]) 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=[];
|
|
const processorOptions=[];
|
|
const processor={{
|
|
run:(items,estimates)=>{{calls.push({{type:'run',items,estimates}});return Promise.resolve();}},
|
|
resume:()=>Promise.resolve(),pending:()=>0,
|
|
}};
|
|
const batchFactory=options=>{{processorOptions.push(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[0].timeBudget(),
|
|
}};
|
|
listeners['confirm-search-batch-estimates:click']();
|
|
processorOptions[0].persistEstimate(items[0],25);
|
|
processorOptions[0].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
|
|
|
|
|
|
def test_search_batch_reviews_and_applies_one_repository_release_plan_before_assignment():
|
|
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',
|
|
'defer-selected-search-results','plan-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',
|
|
'search-release-review','search-release-summary','search-release-milestone',
|
|
'search-release-due-date','search-release-list','cancel-search-release','confirm-search-release'
|
|
]) elements['#'+id]={{hidden:false,disabled:false,textContent:'',innerHTML:'',value:'',
|
|
addEventListener:(name,fn)=>listeners[id+':'+name]=fn,focus:()=>{{}}}};
|
|
const document={{querySelector:selector=>elements[selector] || null,querySelectorAll:()=>[]}};
|
|
const calls=[];
|
|
const fetchJson=(url,options={{}})=>{{
|
|
calls.push({{url,method:options.method || 'GET',body:options.body ? JSON.parse(options.body) : null}});
|
|
if (url.endsWith('/milestones')) return Promise.resolve([{{id:9,title:'August RC'}},{{id:7,title:'Old',state:'closed'}}]);
|
|
const number=Number(url.split('/issues/')[1].split('/')[0]);
|
|
if (url.endsWith('/release-plan')) return Promise.resolve({{
|
|
number,milestone:{{id:9,title:'August RC'}},due_date:'2026-08-31T23:59:59Z'
|
|
}});
|
|
return Promise.resolve();
|
|
}};
|
|
const processorOptions=[];
|
|
const processors=[];
|
|
const batchFactory=options=>{{processorOptions.push(options);const processor={{
|
|
run:(items,estimates,context)=>{{calls.push({{run:items.map(item=>item.number),context}});return Promise.resolve();}},
|
|
resume:()=>Promise.resolve(),pending:()=>0,
|
|
}};processors.push(processor);return processor;}};
|
|
const planning={{capacity_minutes:null,estimates:{{}}}};
|
|
const todayWork={{limit:8,read:()=>[],planning:()=>planning,identity:item=>item.repository+'#'+item.number,replacePlanning:()=>{{}}}};
|
|
const items=[
|
|
{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:815,title:'Release plan'}},
|
|
{{kind:'issue',state:'open',repository:'stackchain/dashboard',number:816,title:'Second issue'}},
|
|
];
|
|
const mounted=createSearchBatchPlan.mount(
|
|
document,batchFactory,todayWork,()=> 'timmy',fetchJson,()=>'',()=>Promise.resolve('queued'),
|
|
{{enqueueConfiguration:()=>{{}},flush:()=>{{}}}},item=>item,index=>items[index],()=>{{}},
|
|
value=>value,value=>value,null,null
|
|
);
|
|
mounted.plan.start();items.forEach(item=>mounted.plan.toggle(item));
|
|
listeners['plan-selected-search-results:click']().then(async()=>{{
|
|
const before={{calls:calls.slice(),reviewHidden:elements['#search-release-review'].hidden,
|
|
actionsHidden:elements['#search-batch-actions'].hidden,options:elements['#search-release-milestone'].innerHTML,
|
|
list:elements['#search-release-list'].innerHTML}};
|
|
elements['#search-release-milestone'].value='9';
|
|
elements['#search-release-milestone'].selectedIndex=0;
|
|
elements['#search-release-milestone'].options=[{{textContent:'August RC'}}];
|
|
elements['#search-release-due-date'].value='2026-08-31';
|
|
listeners['confirm-search-release:click']();
|
|
const queued=await processorOptions[2].queue({{...items[0],assigned_to_me:true}},
|
|
{{milestone_id:9,milestone_title:'August RC',due_date:'2026-08-31T23:59:59Z'}});
|
|
process.stdout.write(JSON.stringify({{before,calls,queued,journals:processorOptions.map(option=>option.journalName)}}));
|
|
}});
|
|
"""
|
|
|
|
result = run_node(script)
|
|
assert result["before"]["calls"] == [{
|
|
"url": "api/v1/repos/stackchain/dashboard/milestones", "method": "GET", "body": None,
|
|
}]
|
|
assert result["before"]["reviewHidden"] is False
|
|
assert result["before"]["actionsHidden"] is True
|
|
assert "August RC" in result["before"]["options"]
|
|
assert "Old" not in result["before"]["options"]
|
|
assert "Release plan" in result["before"]["list"]
|
|
assert result["calls"][1] == {
|
|
"run": [815, 816],
|
|
"context": {
|
|
"milestone_id": 9, "milestone_title": "August RC",
|
|
"due_date": "2026-08-31T23:59:59Z",
|
|
},
|
|
}
|
|
assert result["calls"][2:] == [{
|
|
"url": "api/v1/repos/stackchain/dashboard/issues/815/release-plan",
|
|
"method": "PATCH",
|
|
"body": {"milestone_id": 9, "due_date": "2026-08-31T23:59:59Z"},
|
|
}]
|
|
assert result["queued"] == "queued"
|
|
assert result["journals"] == [
|
|
"search-today-batch", "search-later-batch", "search-release-batch",
|
|
]
|