feat: plan tomorrow independently (Closes #1152)
CI / lint (pull_request) Successful in 3m21s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 3m53s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-19 22:46:56 +00:00
1 parent 664e5e999f
commit 0cb7328aa1
15 files changed
+632 -25

No files matched your search

@@ -219,6 +219,18 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
page.locator("#work-settings-toggle").click()
tomorrow = page.locator("#plan-tomorrow")
expect(tomorrow).to_be_visible()
tomorrow_bounds = tomorrow.bounding_box()
assert tomorrow_bounds and tomorrow_bounds["height"] >= 44
tomorrow.click()
expect(page.locator("#plan-today-sheet")).to_be_visible()
expect(page.locator("#plan-today-title")).to_have_text("Plan Tomorrow")
expect(page.locator("#save-and-start-today")).to_be_hidden()
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#cancel-plan-today").click()
page.locator('[data-mobile-task="queues"]').click()
delivery_queue = page.locator('[data-mobile-queue="delivery"]')
expect(delivery_queue).to_be_visible()
+4 -1
View File
@@ -50,7 +50,7 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert set(first.feature_bundles) == {
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "sign-out", "device-setup",
"today-timer", "security-center",
"today-timer", "security-center", "planning",
}
assert first.dashboard_html.count("<script src=") == 1
assert f'<script src="{first.runtime_name}"></script>' in first.dashboard_html
@@ -71,6 +71,9 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert b"function createAssignAndStart" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createQueueToday" not in first.runtime_bytes
assert b"function createQueueToday" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createPlanToday" in first.feature_bundles["planning"].runtime_bytes
assert b"function createTomorrowPlan" in first.feature_bundles["planning"].runtime_bytes
assert b"function createPlanToday" not in first.runtime_bytes
assert b"function createConversationPager" in first.feature_bundles["today-timer"].runtime_bytes
assert b"function createConversationPager" not in first.feature_bundles["comment-actions"].runtime_bytes
assert b"function createMobileDeliveryRecovery" not in first.runtime_bytes
+1
View File
@@ -1212,6 +1212,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/plan-today.js",
"/dashboard/static/plan-today-readiness.js",
"/dashboard/static/plan-today-preview.js",
"/dashboard/static/tomorrow-plan.js",
"/dashboard/static/today-sync.js",
"/dashboard/static/today-rollover.js",
"/dashboard/static/update-ownership.js",
+112
View File
@@ -0,0 +1,112 @@
import sqlite3
import pytest
from src import main
from src.today_store import TodayStore, TomorrowPlanConflict, TodayPromotionConflict
def test_tomorrow_plan_is_encrypted_scoped_and_promotes_atomically_exactly_once(tmp_path):
path = tmp_path / "today.sqlite3"
key = b"n" * 32
store = TodayStore(path, encryption_key=key)
store.apply("timmy", "today-1", "add", "issue:r:1:")
tomorrow = store.replace_tomorrow(
"Timmy", base_revision=0,
ids=["issue:secret/repo:3:", "issue:secret/repo:2:"],
capacity_minutes=150,
estimates={"issue:secret/repo:3:": 60, "issue:secret/repo:2:": 45},
plan_date="2026-08-20", timezone="America/Los_Angeles",
)
assert tomorrow == {
"revision": 1,
"ids": ["issue:secret/repo:3:", "issue:secret/repo:2:"],
"capacity_minutes": 150,
"estimates": {"issue:secret/repo:3:": 60, "issue:secret/repo:2:": 45},
"plan_date": "2026-08-20",
"timezone": "America/Los_Angeles",
}
assert store.get("timmy")["ids"] == ["issue:r:1:"]
assert store.get_tomorrow("alexander")["ids"] == []
retained = path.read_bytes()
assert b"issue:secret/repo" not in retained
assert b"America/Los_Angeles" not in retained
promoted = store.promote_tomorrow(
"timmy", promotion_id="rollover-2026-08-20",
tomorrow_revision=1, today_revision=1,
)
replay = store.promote_tomorrow(
"timmy", promotion_id="rollover-2026-08-20",
tomorrow_revision=1, today_revision=1,
)
assert replay == promoted
assert promoted["ids"] == tomorrow["ids"]
assert promoted["capacity_minutes"] == 150
assert promoted["plan_date"] == "2026-08-20"
assert store.get_tomorrow("timmy") == {
"revision": 2, "ids": [], "capacity_minutes": None, "estimates": {}
}
def test_tomorrow_edit_and_promotion_reject_stale_revisions_without_touching_today(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"s" * 32)
original_today = store.apply("timmy", "today", "add", "issue:r:1:")
first = store.replace_tomorrow(
"timmy", base_revision=0, ids=["issue:r:2:"], capacity_minutes=60,
estimates={"issue:r:2:": 30}, plan_date="2026-08-20", timezone="UTC",
)
with pytest.raises(TomorrowPlanConflict) as stale_edit:
store.replace_tomorrow(
"timmy", base_revision=0, ids=["issue:r:9:"], capacity_minutes=None,
estimates={}, plan_date="2026-08-20", timezone="UTC",
)
assert stale_edit.value.snapshot == first
store.apply("timmy", "today-changed", "add", "issue:r:3:")
with pytest.raises(TodayPromotionConflict) as stale_today:
store.promote_tomorrow(
"timmy", promotion_id="rollover", tomorrow_revision=1,
today_revision=original_today["revision"],
)
assert stale_today.value.today["ids"] == ["issue:r:1:", "issue:r:3:"]
assert store.get_tomorrow("timmy") == first
def test_tomorrow_validation_is_bounded_and_date_specific(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3")
with pytest.raises(ValueError, match="ISO calendar date"):
store.replace_tomorrow(
"timmy", base_revision=0, ids=[], capacity_minutes=None, estimates={},
plan_date="tomorrow", timezone="UTC",
)
with pytest.raises(ValueError, match="unique"):
store.replace_tomorrow(
"timmy", base_revision=0, ids=["same", "same"], capacity_minutes=None,
estimates={}, plan_date="2026-08-20", timezone="UTC",
)
@pytest.mark.anyio
async def test_tomorrow_api_round_trip_and_conflict_contract(monkeypatch, tmp_path):
async def user():
return {"login": "timmy"}
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"a" * 32)
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main, "_today_store", lambda: store)
payload = main.TomorrowPlanUpdate(
base_revision=0, ids=["issue:r:2:"], capacity_minutes=90,
estimates={"issue:r:2:": 45}, plan_date="2026-08-20", timezone="UTC",
)
saved = await main.replace_tomorrow_plan(payload)
assert await main.get_tomorrow_plan() == saved
with pytest.raises(main.HTTPException) as raised:
await main.replace_tomorrow_plan(payload)
assert raised.value.status_code == 409
assert raised.value.detail == {"code": "tomorrow_changed", "snapshot": saved}
+100
View File
@@ -0,0 +1,100 @@
import json
import subprocess
from pathlib import Path
FRONTEND = Path(__file__).parents[1] / "frontend"
CONTROLLER = FRONTEND / "tomorrow-plan.js"
INDEX = FRONTEND / "index.html"
CSS = FRONTEND / "dashboard.css"
def run_controller(scenario: str) -> dict:
harness = f"""
const createTomorrowPlan = 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_tomorrow_planner_loads_and_saves_independently_from_today():
result = run_controller("""
const requests=[];
const fetchJson=async (url, options={})=>{
requests.push({url,method:options.method||'GET',body:options.body ? JSON.parse(options.body) : null});
if ((options.method||'GET') === 'GET') return {
revision:2, ids:['issue:r:2:'], capacity_minutes:90,
estimates:{'issue:r:2:':45}, plan_date:'2026-08-20', timezone:'UTC'
};
return {revision:3,...JSON.parse(options.body)};
};
const planner=createTomorrowPlan({fetchJson, localDate:()=> '2026-08-19', timeZone:()=> 'UTC'});
const loaded=await planner.load();
const saved=await planner.save({ids:['issue:r:3:'],capacity_minutes:120,estimates:{'issue:r:3:':60}});
console.log(JSON.stringify({loaded,saved,requests}));
""")
assert result["loaded"]["ids"] == ["issue:r:2:"]
assert result["saved"]["plan_date"] == "2026-08-20"
assert result["requests"] == [
{"url": "api/v1/tomorrow", "method": "GET", "body": None},
{
"url": "api/v1/tomorrow",
"method": "PUT",
"body": {
"base_revision": 2,
"ids": ["issue:r:3:"],
"capacity_minutes": 120,
"estimates": {"issue:r:3:": 60},
"plan_date": "2026-08-20",
"timezone": "UTC",
},
},
]
assert all(request["url"] != "api/v1/today" for request in result["requests"])
def test_tomorrow_planner_promotes_only_on_matching_local_date():
result = run_controller("""
const requests=[];
const fetchJson=async (url, options={})=>{
requests.push({url,body:options.body ? JSON.parse(options.body) : null});
return {ids:['issue:r:2:'],revision:8};
};
const before=createTomorrowPlan({fetchJson,localDate:()=> '2026-08-19',timeZone:()=> 'UTC',createId:()=> 'rollover-id'});
before.adopt({revision:3,ids:['issue:r:2:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'});
const early=await before.promote(7);
const due=createTomorrowPlan({fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC',createId:()=> 'rollover-id'});
due.adopt({revision:3,ids:['issue:r:2:'],capacity_minutes:60,estimates:{},plan_date:'2026-08-20',timezone:'UTC'});
const promoted=await due.promote(7);
console.log(JSON.stringify({early,promoted,requests}));
""")
assert result["early"] is False
assert result["promoted"]["revision"] == 8
assert result["requests"] == [{
"url": "api/v1/tomorrow/promote",
"body": {
"promotion_id": "rollover-id",
"tomorrow_revision": 3,
"today_revision": 7,
},
}]
def test_tomorrow_plan_has_a_touch_safe_mobile_entry_and_reuses_the_ordered_planner():
index = INDEX.read_text()
css = CSS.read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
assert '<button class="plan-tomorrow" id="plan-tomorrow" type="button">Plan Tomorrow</button>' in index
assert ".plan-tomorrow { min-height:44px;" in css
assert '<script src="static/tomorrow-plan.js"></script>' in index
assert "const tomorrowPlan = createTomorrowPlan" in dashboard
assert "qs('#plan-tomorrow').addEventListener('click'" in dashboard
assert "planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan)" in dashboard
assert "tomorrowPlan.load()" in dashboard
assert "tomorrowPlan.promote(plan.revision)" in dashboard
+15 -8
View File
@@ -25,16 +25,23 @@ const status={textContent:''};
const document={
querySelector(selector) {
if (selector === 'meta[name="stackchain-feature-today-timer"]') return {content:'feature-workspace-abc.js'};
if (selector === 'meta[name="stackchain-feature-planning"]') return {content:'feature-planning-def.js'};
if (selector === '#my-work-action-status') return status;
return null;
}
};
let requested='';
const createLoader=options=>({load:async name=>{requested=name + ':' + options.urls[name];}});
const requested=[];
const createLoader=options=>({load:async name=>{requested.push(name + ':' + options.urls[name]);}});
await loadWorkspace({document,createLoader});
console.log(JSON.stringify({requested,status:status.textContent}));
""")
assert result == {"requested": "today-timer:feature-workspace-abc.js", "status": ""}
assert result == {
"requested": [
"today-timer:feature-workspace-abc.js",
"planning:feature-planning-def.js",
],
"status": "",
}
def test_workspace_bootstrap_recovers_one_transient_failure_in_place():
@@ -51,7 +58,7 @@ const schedule=callback=>{callback();};
await loadWorkspace({document,createLoader,schedule});
console.log(JSON.stringify({attempts,status:status.textContent,retryHidden:retry.hidden}));
""")
assert result == {"attempts": 2, "status": "", "retryHidden": True}
assert result == {"attempts": 4, "status": "", "retryHidden": True}
def test_workspace_bootstrap_offers_single_flight_manual_retry_without_reloading():
@@ -65,7 +72,7 @@ const document={querySelector(selector) {
return null;
}};
const window={location:{reload(){reloads++;}},addEventListener(){},removeEventListener(){}};
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 3) throw new Error('offline');}});
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 5) throw new Error('offline');}});
const loading=loadWorkspace({document,window,createLoader,schedule:callback=>callback()});
await new Promise(resolve=>setImmediate(resolve));
const offered={hidden:retry.hidden,disabled:retry.disabled,status:status.textContent};
@@ -74,7 +81,7 @@ await Promise.all([first,second,loading]);
console.log(JSON.stringify({attempts,reloads,offered,status:status.textContent,retryHidden:retry.hidden}));
""")
assert result == {
"attempts": 3,
"attempts": 6,
"reloads": 0,
"offered": {
"hidden": False,
@@ -109,7 +116,7 @@ const window={
addEventListener(name,callback) { listeners[name]=callback; },
removeEventListener(name,callback) { if (listeners[name] === callback) delete listeners[name]; },
};
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 3) throw new Error('offline');}});
const createLoader=()=>({load:async()=>{attempts++; if (attempts < 5) throw new Error('offline');}});
const loading=loadWorkspace({document,window,createLoader,schedule:callback=>callback()});
await new Promise(resolve=>setImmediate(resolve));
const waiting=Boolean(listeners.online);
@@ -118,7 +125,7 @@ await loading;
console.log(JSON.stringify({attempts,waiting,reloads,status:status.textContent}));
""")
assert result == {
"attempts": 3,
"attempts": 6,
"waiting": True,
"reloads": 0,
"status": "",