feat: attach screenshots during issue capture (#469)
This commit is contained in:
1 parent
11341ec5d9
commit
5bf491c725
19 files changed
+488
-51
No files matched your search
@@ -64,6 +64,71 @@ const fetchJson = async (url, options = {{}}) => {{
|
||||
}
|
||||
|
||||
|
||||
def test_capture_attachment_retry_resumes_after_creation_without_duplicate_issue():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
let item = {{
|
||||
id:'capture-image',operationId:'capture-image',ownerLogin:'timmy',status:'queued',
|
||||
repository:'stackchain/dashboard',title:'Broken mobile layout',body:'At 320px',labelIds:[],
|
||||
attachment:{{filename:'phone.png',contentType:'image/png',data:'iVBORw0KGgo='}},
|
||||
}};
|
||||
const state = {{calls:[],completed:0,released:0}};
|
||||
const store = {{
|
||||
claimNext:async()=>item ? {{...item}} : null,
|
||||
update:async(_id,transform)=>{{item=transform(item);}},
|
||||
complete:async()=>{{state.completed+=1;item=null;}},
|
||||
release:async()=>{{state.released+=1;item={{...item,status:'queued'}};}},
|
||||
fail:async()=>{{}},countBlocked:async()=>0,
|
||||
}};
|
||||
let uploadAttempts=0;
|
||||
const fetchJson=async(url,options={{}})=>{{
|
||||
state.calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body&&JSON.parse(options.body)}});
|
||||
if(url==='api/v1/background-identity')return{{login:'timmy'}};
|
||||
if(url.endsWith('/issues'))return{{repository:'stackchain/dashboard',number:469,title:'Broken mobile layout'}};
|
||||
if(url.endsWith('/attachments') && uploadAttempts++ === 0){{const error=new Error('Upload unavailable');error.status=503;throw error;}}
|
||||
if(url.endsWith('/attachments'))return{{markdown:''}};
|
||||
if(url.endsWith('/comments'))return{{id:91}};
|
||||
}};
|
||||
(async()=>{{
|
||||
const sync=createBackgroundIssueSync({{store,fetchJson}});
|
||||
let firstError='';try{{await sync.flush();}}catch(error){{firstError=error.message;}}
|
||||
const afterFirst={{...item}};
|
||||
const second=await sync.flush();
|
||||
process.stdout.write(JSON.stringify({{state,firstError,afterFirst,second}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["firstError"] == "Upload unavailable"
|
||||
assert output["afterFirst"]["deliveredIssue"]["number"] == 469
|
||||
assert [call["url"] for call in output["state"]["calls"]].count(
|
||||
"api/v1/repos/stackchain/dashboard/issues"
|
||||
) == 1
|
||||
assert output["state"]["calls"][-2]["key"] == "capture-image:attachment"
|
||||
assert output["state"]["calls"][-1]["key"] == "capture-image:attachment-comment"
|
||||
assert output["state"]["calls"][-1]["body"] == {
|
||||
"body": ""
|
||||
}
|
||||
assert output["state"]["completed"] == 1
|
||||
assert output["second"]["confirmed"][0]["number"] == 469
|
||||
|
||||
|
||||
def test_capture_attachment_stage_keys_remain_within_idempotency_limit():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
|
||||
const item={{id:'long',operationId:'x'.repeat(128),ownerLogin:'timmy',repository:'o/r',title:'Bug',body:'',labelIds:[],attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}};
|
||||
const keys=[];let queued=true;
|
||||
const store={{claimNext:async()=>queued?(queued=false,item):null,update:async()=>{{}},complete:async()=>{{}},release:async()=>{{}},fail:async()=>{{}},countBlocked:async()=>0}};
|
||||
const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};if(options.headers?.['Idempotency-Key'])keys.push(options.headers['Idempotency-Key']);if(url.endsWith('/issues'))return{{repository:'o/r',number:1}};if(url.endsWith('/attachments'))return{{markdown:''}};return{{id:1}};}};
|
||||
createBackgroundIssueSync({{store,fetchJson}}).flush().then(()=>process.stdout.write(JSON.stringify(keys)));
|
||||
"""
|
||||
keys = run_node(script)
|
||||
|
||||
assert len(keys) == 3
|
||||
assert len(set(keys)) == 3
|
||||
assert all(len(key) <= 128 for key in keys)
|
||||
|
||||
|
||||
def test_closed_app_sync_retains_created_issue_for_create_and_start_recovery():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
@@ -289,6 +354,25 @@ const transaction=work=>{{const run=tail.then(()=>work({{
|
||||
]
|
||||
|
||||
|
||||
def test_stale_foreground_upsert_preserves_confirmed_attachment_delivery_stages():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
|
||||
const records=new Map();let tail=Promise.resolve();
|
||||
const transaction=work=>{{const run=tail.then(()=>work({{getAll:async()=>[...records.values()].map(value=>({{...value}})),put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id)}}));tail=run.catch(()=>{{}});return run;}};
|
||||
(async()=>{{
|
||||
const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
|
||||
await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}});
|
||||
await store.update('capture',item=>({{...item,deliveredIssue:{{number:7}},attachmentMarkdown:''}}));
|
||||
const result=await store.upsert({{id:'capture',operationId:'same',ownerLogin:'timmy',status:'queued',attachment:{{filename:'a.png'}}}});
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["deliveredIssue"]["number"] == 7
|
||||
assert output["attachmentMarkdown"] == ""
|
||||
|
||||
|
||||
def test_indexeddb_store_closes_on_version_change_and_reopens_afterward():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
|
||||
@@ -281,4 +281,4 @@ async def test_current_today_update_offers_reply_and_next_without_marking_read()
|
||||
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-v80" in worker
|
||||
assert "stackchain-dashboard-shell-v81" in worker
|
||||
@@ -54,6 +54,43 @@ controller.select(file);
|
||||
}
|
||||
|
||||
|
||||
def test_selected_screenshot_serializes_for_durable_issue_capture_without_uploading():
|
||||
script = f"""
|
||||
const attachment = require({json.dumps(str(ATTACHMENT))});
|
||||
const calls=[];
|
||||
const controller=attachment.create({{
|
||||
readDataUrl:async file=>{{calls.push('read:'+file.name);return 'data:image/webp;base64,UklGRg==';}},
|
||||
upload:async()=>{{calls.push('upload');}},
|
||||
}});
|
||||
controller.select({{name:'phone.webp',type:'image/webp',size:8}});
|
||||
controller.serialize().then(value=>process.stdout.write(JSON.stringify({{value,calls}})));
|
||||
"""
|
||||
output = json.loads(run_node(script))
|
||||
|
||||
assert output == {
|
||||
"value": {
|
||||
"filename": "phone.webp",
|
||||
"contentType": "image/webp",
|
||||
"data": "UklGRg==",
|
||||
},
|
||||
"calls": ["read:phone.webp"],
|
||||
}
|
||||
|
||||
|
||||
def test_serialized_capture_attachment_can_be_restored_or_removed_while_editing():
|
||||
script = f"""
|
||||
const attachment=require({json.dumps(str(ATTACHMENT))});
|
||||
const controller=attachment.create({{readDataUrl:async()=>{{throw new Error('must not reread');}},upload:async()=>{{}}}});
|
||||
controller.restore({{filename:'saved.png',contentType:'image/png',data:'iVBORw0KGgo='}});
|
||||
(async()=>{{const restored={{state:controller.state(),value:await controller.serialize()}};controller.clear();process.stdout.write(JSON.stringify({{restored,removed:await controller.serialize()}}));}})();
|
||||
"""
|
||||
output = json.loads(run_node(script))
|
||||
|
||||
assert output["restored"]["state"]["name"] == "saved.png"
|
||||
assert output["restored"]["value"]["data"] == "iVBORw0KGgo="
|
||||
assert output["removed"] is None
|
||||
|
||||
|
||||
def test_issue_composer_renders_thumb_reachable_screenshot_preview():
|
||||
html = INDEX.read_text()
|
||||
css = CSS.read_text()
|
||||
@@ -69,6 +106,23 @@ def test_issue_composer_renders_thumb_reachable_screenshot_preview():
|
||||
assert 'min-height:44px' in css
|
||||
|
||||
|
||||
def test_new_issue_sheet_captures_screenshot_into_durable_outbox():
|
||||
html = INDEX.read_text()
|
||||
source = DASHBOARD.read_text()
|
||||
css = CSS.read_text()
|
||||
|
||||
assert 'id="create-issue-attachment"' in html
|
||||
assert 'accept="image/png,image/jpeg,image/webp"' in html
|
||||
assert 'id="create-issue-attachment-preview"' in html
|
||||
assert 'id="remove-create-issue-attachment"' in html
|
||||
assert "const createIssueAttachmentController = issueAttachment.mount({" in source
|
||||
assert "attachment: await createIssueAttachmentController.serialize()" in source
|
||||
assert "createIssueAttachmentController.restore(queued.attachment);" in source
|
||||
assert "createIssueAttachmentController.clear();" in source
|
||||
assert ".create-issue-attachment" in css
|
||||
assert "overflow-x:hidden" in css
|
||||
|
||||
|
||||
def test_attachment_view_keeps_invalid_draft_and_removes_preview():
|
||||
script = f"""
|
||||
const attachment = require({json.dumps(str(ATTACHMENT))});
|
||||
@@ -132,3 +186,5 @@ def test_readme_documents_mobile_screenshot_limits_and_delivery_order():
|
||||
assert "PNG, JPEG, or WebP" in readme
|
||||
assert "2 MB" in readme
|
||||
assert "uploads before the comment is posted" in readme
|
||||
assert "New issue" in readme
|
||||
assert "retry resumes with the confirmed issue" in readme
|
||||
@@ -71,6 +71,29 @@ process.stdout.write(JSON.stringify({{items:reloaded, stored:JSON.parse(values.g
|
||||
assert output["stored"]["version"] == 3
|
||||
|
||||
|
||||
def test_issue_outbox_persists_a_validated_screenshot_with_the_capture():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map();
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const outbox = createIssueOutbox({{
|
||||
storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'capture-image-1',
|
||||
}});
|
||||
outbox.enqueue({{
|
||||
repository:'stackchain/dashboard',title:'Layout breaks',body:'At 320px',
|
||||
attachment:{{filename:'phone.webp',contentType:'image/webp',data:'UklGRg=='}},
|
||||
}});
|
||||
process.stdout.write(JSON.stringify(createIssueOutbox({{storage}}).list()[0]));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["attachment"] == {
|
||||
"filename": "phone.webp",
|
||||
"contentType": "image/webp",
|
||||
"data": "UklGRg==",
|
||||
}
|
||||
|
||||
|
||||
def test_issue_outbox_persists_create_and_start_intent_and_returns_it_with_confirmation():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
@@ -154,6 +177,50 @@ outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{resul
|
||||
assert output["remaining"][0]["status"] == "queued"
|
||||
|
||||
|
||||
def test_foreground_attachment_retry_does_not_create_a_second_issue():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values=new Map();const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const calls=[];let uploadAttempts=0;
|
||||
const outbox=createIssueOutbox({{
|
||||
storage,getOwnerLogin:()=>'timmy',createOperationId:()=>'image-op',
|
||||
fetchJson:async(url,options={{}})=>{{
|
||||
calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body&&JSON.parse(options.body)}});
|
||||
if(url.endsWith('/issues'))return{{repository:'o/r',number:7,title:'Visual bug'}};
|
||||
if(url.endsWith('/attachments') && uploadAttempts++ === 0){{const error=new Error('offline');error.status=503;throw error;}}
|
||||
if(url.endsWith('/attachments'))return{{markdown:''}};
|
||||
return{{id:8}};
|
||||
}},
|
||||
}});
|
||||
const queued=outbox.enqueue({{repository:'o/r',title:'Visual bug',attachment:{{filename:'screen.png',contentType:'image/png',data:'abc'}}}});
|
||||
(async()=>{{await outbox.flush('timmy');const partial=outbox.list()[0];const result=await outbox.retry(queued.id,'timmy');process.stdout.write(JSON.stringify({{calls,partial,result,remaining:outbox.list()}}));}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["partial"]["deliveredIssue"]["number"] == 7
|
||||
assert [call["url"] for call in output["calls"]].count("api/v1/repos/o/r/issues") == 1
|
||||
assert output["calls"][-1]["url"] == "api/v1/repos/o/r/issues/7/comments"
|
||||
assert output["result"]["confirmed"][0]["number"] == 7
|
||||
assert output["remaining"] == []
|
||||
|
||||
|
||||
def test_replacing_a_partial_capture_screenshot_keeps_issue_and_restarts_upload_stage():
|
||||
script = f"""
|
||||
const createIssueOutbox=require({json.dumps(str(OUTBOX))});
|
||||
const values=new Map();const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};let sequence=0;
|
||||
const outbox=createIssueOutbox({{storage,getOwnerLogin:()=>'timmy',createOperationId:()=> 'op-'+(++sequence)}});
|
||||
const queued=outbox.enqueue({{repository:'o/r',title:'Visual',attachment:{{filename:'old.png',contentType:'image/png',data:'old'}}}});
|
||||
values.set('stackchain.issue-outbox.v1',JSON.stringify({{version:3,items:[{{...queued,deliveredIssue:{{repository:'o/r',number:7}},attachmentMarkdown:''}}]}}));
|
||||
const updated=outbox.update(queued.id,{{...queued,attachment:{{filename:'new.png',contentType:'image/png',data:'new'}}}});
|
||||
process.stdout.write(JSON.stringify(updated));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["deliveredIssue"]["number"] == 7
|
||||
assert output["attachment"]["filename"] == "new.png"
|
||||
assert "attachmentMarkdown" not in output
|
||||
|
||||
|
||||
def test_issue_outbox_preserves_permanent_failures_for_edit_and_retry():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
@@ -434,6 +501,21 @@ process.stdout.write(JSON.stringify(outbox.list()));
|
||||
]
|
||||
|
||||
|
||||
def test_page_reconciliation_keeps_confirmed_attachment_delivery_stages():
|
||||
script = f"""
|
||||
const createIssueOutbox=require({json.dumps(str(OUTBOX))});
|
||||
const values=new Map();const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const outbox=createIssueOutbox({{storage,getOwnerLogin:()=>'timmy',createOperationId:()=>'staged'}});
|
||||
outbox.enqueue({{repository:'o/r',title:'Visual',attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}});
|
||||
outbox.reconcileBackground([{{id:'staged',status:'queued',deliveredIssue:{{repository:'o/r',number:7}},attachmentMarkdown:''}}]);
|
||||
process.stdout.write(JSON.stringify(outbox.list()[0]));
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["deliveredIssue"]["number"] == 7
|
||||
assert output["attachmentMarkdown"] == ""
|
||||
|
||||
|
||||
def test_page_preserves_background_create_and_start_until_the_matching_account_continues_it():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
|
||||
@@ -347,5 +347,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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/later-sync.js'" in source
|
||||
@@ -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-v80" in worker
|
||||
assert "stackchain-dashboard-shell-v81" in worker
|
||||
@@ -35,7 +35,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-v80" in worker
|
||||
assert "stackchain-dashboard-shell-v81" in worker
|
||||
|
||||
|
||||
def test_all_conversation_composers_offer_accessible_mobile_mentions():
|
||||
|
||||
@@ -292,6 +292,6 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||
source = SERVICE_WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/plan-today.js'" in source
|
||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||
@@ -122,7 +122,7 @@ async function dispatchNotificationClick(route) {{
|
||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/my-work.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
@@ -131,14 +131,14 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" 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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/today-completion.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -146,7 +146,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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
|
||||
@@ -154,14 +154,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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" 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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
@@ -170,21 +170,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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" 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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" 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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
@@ -385,7 +385,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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/queue-today.js'" in source
|
||||
|
||||
|
||||
|
||||
@@ -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-v80';" in service_worker
|
||||
assert "const CACHE = 'stackchain-dashboard-shell-v81';" in service_worker
|
||||
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ sync.enqueueConfiguration(120, {{'issue:r:1:':60}});
|
||||
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-v80" in source
|
||||
assert "stackchain-dashboard-shell-v81" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user