import json
import subprocess
from pathlib import Path
ASSIGN_AND_START = Path(__file__).parents[1] / "frontend" / "assign-and-start.js"
FILED_CLAIM = Path(__file__).parents[1] / "frontend" / "filed-claim.js"
QUEUE_TODAY = Path(__file__).parents[1] / "frontend" / "queue-today.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"
WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js"
def run_node(script):
return json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout)
def test_filed_claim_actions_require_open_authored_unassigned_canonical_issue():
script = f"""
const eligible=require({json.dumps(str(FILED_CLAIM))});
const item={{is_filed:true,is_assigned:false,is_completed:false,state:'open'}};
process.stdout.write(JSON.stringify({{
eligible:eligible(item, {{state:'open',assignees:[]}}),
assigned:eligible(item, {{state:'open',assignees:['alex']}}),
closed:eligible(item, {{state:'closed',assignees:[]}}),
ordinary:eligible({{...item,is_filed:false}}, {{state:'open',assignees:[]}}),
}}));
"""
assert run_node(script) == {
"eligible": True,
"assigned": False,
"closed": False,
"ordinary": False,
}
def test_filed_sheet_exposes_phone_safe_claim_actions_after_canonical_detail():
html = HTML.read_text()
dashboard = DASHBOARD.read_text()
css = CSS.read_text()
worker = WORKER.read_text()
assert 'id="filed-claim-actions"' in html
assert 'id="queue-filed-issue"' in html
assert 'id="start-filed-issue"' in html
assert "filedClaimEligible(item, detail)" in dashboard
assert "filedAssignAndStart.run(item, { destination:'queue' })" in dashboard
assert "filedAssignAndStart.run(item)" in dashboard
assert "Claim failed; this issue is still in Filed." in dashboard
assert ".filed-claim-actions" in css
action_rule = css.split(".filed-claim-actions", 1)[1].split("}", 1)[0]
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in action_rule
assert "width:min(560px,100%)" in action_rule
assert "BASE + 'static/filed-claim.js'" in worker
assert '' in html
def test_repeated_assign_and_start_taps_claim_once_then_start_confirmed_issue():
script = f"""
const fs=require('fs');
if (!fs.existsSync({json.dumps(str(ASSIGN_AND_START))})) {{
process.stdout.write(JSON.stringify({{available:false}}));
}} else {{
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
let release;
const claimResult=new Promise(resolve=>{{release=resolve;}});
const flow=createAssignAndStart({{
available:()=>true,
claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
start:item=>{{calls.push('start:'+item.number);return 'started';}},
recover:item=>calls.push('recover:'+item.number),
announce:message=>calls.push('announce:'+message),
}});
const issue={{repository:'stackchain/dashboard',number:411}};
const first=flow.run(issue);
const second=flow.run(issue);
release({{...issue,assignees:['timmy']}});
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{available:true,calls,results,same:first===second}})));
}}
"""
assert run_node(script) == {
"available": True,
"calls": [
"claim:411",
"start:411",
"announce:Assigned, added to Today, and ready to work.",
],
"results": ["started", "started"],
"same": True,
}
def test_full_today_blocks_before_assignment_and_explains_recovery():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const flow=createAssignAndStart({{
available:()=>false,
claim:()=>{{calls.push('claim');return Promise.resolve({{}});}},
start:()=>calls.push('start'),
recover:()=>calls.push('recover'),
announce:message=>calls.push('announce:'+message),
}});
flow.run({{repository:'stackchain/dashboard',number:411}}).then(result=>
process.stdout.write(JSON.stringify({{result,calls}}))
);
"""
assert run_node(script) == {
"result": "full",
"calls": ["announce:Today is full—remove an item before assigning this issue."],
}
def test_confirmed_assignment_recovers_in_issue_detail_when_today_start_fails():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:411,assignees:['timmy']}};
const flow=createAssignAndStart({{
available:()=>true,
claim:()=>Promise.resolve(issue),
start:()=> 'sync-unavailable',
recover:item=>calls.push('recover:'+item.number),
announce:message=>calls.push('announce:'+message),
}});
flow.run(issue).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
"""
assert run_node(script) == {
"result": "recovery",
"calls": [
"announce:Assigned to you, but Today could not start. The issue is open so you can recover.",
"recover:411",
],
}
def test_failed_assignment_does_not_start_today_and_can_be_retried():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];let attempt=0;
const issue={{repository:'stackchain/dashboard',number:411}};
const flow=createAssignAndStart({{
available:()=>true,
claim:()=>{{attempt+=1;calls.push('claim:'+attempt);return attempt===1 ? Promise.reject(new Error('conflict')) : Promise.resolve(issue);}},
start:()=>{{calls.push('start');return 'started';}},
recover:()=>calls.push('recover'),
announce:message=>calls.push('announce:'+message),
}});
flow.run(issue).catch(error=>{{
calls.push('error:'+error.message);
return flow.run(issue);
}}).then(result=>process.stdout.write(JSON.stringify({{result,calls}})));
"""
assert run_node(script) == {
"result": "started",
"calls": [
"claim:1",
"error:conflict",
"claim:2",
"start",
"announce:Assigned, added to Today, and ready to work.",
],
}
def test_already_owned_search_result_starts_without_claiming():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:413,assigned_to_me:true}};
const flow=createAssignAndStart({{
available:()=>true,
claim:()=>{{calls.push('claim');return Promise.resolve(issue);}},
start:item=>{{calls.push('start:'+item.number);return 'started';}},
recover:item=>calls.push('recover:'+item.number),
announce:message=>calls.push('announce:'+message),
}});
flow.run(issue, {{alreadyOwned:true}}).then(result=>
process.stdout.write(JSON.stringify({{result,calls}}))
);
"""
assert run_node(script) == {
"result": "started",
"calls": [
"start:413",
"announce:Added to Today and ready to work.",
],
}
def test_queue_today_claims_once_without_starting_the_active_session():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
let release;
const claimResult=new Promise(resolve=>{{release=resolve;}});
const issue={{repository:'stackchain/dashboard',number:415}};
const flow=createAssignAndStart({{
available:()=>true,
claim:item=>{{calls.push('claim:'+item.number);return claimResult;}},
start:item=>{{calls.push('start:'+item.number);return 'started';}},
queue:item=>{{calls.push('queue:'+item.number);return 'queued';}},
recover:item=>calls.push('recover:'+item.number),
announce:message=>calls.push('announce:'+message),
}});
const first=flow.run(issue, {{destination:'queue'}});
const second=flow.run(issue, {{destination:'queue'}});
release({{...issue,assignees:['timmy']}});
Promise.all([first,second]).then(results=>process.stdout.write(JSON.stringify({{
calls,results,same:first===second
}})));
"""
assert run_node(script) == {
"calls": [
"claim:415",
"queue:415",
"announce:Assigned and queued in Today. Keep finding work when ready.",
],
"results": ["queued", "queued"],
"same": True,
}
def test_queue_today_persists_syncs_and_warms_without_a_session_side_effect():
script = f"""
const fs=require('fs');
if (!fs.existsSync({json.dumps(str(QUEUE_TODAY))})) {{
process.stdout.write(JSON.stringify({{available:false}}));
}} else {{
const createQueueToday=require({json.dumps(str(QUEUE_TODAY))});
const calls=[];
const issue={{kind:'issue',repository:'stackchain/dashboard',number:415}};
const queue=createQueueToday({{
todayWork:{{identity:()=> 'issue:stackchain/dashboard:415:',add:()=>{{calls.push('add');return 'added';}}}},
todaySync:{{enqueue:(action,id)=>{{calls.push(action+':'+id);return true;}},flush:()=>calls.push('flush')}},
refresh:()=>calls.push('refresh'),
warm:()=>calls.push('warm'),
}});
process.stdout.write(JSON.stringify({{available:true,result:queue(issue),calls}}));
}}
"""
assert run_node(script) == {
"available": True,
"result": "queued",
"calls": [
"add",
"add:issue:stackchain/dashboard:415:",
"refresh",
"flush",
"warm",
],
}
def test_queue_today_reports_assignment_when_local_planning_needs_recovery():
script = f"""
const createAssignAndStart=require({json.dumps(str(ASSIGN_AND_START))});
const calls=[];
const issue={{repository:'stackchain/dashboard',number:415,assignees:['timmy']}};
const flow=createAssignAndStart({{
available:()=>true,
claim:()=>Promise.resolve(issue),
start:()=> 'started',
queue:()=> 'sync-unavailable',
recover:item=>calls.push('recover:'+item.number),
announce:message=>calls.push('announce:'+message),
}});
flow.run(issue, {{destination:'queue'}}).then(result=>
process.stdout.write(JSON.stringify({{result,calls}}))
);
"""
assert run_node(script) == {
"result": "recovery",
"calls": [
"announce:Assigned and saved in Today on this device, but account sync is unavailable. The issue is open so you can recover.",
"recover:415",
],
}
def test_find_work_renders_phone_safe_assign_and_start_and_wires_offline_shell():
html = HTML.read_text()
dashboard = DASHBOARD.read_text()
css = CSS.read_text()
worker = WORKER.read_text()
assert "data-claim-start-index" in dashboard
assert "data-claim-queue-index" in dashboard
assert ">Start now" in dashboard
assert ">Queue Today" in dashboard
assert "const assignAndStart = createAssignAndStart({" in dashboard
assert "assignAndStart.run(item)" in dashboard
assert "assignAndStart.run(item, { destination:'queue' })" in dashboard
assert "queue: confirmed =>" in dashboard
assert "const queueToday = createQueueToday({" in dashboard
assert "return queueToday(claimed)" in dashboard
assert "createAndStart.available" in dashboard
assert "createAndStart.complete" in dashboard
assert ".find-work-claim-actions" in css
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in css
assert ".find-work-claim-actions [data-claim-start-index]" in css
assert "grid-column:1 / -1" in css
assert "BASE + 'static/assign-and-start.js'" in worker
assert "BASE + 'static/queue-today.js'" in worker
assert '' in html