feat: prepare conflict-safe release rollback pulls (Closes #1370)
CI / lint (pull_request) Successful in 3m11s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Failing after 6m7s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-24 23:55:33 +00:00
1 parent 400534f446
commit b753cdd922
25 files changed
+754 -43

No files matched your search

@@ -89,3 +89,47 @@ def test_failed_release_check_is_diagnosable_and_retryable_on_phone(viewport):
assert result["state"] == "Checks running"
assert [call["method"] for call in result["calls"]] == ["GET", "GET", "POST"]
browser.close()
@pytest.mark.parametrize("viewport", [
{"width": 320, "height": 568},
{"width": 390, "height": 844},
])
def test_failed_release_prepares_a_visible_draft_rollback_receipt_on_phone(viewport):
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport=viewport)
page.set_content((FRONTEND / "index.html").read_text())
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "release-receipt.js")
result = page.evaluate("""async () => {
const values=new Map(), calls=[], opened=[];
const receipt=createReleaseReceipt({
storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)},
getLogin:()=> 'timmy', listNode:document.querySelector('#release-watchlist'),
confirmAction:()=>true, openPull:pull=>opened.push(pull),
fetchJson:async(path, options={})=>{
calls.push({path,method:options.method||'GET'});
if(path.endsWith('/failure')) return {commit_sha:'abc1234',run_id:91,job_index:3,name:'CI / browser',excerpt:'failed'};
if(path.endsWith('/rollback')) return {repository:'stackchain/api',number:9,draft:true,rollback_of:'abc1234',url:'https://forge.example/git/stackchain/api/pulls/9'};
return {commit_sha:'abc1234',ci_state:'failure',release:null,checks:[{name:'CI / browser',state:'failure',recovery:{run_id:91,job_index:3}}]};
},
});
receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'},{merge_commit_sha:'abc1234'});
document.querySelector('#release-receipt-sheet').showModal();
await receipt.refresh();
document.querySelector('.release-failure-summary button').click();
await new Promise(resolve=>setTimeout(resolve,0));
const rollback=[...document.querySelectorAll('.release-failure-actions button')].find(button=>button.textContent==='Prepare rollback PR');
const actionHeight=rollback.getBoundingClientRect().height;
rollback.click();
await new Promise(resolve=>setTimeout(resolve,0));
const status=document.querySelector('.release-rollback-status');
return {calls,opened,actionHeight,status:status?.textContent||'',overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth};
}""")
assert result["actionHeight"] >= 44
assert result["status"] == "Draft rollback #9 is ready for review."
assert result["opened"][0]["number"] == 9
assert result["calls"][-1]["path"].endswith("/release-receipt/abc1234/rollback")
assert result["overflow"] is False
browser.close()
+1 -1
View File
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-reply-actions button { min-height:44px;' in html
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v138" in worker
assert "stackchain-dashboard-shell-v139" in worker
+5
View File
@@ -2,6 +2,7 @@ import asyncio
import sqlite3
import threading
import time
from typing import get_args
from urllib.parse import parse_qs, urlsplit
import httpx
@@ -13,6 +14,10 @@ from src.session_store import SessionStoreError
from src.views import FRONTEND_BUILD
def test_release_rollback_is_a_supported_step_up_action():
assert "prepare_release_rollback" in get_args(main.StepUpAction)
@pytest.fixture
def access_control(monkeypatch, tmp_path):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
+1 -1
View File
@@ -593,7 +593,7 @@ process.stdout.write(JSON.stringify({{
assert ".following-disposition-mode" in css
assert "if (searchPreviewReturnKind === 'following')" in dashboard
assert "e.key === 'Escape' && searchPreviewReturnKind === 'following'" in dashboard
assert "stackchain-dashboard-shell-v138" in service_worker
assert "stackchain-dashboard-shell-v139" in service_worker
def test_prepare_today_lazily_refreshes_and_directly_reviews_following():
+1 -1
View File
@@ -435,5 +435,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
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-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -256,4 +256,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-v138" in worker
assert "stackchain-dashboard-shell-v139" in worker
+1 -1
View File
@@ -45,7 +45,7 @@ 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-v138" in worker
assert "stackchain-dashboard-shell-v139" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "controller.recoverPermission('deadline')" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v138" in worker
assert "stackchain-dashboard-shell-v139" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -274,5 +274,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
def test_mobile_insights_rolls_into_the_offline_shell():
worker = (CONTROLLER.parent / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v138" in worker
assert "stackchain-dashboard-shell-v139" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -414,7 +414,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
assert "stackchain-dashboard-shell-v138" in service_worker
assert "stackchain-dashboard-shell-v139" in service_worker
@pytest.mark.anyio
+1 -1
View File
@@ -418,7 +418,7 @@ async def test_starting_saved_today_work_closes_a_concurrent_rollover_planner():
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
+229
View File
@@ -1,4 +1,5 @@
import asyncio
import base64
import json
import httpx
@@ -8,6 +9,15 @@ from src import gitea_proxy, main
from src.security_event_store import SecurityEventStoreError
def _content_payload(text: str, sha: str) -> dict:
return {
"type": "file",
"encoding": "base64",
"content": base64.b64encode(text.encode()).decode(),
"sha": sha,
}
@pytest.mark.anyio
async def test_gitea_updates_authored_pull_branch_and_confirms_new_head():
requests = []
@@ -2320,3 +2330,222 @@ async def test_source_branch_cleanup_resolves_a_lost_delete_response_with_one_ab
assert result["deleted"] is True
assert sum(method == "GET" and path.endswith("/branches/timmy%2Ffeature") for method, path in requests) == 2
@pytest.mark.anyio
async def test_prepare_release_rollback_creates_one_atomic_draft_pull_from_exact_merge_parent():
requests = []
branch_created = False
merge_sha = "abc1234def5678"
parent_sha = "1111111aaaaaaa"
current_sha = "2222222bbbbbbb"
rollback_sha = "3333333ccccccc"
branch = "timmy/rollback-7-abc1234def56"
async def handler(request):
nonlocal branch_created
path = request.url.raw_path.decode().split("?", 1)[0]
requests.append((request.method, path))
if path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
if path.endswith("/pulls/7") and request.method == "GET":
return httpx.Response(200, json={
"number": 7,
"state": "closed",
"merged": True,
"merge_commit_sha": merge_sha,
"title": "Ship release",
"user": {"login": "timmy"},
"assignees": [],
})
if path.endswith("/repos/stackchain/api"):
return httpx.Response(200, json={
"default_branch": "main", "permissions": {"push": True},
})
if path.endswith(f"/git/commits/{merge_sha}"):
return httpx.Response(200, json={
"sha": merge_sha,
"parents": [{"sha": parent_sha}, {"sha": "feature-parent"}],
"files": [{"filename": "app.txt", "status": "modified"}],
})
if path.endswith("/contents/app.txt"):
ref = request.url.params.get("ref")
if ref == merge_sha:
return httpx.Response(200, json=_content_payload("broken\n", "merge-blob"))
if ref == parent_sha:
return httpx.Response(200, json=_content_payload("working\n", "parent-blob"))
if ref == "main":
return httpx.Response(200, json=_content_payload("broken\n", "current-blob"))
if path.endswith("/branches/timmy%2Frollback-7-abc1234def56"):
if request.method == "GET":
return httpx.Response(
200, json={"commit": {"id": rollback_sha}}
) if branch_created else httpx.Response(404)
if path.endswith("/contents") and request.method == "POST":
body = json.loads(request.content)
assert body == {
"branch": "main",
"new_branch": branch,
"message": f"Revert {merge_sha} from pull #7",
"files": [{
"operation": "update",
"path": "app.txt",
"sha": "current-blob",
"content": base64.b64encode(b"working\n").decode(),
}],
}
branch_created = True
return httpx.Response(201, json={"commit": {"sha": rollback_sha}})
if path.endswith("/pulls") and request.method == "GET":
return httpx.Response(200, json=[])
if path.endswith("/pulls") and request.method == "POST":
body = json.loads(request.content)
assert body["head"] == branch
assert body["base"] == "main"
assert body["draft"] is True
assert f"Rollback of #{7} at `{merge_sha}`" in body["body"]
return httpx.Response(201, json={
"number": 8,
"title": body["title"],
"body": body["body"],
"state": "open",
"draft": True,
"head": {"ref": branch, "sha": rollback_sha},
"base": {"ref": "main"},
"user": {"login": "timmy"},
"html_url": "https://forge.example/git/stackchain/api/pulls/8",
})
raise AssertionError(f"unexpected request: {request.method} {request.url}")
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
result = await gitea_proxy.prepare_release_rollback(
"stackchain/api", 7, merge_sha
)
finally:
await gitea_proxy.stop_client()
assert result["number"] == 8
assert result["draft"] is True
assert result["head"] == {"ref": branch, "sha": rollback_sha}
assert result["rollback_of"] == merge_sha
assert result["files_changed"] == 1
assert sum(method == "POST" and path.endswith("/contents") for method, path in requests) == 1
assert sum(method == "POST" and path.endswith("/pulls") for method, path in requests) == 1
@pytest.mark.anyio
async def test_release_rollback_endpoint_requires_failed_exact_commit_and_audits_draft_pull(monkeypatch):
lifecycle = []
class Journal:
def reserve(self, kind, *, target):
lifecycle.append(("reserve", kind, target))
return "rollback-operation"
def finalize(self, operation_id):
lifecycle.append(("finalize", operation_id))
def discard(self, operation_id):
lifecycle.append(("discard", operation_id))
async def status(repository, commit_sha):
lifecycle.append(("status", repository, commit_sha))
return {"commit_sha": commit_sha, "ci_state": "failure", "release": None}
async def prepare(repository, number, commit_sha):
lifecycle.append(("prepare", repository, number, commit_sha))
return {
"repository": repository,
"number": 9,
"state": "open",
"draft": True,
"head": {"ref": "timmy/rollback-7-abc1234", "sha": "def5678"},
"base": {"ref": "main"},
"url": "https://forge.example/git/stackchain/api/pulls/9",
"rollback_of": commit_sha,
"files_changed": 2,
"existing": False,
}
monkeypatch.setattr(main, "_security_event_store", lambda: Journal())
monkeypatch.setattr(main.gitea_proxy, "release_receipt_status", status)
monkeypatch.setattr(main.gitea_proxy, "prepare_release_rollback", prepare, raising=False)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/api/v1/repos/stackchain/api/pulls/7/release-receipt/abc1234/rollback"
)
assert response.status_code == 201
assert response.headers["cache-control"] == "no-store"
assert response.json()["rollback_of"] == "abc1234"
assert lifecycle == [
("status", "stackchain/api", "abc1234"),
("reserve", "release_rollback_prepared", "stackchain/api#7@abc1234"),
("prepare", "stackchain/api", 7, "abc1234"),
("finalize", "rollback-operation"),
]
@pytest.mark.anyio
async def test_prepare_release_rollback_reconciles_a_concurrent_branch_creation_without_duplicate_commit():
merge_sha = "abc1234def5678"
rollback_sha = "3333333ccccccc"
branch_reads = 0
pull_created = False
async def handler(request):
nonlocal branch_reads, pull_created
path = request.url.raw_path.decode().split("?", 1)[0]
if path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
if path.endswith("/pulls/7"):
return httpx.Response(200, json={
"state": "closed", "merged": True, "merge_commit_sha": merge_sha,
"title": "Ship release", "user": {"login": "timmy"}, "assignees": [],
})
if path.endswith("/repos/stackchain/api"):
return httpx.Response(200, json={"default_branch": "main", "permissions": {"push": True}})
if path.endswith(f"/git/commits/{merge_sha}"):
return httpx.Response(200, json={
"sha": merge_sha, "parents": [{"sha": "parent123"}, {"sha": "feature123"}],
"files": [{"filename": "app.txt", "status": "modified"}],
})
if path.endswith("/contents/app.txt"):
ref = request.url.params["ref"]
return httpx.Response(200, json=_content_payload(
"working\n" if ref == "parent123" else "broken\n",
"current-blob" if ref == "main" else ref + "-blob",
))
if path.endswith("/branches/timmy%2Frollback-7-abc1234def56"):
branch_reads += 1
return httpx.Response(404) if branch_reads == 1 else httpx.Response(
200, json={"commit": {"id": rollback_sha}}
)
if path.endswith("/contents") and request.method == "POST":
return httpx.Response(422, json={"message": "branch already exists"})
if path.endswith("/pulls") and request.method == "GET":
return httpx.Response(200, json=[])
if path.endswith("/pulls") and request.method == "POST":
pull_created = True
body = json.loads(request.content)
return httpx.Response(201, json={
"number": 9, "title": body["title"], "body": body["body"],
"state": "open", "draft": True,
"head": {"ref": body["head"], "sha": rollback_sha},
"base": {"ref": body["base"]}, "user": {"login": "timmy"},
"html_url": "https://forge.example/git/stackchain/api/pulls/9",
})
raise AssertionError(f"unexpected request {request.method} {request.url}")
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
result = await gitea_proxy.prepare_release_rollback("stackchain/api", 7, merge_sha)
finally:
await gitea_proxy.stop_client()
assert result["number"] == 9
assert result["head"]["sha"] == rollback_sha
assert branch_reads == 3 # initial check, race reconciliation, pull-creation revalidation
assert pull_created is True
+50 -1
View File
@@ -174,6 +174,54 @@ receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {
assert "release-failure-recovery" in output["classes"]
def test_diagnosed_failed_release_prepares_one_rollback_pull_and_opens_mobile_workspace():
output = run_node(r"""
function node(tag='div') { return {tag,children:[],hidden:false,textContent:'',className:'',disabled:false,append(...xs){this.children.push(...xs)},replaceChildren(...xs){this.children=[...xs]},setAttribute(k,v){this[k]=v},addEventListener(k,fn){this[k]=fn}}; }
global.document={createElement:tag=>node(tag)};
const values=new Map(), calls=[], opened=[], listNode=node(), dialog={open:true,close(){this.open=false}};
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const receipt=createReleaseReceipt({
storage,getLogin:()=> 'timmy',listNode,dialog,confirmAction:()=>true,openPull:pull=>opened.push(pull),
fetchJson:async(path,options={})=>{
calls.push({path,method:options.method||'GET'});
if (path.endsWith('/failure')) return {commit_sha:'abc1234',run_id:91,job_index:3,name:'CI / browser',excerpt:'AssertionError: failed'};
if (path.endsWith('/rollback')) return {repository:'stackchain/api',number:9,draft:true,url:'https://forge.example/git/stackchain/api/pulls/9',rollback_of:'abc1234'};
return {commit_sha:'abc1234',ci_state:'failure',checks:[{name:'CI / browser',state:'failure',recovery:{run_id:91,job_index:3}}],release:null};
},
});
const flatten=root=>[root,...root.children.flatMap(flatten)];
receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'}, {merge_commit_sha:'abc1234'});
(async()=>{
await receipt.refresh();
const before=flatten(listNode).filter(value=>value.textContent==='Prepare rollback PR').length;
await flatten(listNode).find(value=>value.textContent==='Review failure').click();
const rollback=flatten(listNode).find(value=>value.textContent==='Prepare rollback PR');
if (rollback) await rollback.click();
process.stdout.write(JSON.stringify({before,found:!!rollback,calls,opened,dialogOpen:dialog.open,status:receipt.restore()[0].rollback||null}));
})();
""")
assert output["before"] == 0
assert output["found"] is True
assert output["calls"][-1] == {
"path": "api/v1/repos/stackchain/api/pulls/7/release-receipt/abc1234/rollback",
"method": "POST",
}
assert output["opened"] == [{
"repository": "stackchain/api",
"number": 9,
"draft": True,
"url": "https://forge.example/git/stackchain/api/pulls/9",
"rollback_of": "abc1234",
"kind": "pull",
"key": "stackchain/api#9",
"state": "open",
}]
assert output["dialogOpen"] is False
assert output["status"]["state"] == "prepared"
assert output["status"]["number"] == 9
def test_watchlist_preserves_distinct_merges_deduplicates_and_dismisses_one():
output = run_node(r"""
const values=new Map();
@@ -345,8 +393,9 @@ def test_mobile_release_receipt_is_wired_into_the_merge_flow_and_phone_safe():
assert 'id="release-receipt-launcher"' in html
assert 'id="release-receipt-sheet"' in html
assert 'id="release-watchlist"' in html
assert 'releaseReceipt.capture(merging, mergeResult)' in dashboard
assert 'rR.capture(merging, mergeResult)' in dashboard
assert "listNode:qs('#release-watchlist')" in dashboard
assert "openPull:openPullSheet" in dashboard
assert "min-height:44px" in css[css.index(".release-receipt-sheet"):]
assert "overflow-x:hidden" in css[css.index(".release-receipt-sheet"):]
assert ".release-watchlist-item" in css
+6
View File
@@ -12,6 +12,12 @@ def test_source_branch_cleanup_has_a_specific_security_activity_label():
assert "source_branch_deleted: 'Source branch deleted'" in source
def test_release_rollback_has_a_specific_security_activity_label():
source = SECURITY_CENTER.read_text()
assert "release_rollback_prepared: 'Release rollback prepared'" in source
def test_open_security_center_loads_all_sections_concurrently_and_is_awaitable():
harness = f"""
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
+16 -16
View File
@@ -189,7 +189,7 @@ async function dispatchPush(payload) {{
def test_week_unplan_undo_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/week-plan.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -197,20 +197,20 @@ def test_week_unplan_undo_rolls_the_offline_shell():
def test_private_today_action_mailbox_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/week-plan.js'" in source
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -219,7 +219,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/authored-outbox.js'" in source
assert "BASE + 'static/background-issue-sync.js'" in source
@@ -228,7 +228,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -236,14 +236,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/dashboard.js'" in source
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -251,7 +251,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -259,7 +259,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/issue-sheet.js'" in source
assert "BASE + 'static/checklist-conflict.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -269,14 +269,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" 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-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -285,21 +285,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-v138" in source
assert "stackchain-dashboard-shell-v139" 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-v138" in source
assert "stackchain-dashboard-shell-v139" 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-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -1354,7 +1354,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
def test_queue_today_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/queue-today.js'" in source
+1 -1
View File
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
assert "const CACHE = 'stackchain-dashboard-shell-v138';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v139';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
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-v138" in source
assert "stackchain-dashboard-shell-v139" in source
assert "BASE + 'static/today-sync.js'" in source