feat: replan overdue Agenda deadlines (Closes #707)
This commit is contained in:
1 parent
7d7abefb0b
commit
46888d2876
7 files changed
+272
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPLAN = ROOT / "frontend" / "agenda-replan.js"
|
||||
|
||||
|
||||
def run_node(script):
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_overdue_replan_sweep_orders_items_and_only_advances_after_confirmed_change():
|
||||
script = f"""
|
||||
const createSweep = require({json.dumps(str(REPLAN))});
|
||||
const calls = [];
|
||||
let fail = true;
|
||||
const items = [
|
||||
{{key:'o/r#3', repository:'o/r', number:3, due_date:'2026-08-11T18:00:00Z'}},
|
||||
{{key:'o/r#1', repository:'o/r', number:1, due_date:'2026-08-09T18:00:00Z'}},
|
||||
{{key:'o/r#2', repository:'o/r', number:2, due_date:'2026-08-10T18:00:00Z'}},
|
||||
];
|
||||
const sweep = createSweep({{
|
||||
now: () => new Date('2026-08-12T12:00:00'),
|
||||
update: async (item, due) => {{ calls.push([item.key, due]); if (fail) throw new Error('offline'); return {{due_date:due}}; }},
|
||||
}});
|
||||
async function run() {{
|
||||
const started = sweep.start(items);
|
||||
const failed = await sweep.tomorrow();
|
||||
const afterFailure = sweep.snapshot();
|
||||
fail = false;
|
||||
const changed = await sweep.tomorrow();
|
||||
const afterChange = sweep.snapshot();
|
||||
const kept = await sweep.keep();
|
||||
process.stdout.write(JSON.stringify({{started, failed, afterFailure, changed, afterChange, kept, calls}}));
|
||||
}}
|
||||
run();
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"started": {"active": True, "index": 0, "total": 3, "current": "o/r#1", "pending": False},
|
||||
"failed": {"ok": False, "error": "offline"},
|
||||
"afterFailure": {"active": True, "index": 0, "total": 3, "current": "o/r#1", "pending": False},
|
||||
"changed": {"ok": True, "done": False},
|
||||
"afterChange": {"active": True, "index": 1, "total": 3, "current": "o/r#2", "pending": False},
|
||||
"kept": {"ok": True, "done": False},
|
||||
"calls": [["o/r#1", "2026-08-13T23:59:59Z"], ["o/r#1", "2026-08-13T23:59:59Z"]],
|
||||
}
|
||||
|
||||
|
||||
def test_overdue_replan_rejects_non_future_choice_and_prevents_parallel_updates():
|
||||
script = f"""
|
||||
const createSweep = require({json.dumps(str(REPLAN))});
|
||||
let resolveUpdate;
|
||||
let calls = 0;
|
||||
const sweep = createSweep({{
|
||||
now: () => new Date('2026-08-12T12:00:00'),
|
||||
update: async () => {{ calls += 1; await new Promise(resolve => resolveUpdate = resolve); return {{}}; }},
|
||||
}});
|
||||
async function run() {{
|
||||
sweep.start([{{key:'o/r#1', due_date:'2026-08-10T18:00:00Z'}}]);
|
||||
const invalid = await sweep.choose('2026-08-12');
|
||||
const first = sweep.choose('2026-08-14');
|
||||
const duplicate = await sweep.choose('2026-08-15');
|
||||
resolveUpdate();
|
||||
const confirmed = await first;
|
||||
process.stdout.write(JSON.stringify({{invalid, duplicate, confirmed, calls, final:sweep.snapshot()}}));
|
||||
}}
|
||||
run();
|
||||
"""
|
||||
assert run_node(script) == {
|
||||
"invalid": {"ok": False, "error": "Choose a future date."},
|
||||
"duplicate": {"ok": False, "error": "Deadline update already in progress."},
|
||||
"confirmed": {"ok": True, "done": True},
|
||||
"calls": 1,
|
||||
"final": {"active": False, "index": 1, "total": 1, "current": None, "pending": False},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_agenda_exposes_thumb_safe_replan_controls_and_wires_existing_mutation():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="start-agenda-replan"' in html
|
||||
assert 'id="agenda-replan-controls"' in html
|
||||
assert 'id="agenda-replan-progress"' in html
|
||||
assert 'id="agenda-replan-keep"' in html
|
||||
assert 'id="agenda-replan-tomorrow"' in html
|
||||
assert 'id="agenda-replan-date"' in html
|
||||
assert 'id="agenda-replan-choose"' in html
|
||||
assert 'id="agenda-replan-cancel"' in html
|
||||
assert "agendaReplan.start(overdue)" in html
|
||||
assert "issueController.updateDueDate(item, dueDate)" in html
|
||||
assert ".agenda-replan-actions button { min-height:44px;" in html
|
||||
assert "@media(max-width:360px)" in html
|
||||
@@ -695,6 +695,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/offline-work.js",
|
||||
"/dashboard/static/offline-today.js",
|
||||
"/dashboard/static/my-work.js",
|
||||
"/dashboard/static/agenda-replan.js",
|
||||
"/dashboard/static/notification-undo.js",
|
||||
"/dashboard/static/card-planning.js",
|
||||
"/dashboard/static/work-selection.js",
|
||||
|
||||
Reference in new issue
Block a user