stackchain-dashboard/tests/test_today_week_reschedule.py
timmy 9d8a362583
All checks were successful
CI / lint (pull_request) Successful in 3m33s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 4m57s
CI / release-candidate (pull_request) Has been skipped
feat: reschedule active Today work into Week Ahead (Closes #1228)
2026-08-21 16:59:40 +00:00

110 lines
5.2 KiB
Python

import json
import subprocess
from pathlib import Path
CONTROLLER = Path(__file__).parents[1] / "frontend" / "today-week-reschedule.js"
INDEX = CONTROLLER.parent / "index.html"
CSS = CONTROLLER.parent / "dashboard.css"
DASHBOARD = CONTROLLER.parent / "dashboard.js"
BUNDLE = CONTROLLER.parents[1] / "src" / "frontend_bundle.py"
def run_controller(scenario: str) -> dict:
harness = f"""
const createReschedule = require({json.dumps(str(CONTROLLER))});
(async()=>{{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}});
"""
completed = subprocess.run(
["node", "-e", harness], check=True, capture_output=True, text=True
)
return json.loads(completed.stdout)
def test_reschedule_controller_reviews_load_and_atomically_confirms_selected_day():
result = run_controller("""
const requests=[];
const weekState={revision:7,offline_snapshot:false,days:[]};
const review={days:[
{plan_date:'2026-08-24',label:'Mon, Aug 24',ids:['one'],planned_minutes:30,capacity_minutes:60,overloaded:false},
{plan_date:'2026-08-25',label:'Tue, Aug 25',ids:[],planned_minutes:0,capacity_minutes:90,overloaded:false}
]};
const week={load:async()=>weekState,review:()=>review,adopt:value=>{weekState.revision=value.revision;weekState.days=value.days;}};
const api=async(url,options)=>{
if(url==='api/v1/today')return {revision:4,ids:['active','other'],estimates:{active:45}};
requests.push({url,body:JSON.parse(options.body)});return {
today:{revision:5,ids:['other'],capacity_minutes:120,estimates:{other:20}},
week:{revision:8,timezone:'UTC',days:[{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}]}
};};
let adoptedToday=null;
const controller=createReschedule({week,api,getToday:()=>({revision:4,ids:['active','other'],estimates:{active:45}}),
adoptToday:value=>{adoptedToday=value;},operationId:()=> 'move-active'});
const opened=await controller.open('active');
const confirmed=await controller.confirm('2026-08-25',45);
console.log(JSON.stringify({opened,confirmed,requests,adoptedToday,weekState}));
""")
assert result["opened"]["estimate_minutes"] == 45
assert result["opened"]["days"][0]["load"] == "30 / 60 min"
assert result["opened"]["days"][1]["eligible"] is True
assert result["requests"] == [{
"url": "api/v1/week/reschedule",
"body": {
"operation_id": "move-active",
"identity": "active",
"estimate_minutes": 45,
"plan_date": "2026-08-25",
"today_revision": 4,
"week_revision": 7,
"allow_over_capacity": False,
},
}]
assert result["adoptedToday"]["ids"] == ["other"]
assert result["weekState"]["revision"] == 8
assert result["confirmed"]["today"]["revision"] == 5
def test_reschedule_controller_blocks_offline_full_and_overloaded_days_without_mutation():
result = run_controller("""
let calls=0;
const days=[
{plan_date:'2026-08-24',label:'Mon',ids:['1','2','3','4','5'],planned_minutes:50,capacity_minutes:60},
{plan_date:'2026-08-25',label:'Tue',ids:['1'],planned_minutes:50,capacity_minutes:60}
];
const week={load:async()=>({revision:2,offline_snapshot:false}),review:()=>({days}),adopt:()=>{}};
const api=async url=>{if(url==='api/v1/today')return {revision:3,ids:['active'],estimates:{active:30}};calls++;};
const controller=createReschedule({week,api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
const opened=await controller.open('active');
let fullError='',overError='';
try{await controller.confirm('2026-08-24',30);}catch(error){fullError=error.message;}
try{await controller.confirm('2026-08-25',30);}catch(error){overError=error.message;}
const offline=createReschedule({week:{load:async()=>({revision:2,offline_snapshot:true}),review:()=>({days})},api,getToday:()=>({revision:3,ids:['active'],estimates:{active:30}})});
let offlineError='';try{await offline.open('active');}catch(error){offlineError=error.message;}
console.log(JSON.stringify({opened,fullError,overError,offlineError,calls}));
""")
assert result["opened"]["days"][0]["eligible"] is False
assert result["fullError"] == "That Week Ahead day already has five items."
assert result["overError"] == "That move exceeds the day's capacity. Confirm overload before rescheduling."
assert result["offlineError"] == "Reconnect before rescheduling Today into Week Ahead."
assert result["calls"] == 0
def test_mobile_active_today_reschedule_dialog_is_touch_safe_and_wired_into_release_bundle():
index = INDEX.read_text()
css = CSS.read_text()
dashboard = DASHBOARD.read_text()
bundle = BUNDLE.read_text()
assert 'data-work-session-reschedule-week' in index
assert 'id="today-week-reschedule"' in index
assert 'id="today-week-reschedule-days"' in index
assert 'id="today-week-reschedule-estimate"' in index
assert 'id="confirm-today-week-reschedule"' in index
assert 'id="cancel-today-week-reschedule"' in index
assert '.today-week-reschedule-days button { min-height:44px;' in css
assert '.today-week-reschedule-panel { width:min(100%,560px);' in css
assert 'x:currentTodayProgressTarget' in dashboard
assert "runTodayTransition('next')" in dashboard
assert '"static/today-week-reschedule.js"' in bundle