feat: bound planning sync history (#381)
CI / lint (pull_request) Successful in 39s
CI / build-release (pull_request) Successful in 4s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-09 07:43:53 +00:00
1 parent 51278bc86f
commit 4e13f5482b
16 files changed
+390 -57

No files matched your search

+57
View File
@@ -137,6 +137,63 @@ def test_batch_rejects_stale_same_item_intent_without_blocking_other_items(tmp_p
assert duplicate["records"] == replay["records"]
def test_later_receipts_are_bounded_per_account_and_existing_schema_migrates(tmp_path):
path = tmp_path / "later.sqlite3"
with sqlite3.connect(path) as connection:
connection.execute(
"CREATE TABLE later_operations (login TEXT NOT NULL, operation_id TEXT NOT NULL, "
"PRIMARY KEY (login, operation_id))"
)
connection.execute(
"INSERT INTO later_operations(login, operation_id) VALUES ('timmy', 'legacy')"
)
store = LaterStore(path, operation_limit=2, clock=lambda: 1_000)
for index in range(4):
store.apply(
"timmy", f"op-{index}", "defer", f"issue:r:{index}:",
wake_at="2026-08-10T09:00:00.000Z",
)
store.apply(
"alexander", "other", "defer", "issue:r:99:",
wake_at="2026-08-10T09:00:00.000Z",
)
with sqlite3.connect(path) as connection:
columns = {row[1] for row in connection.execute("PRAGMA table_info(later_operations)")}
timmy = connection.execute(
"SELECT operation_id FROM later_operations WHERE login = 'timmy' ORDER BY rowid"
).fetchall()
alexander = connection.execute(
"SELECT operation_id FROM later_operations WHERE login = 'alexander'"
).fetchall()
assert "created_at" in columns
assert timmy == [("op-2",), ("op-3",)]
assert alexander == [("other",)]
def test_later_receipt_age_pruning_is_account_scoped(tmp_path):
clock = [1_000.0]
path = tmp_path / "later.sqlite3"
store = LaterStore(path, operation_retention_seconds=60, clock=lambda: clock[0])
store.apply("timmy", "old", "restore", "issue:r:1:")
store.apply("alexander", "other-old", "restore", "issue:r:2:")
clock[0] += 61
store.apply("timmy", "fresh", "restore", "issue:r:3:")
with sqlite3.connect(path) as connection:
timmy = connection.execute(
"SELECT operation_id FROM later_operations WHERE login = 'timmy'"
).fetchall()
alexander = connection.execute(
"SELECT operation_id FROM later_operations WHERE login = 'alexander'"
).fetchall()
assert timmy == [("fresh",)]
assert alexander == [("other-old",)]
@pytest.mark.anyio
async def test_authenticated_later_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
+26 -1
View File
@@ -97,6 +97,29 @@ sync.enqueue('defer','issue:r:2:','2026-08-10T09:00:00.000Z');
assert result["pending"] == []
def test_later_queue_expires_ancient_edits_before_replay():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
const values=new Map();const statuses=[];const requests=[];
const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync=createLaterSync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-later',
now:()=>1_800_000_000_000,maxOfflineMs:1000,
fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:4,records:{{}}}}}},
onRemoteRecords:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}});
(async()=>{{await sync.flush();sync.enqueue('restore','issue:r:2:');
const key=[...values.keys()].find(value=>value.includes('.operation.'));
const record=JSON.parse(values.get(key));record.queued_at=1_799_999_000_000;values.set(key,JSON.stringify(record));
await sync.flush();process.stdout.write(JSON.stringify({{requests,statuses,pending:sync.pending()}}));}})();
"""
result = run_node(script)
assert result["requests"] == ["GET", "GET"]
assert ["expired", 1] in result["statuses"]
assert result["statuses"][-1] == ["expired", 1]
assert result["pending"] == []
def test_stale_receipt_adopts_server_truth_and_reports_another_device_conflict():
script = f"""
const createLaterSync=require({json.dumps(str(LATER_SYNC))});
@@ -317,10 +340,12 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
assert "laterSync.flush();" in html
assert "Later saved to account." in html
assert "Another device changed this Later item" in html
assert "Today edit expired after 30 days offline" in html
assert "Later edit expired after 30 days offline" in html
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v55" in worker
assert "stackchain-dashboard-shell-v56" in worker
+1 -1
View File
@@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v55" in worker
assert "stackchain-dashboard-shell-v56" in worker
+9
View File
@@ -57,3 +57,12 @@ def test_readme_documents_offline_unread_update_conversations_and_boundaries():
assert "previously opened unread update" in text
assert "replies enter the account-bound durable outbox" in text
assert "mark read, ownership, deferral, and older-message loading remain disabled" in text
def test_readme_documents_planning_sync_retention_contract():
text = " ".join(README.read_text().split())
assert "30 days" in text
assert "4,096 operation receipts per account" in text
assert "expired edit" in text
assert "account plan is kept" in text
+6 -6
View File
@@ -108,7 +108,7 @@ async function dispatchNotificationClick(route) {{
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -116,14 +116,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -132,21 +132,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/update-ownership.js'" in source
+90
View File
@@ -107,6 +107,96 @@ def test_batch_applies_ordered_operations_once_and_rejects_only_capacity_conflic
assert replay["duplicate_operation_ids"] == ["two", "three"]
def test_operation_receipts_are_bounded_per_account_and_existing_schema_migrates(tmp_path):
path = tmp_path / "today.sqlite3"
with sqlite3.connect(path) as connection:
connection.execute(
"CREATE TABLE today_operations (login TEXT NOT NULL, operation_id TEXT NOT NULL, "
"PRIMARY KEY (login, operation_id))"
)
connection.execute(
"INSERT INTO today_operations(login, operation_id) VALUES ('timmy', 'legacy')"
)
store = TodayStore(path, limit=20, operation_limit=3, clock=lambda: 1_000)
for index in range(5):
store.apply("timmy", f"op-{index}", "add", f"issue:r:{index}:")
store.apply("alexander", "other", "add", "issue:r:99:")
with sqlite3.connect(path) as connection:
columns = {row[1] for row in connection.execute("PRAGMA table_info(today_operations)")}
timmy = connection.execute(
"SELECT operation_id FROM today_operations WHERE login = 'timmy' ORDER BY rowid"
).fetchall()
alexander = connection.execute(
"SELECT operation_id FROM today_operations WHERE login = 'alexander'"
).fetchall()
assert "created_at" in columns
assert timmy == [("op-2",), ("op-3",), ("op-4",)]
assert alexander == [("other",)]
def test_pruned_stale_today_operation_cannot_reorder_a_newer_plan(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", limit=5, operation_limit=1)
store.apply_batch("timmy", [
{"operation_id": "seed-1", "action": "add", "item_id": "issue:r:1:", "base_revision": 0},
{"operation_id": "seed-2", "action": "add", "item_id": "issue:r:2:", "base_revision": 0},
])
store.apply_batch("timmy", [{
"operation_id": "offline-move", "action": "move", "item_id": "issue:r:2:",
"direction": "up", "base_revision": 2,
}])
newer = store.apply_batch("timmy", [{
"operation_id": "newer-move", "action": "move", "item_id": "issue:r:1:",
"direction": "up", "base_revision": 3,
}])
replay = store.apply_batch("timmy", [{
"operation_id": "offline-move", "action": "move", "item_id": "issue:r:2:",
"direction": "up", "base_revision": 2,
}])
assert replay["ids"] == newer["ids"] == ["issue:r:1:", "issue:r:2:"]
assert replay["revision"] == 4
assert replay["accepted_operation_ids"] == []
assert replay["rejected_operations"] == [
{"operation_id": "offline-move", "reason": "stale_intent"}
]
def test_today_receipt_age_pruning_is_account_scoped(tmp_path):
clock = [1_000.0]
path = tmp_path / "today.sqlite3"
store = TodayStore(
path, limit=5, operation_retention_seconds=60, clock=lambda: clock[0]
)
store.apply("timmy", "old", "add", "issue:r:1:")
store.apply("alexander", "other-old", "add", "issue:r:2:")
clock[0] += 61
store.apply("timmy", "fresh", "remove", "issue:r:1:")
with sqlite3.connect(path) as connection:
timmy = connection.execute(
"SELECT operation_id FROM today_operations WHERE login = 'timmy'"
).fetchall()
alexander = connection.execute(
"SELECT operation_id FROM today_operations WHERE login = 'alexander'"
).fetchall()
assert timmy == [("fresh",)]
assert alexander == [("other-old",)]
def test_today_api_model_preserves_client_base_revision():
operation = main.TodayOperation(
operation_id="offline", action="move", item_id="issue:r:2:",
direction="up", base_revision=7,
)
assert operation.model_dump()["base_revision"] == 7
@pytest.mark.anyio
async def test_authenticated_today_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
+39 -8
View File
@@ -74,8 +74,8 @@ sync.enqueue('add', 'issue:r:1:');
assert result == {
"sharesInflight": True,
"patches": [
{"operation_id": "op-1", "action": "add", "item_id": "issue:r:1:", "direction": None},
{"operation_id": "op-2", "action": "add", "item_id": "issue:r:2:", "direction": None},
{"operation_id": "op-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0},
{"operation_id": "op-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
],
"pending": [],
"ids": ["issue:r:1:", "issue:r:2:"],
@@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v55" in source
assert "stackchain-dashboard-shell-v56" in source
assert "BASE + 'static/today-sync.js'" in source
@@ -137,12 +137,41 @@ sync.enqueue('add', 'issue:r:2:');
"action": "add",
"item_id": "issue:r:2:",
"direction": None,
"base_revision": 0,
}]}
assert result["adopted"] == ["issue:r:9:", "issue:r:2:"]
assert result["status"] == "saved"
assert result["pending"] == []
def test_today_operations_capture_server_revision_and_expire_before_replay():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
const values = new Map(); const statuses=[]; const requests=[];
const storage={{get length(){{return values.size}},key:i=>[...values.keys()][i]||null,
getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const sync=createTodaySync({{storage,getLogin:()=> 'timmy',createOperationId:()=> 'old-op',
now:()=>1_800_000_000_000,maxOfflineMs:1000,
fetchJson:async(_url,options={{}})=>{{requests.push(options.method||'GET');return {{revision:7,ids:['server']}}}},
onRemoteIds:()=>{{}},onStatus:(state,detail={{}})=>statuses.push([state,detail.count||0])}});
(async()=>{{await sync.flush();sync.enqueue('move','issue:r:2:','up');
const pendingBefore=sync.pending();
const key=[...values.keys()].find(value=>value.includes('.operation.'));
const record=JSON.parse(values.get(key));record.queued_at=1_799_999_000_000;values.set(key,JSON.stringify(record));
await sync.flush();process.stdout.write(JSON.stringify({{pendingBefore,requests,statuses,pendingAfter:sync.pending()}}));}})();
"""
result = json.loads(subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout)
assert result["pendingBefore"] == [{
"operation_id": "old-op", "action": "move", "item_id": "issue:r:2:",
"direction": "up", "base_revision": 7,
}]
assert result["requests"] == ["GET", "GET"]
assert ["expired", 1] in result["statuses"]
assert result["statuses"][-1] == ["expired", 1]
assert result["pendingAfter"] == []
def test_pending_today_edits_are_sent_as_one_batch_without_a_preflight_get():
script = f"""
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
@@ -184,6 +213,7 @@ sync.enqueue('move', 'issue:r:2:', 'up');
"action": "move",
"item_id": "issue:r:2:",
"direction": "up",
"base_revision": 0,
}],
"status": "retrying",
}
@@ -221,7 +251,7 @@ sync.enqueue('add','issue:r:2:');
assert result["scheduled"] == [2000]
assert result["attempts"] == 2
assert result["patches"] == [{
"operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None,
"operation_id": "stable-op", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0,
}]
assert result["pending"] == []
assert ["retrying", 2000] in result["statuses"]
@@ -254,6 +284,7 @@ process.stdout.write(JSON.stringify({{first, duplicate, pending:sync.pending(),
"action": "remove",
"item_id": "issue:r:1:",
"direction": None,
"base_revision": 0,
}],
"statuses": ["pending", "pending"],
}
@@ -280,8 +311,8 @@ process.stdout.write(JSON.stringify({{first, second, pending:sync.pending()}}));
"first": True,
"second": False,
"pending": [
{"operation_id": "migration-1", "action": "add", "item_id": "issue:r:1:", "direction": None},
{"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None},
{"operation_id": "migration-1", "action": "add", "item_id": "issue:r:1:", "direction": None, "base_revision": 0},
{"operation_id": "migration-2", "action": "add", "item_id": "issue:r:2:", "direction": None, "base_revision": 0},
],
}
@@ -346,8 +377,8 @@ sync.enqueue('remove', '5');
assert result == {
"result": False,
"patches": [
{"operation_id": "op-1", "action": "add", "item_id": "6", "direction": None},
{"operation_id": "op-2", "action": "remove", "item_id": "5", "direction": None},
{"operation_id": "op-1", "action": "add", "item_id": "6", "direction": None, "base_revision": 0},
{"operation_id": "op-2", "action": "remove", "item_id": "5", "direction": None, "base_revision": 0},
],
"pending": [],
"ids": ["1", "2", "3", "4"],