1 parent
be7bfcaa5a
commit
1fb26e0a86
8 files changed
+217
-28
No files matched your search
@@ -127,6 +127,43 @@ process.stdout.write(JSON.stringify({{over, confirmation, stillOpen, result, sav
|
||||
}]
|
||||
|
||||
|
||||
def test_plan_today_applies_recap_actual_only_to_the_draft_after_explicit_action():
|
||||
script = f"""
|
||||
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
|
||||
const item = number => ({{kind:'issue', repository:'stackchain/dashboard', number, title:'Issue ' + number}});
|
||||
const saved = [];
|
||||
const planner = createPlanToday({{
|
||||
identity: value => 'issue:stackchain/dashboard:' + value.number + ':',
|
||||
save: plan => {{ saved.push(plan); return true; }},
|
||||
}});
|
||||
const first='issue:stackchain/dashboard:1:';
|
||||
const second='issue:stackchain/dashboard:2:';
|
||||
planner.open([item(1), item(2)], [item(1), item(2)], {{
|
||||
capacity_minutes:120, estimates:{{[first]:30,[second]:45}}
|
||||
}}, {{[first]:52,[second]:45}});
|
||||
const before=planner.snapshot();
|
||||
const ignored=planner.applyRecommendation(second);
|
||||
const applied=planner.applyRecommendation(first);
|
||||
const after=planner.snapshot();
|
||||
const result=planner.commit();
|
||||
process.stdout.write(JSON.stringify({{before,ignored,applied,after,result,saved}}));
|
||||
"""
|
||||
result = run_node(script)
|
||||
assert result["before"]["estimates"] == {
|
||||
"issue:stackchain/dashboard:1:": 30,
|
||||
"issue:stackchain/dashboard:2:": 45,
|
||||
}
|
||||
assert result["before"]["recommendations"] == {"issue:stackchain/dashboard:1:": 52}
|
||||
assert result["ignored"] is False
|
||||
assert result["applied"] is True
|
||||
assert result["after"]["estimates"]["issue:stackchain/dashboard:1:"] == 52
|
||||
assert result["after"]["recommendations"] == {}
|
||||
assert result["saved"][0]["estimates"] == {
|
||||
"issue:stackchain/dashboard:1:": 52,
|
||||
"issue:stackchain/dashboard:2:": 45,
|
||||
}
|
||||
|
||||
|
||||
def test_plan_today_preview_preserves_draft_scroll_and_adds_item_once_on_return():
|
||||
script = f"""
|
||||
const createPlanToday = require({json.dumps(str(PLAN_TODAY))});
|
||||
@@ -247,6 +284,9 @@ async def test_mobile_dashboard_wires_focused_plan_today_sheet():
|
||||
assert "formatPlanMinutes(state.planned_minutes)" in html
|
||||
assert "planToday.setCapacity" in html
|
||||
assert "planToday.setEstimate" in html
|
||||
assert 'data-plan-recommendation="' in html
|
||||
assert "planToday.applyRecommendation" in html
|
||||
assert "Use " in html and " as new estimate" in html
|
||||
assert "todaySync.enqueueConfiguration" in html
|
||||
assert "todayWork.runway(todayMyWork, state.index - 1)" in html
|
||||
assert "resetPlanTodayConfirmation()" in html
|
||||
|
||||
@@ -119,6 +119,41 @@ recap.save().then(saved=>process.stdout.write(JSON.stringify({{
|
||||
assert output["cleared"] == 1
|
||||
|
||||
|
||||
def test_recap_feedback_uses_work_metadata_and_reports_per_item_variance():
|
||||
script = f"""
|
||||
const createRecap = require({json.dumps(str(TODAY_RECAP))});
|
||||
const rows=createRecap.feedbackRows({{
|
||||
items:[
|
||||
{{identity:'issue:stackchain/dashboard:583:',estimate_minutes:30,actual_minutes:52}},
|
||||
{{identity:'pull:stackchain/api:9:',estimate_minutes:null,actual_minutes:12}},
|
||||
]
|
||||
}}, identity => identity.startsWith('issue:') ? {{
|
||||
title:'Turn recap feedback into a plan', key:'stackchain/dashboard#583'
|
||||
}} : null);
|
||||
process.stdout.write(JSON.stringify(rows));
|
||||
"""
|
||||
run = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert run.returncode == 0, run.stderr
|
||||
assert json.loads(run.stdout) == [
|
||||
{
|
||||
"identity": "issue:stackchain/dashboard:583:",
|
||||
"label": "Turn recap feedback into a plan",
|
||||
"context": "stackchain/dashboard#583",
|
||||
"estimate_minutes": 30,
|
||||
"actual_minutes": 52,
|
||||
"variance_minutes": 22,
|
||||
},
|
||||
{
|
||||
"identity": "pull:stackchain/api:9:",
|
||||
"label": "pull:stackchain/api:9:",
|
||||
"context": "Work details unavailable",
|
||||
"estimate_minutes": None,
|
||||
"actual_minutes": 12,
|
||||
"variance_minutes": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_recap_controller_restores_corrected_draft_with_stable_retry_identity():
|
||||
script = f"""
|
||||
const createRecap = require({json.dumps(str(TODAY_RECAP))});
|
||||
@@ -238,6 +273,39 @@ recap.begin([{{identity:'issue:r:1:',elapsed_ms:60000}}]);
|
||||
assert output["afterSuccess"] == {"draft": None, "stored": False, "cleared": 1}
|
||||
|
||||
|
||||
def test_recap_controller_returns_actuals_for_replanning_only_after_save_confirmation():
|
||||
script = f"""
|
||||
const createRecap = require({json.dumps(str(TODAY_RECAP))});
|
||||
let fail=true;
|
||||
const recap=createRecap({{
|
||||
save:payload => fail ? Promise.reject(new Error('offline')) : Promise.resolve({{saved:true}}),
|
||||
clear:()=>{{}}, makeId:()=> 'replan-session'
|
||||
}});
|
||||
recap.begin([
|
||||
{{identity:'issue:r:1:',elapsed_ms:52*60000}},
|
||||
{{identity:'issue:r:2:',elapsed_ms:7*60000}},
|
||||
], {{'issue:r:1:':30,'issue:r:2:':7}});
|
||||
(async()=>{{
|
||||
let failed;
|
||||
try {{ await recap.saveForReplan(); }} catch (error) {{ failed=error.message; }}
|
||||
const retained=recap.snapshot();
|
||||
fail=false;
|
||||
const confirmed=await recap.saveForReplan();
|
||||
process.stdout.write(JSON.stringify({{failed,retained,confirmed,after:recap.snapshot()}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
run = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert run.returncode == 0, run.stderr
|
||||
output = json.loads(run.stdout)
|
||||
assert output["failed"] == "offline"
|
||||
assert output["retained"]["session_id"] == "replan-session"
|
||||
assert output["confirmed"] == {
|
||||
"result": {"saved": True},
|
||||
"actual_minutes": {"issue:r:1:": 52, "issue:r:2:": 7},
|
||||
}
|
||||
assert output["after"] is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_renders_mobile_today_recap_flow():
|
||||
html = main.FRONTEND_BUILD.dashboard_html
|
||||
@@ -259,3 +327,11 @@ async def test_dashboard_renders_mobile_today_recap_flow():
|
||||
assert ".today-recap-header button { min-height:44px;" in css
|
||||
assert ".today-recap-actions button { min-height:44px;" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
assert "Save recap & adjust plan" in html
|
||||
assert "todayRecapFeedbackRows(draft, describeWork)" in recap_source
|
||||
assert 'class="today-recap-variance"' in recap_source
|
||||
assert "await recap.saveForReplan()" in recap_source
|
||||
assert "adjustPlan(handoff.actual_minutes)" in recap_source
|
||||
assert "identity => [...todayMyWork, ...activeMyWork].find" in dashboard
|
||||
assert "openPlanToday(qs('#plan-today'), true, actualMinutes)" in dashboard
|
||||
assert ".today-recap-row { grid-template-columns:1fr; }" in css
|
||||
Reference in new issue
Block a user