import json import re import subprocess from pathlib import Path ATTACHMENT = Path(__file__).parents[1] / "frontend" / "issue-attachment.js" INDEX = Path(__file__).parents[1] / "frontend" / "index.html" CSS = Path(__file__).parents[1] / "frontend" / "dashboard.css" DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js" SERVICE_WORKER = Path(__file__).parents[1] / "frontend" / "service-worker.js" README = Path(__file__).parents[1] / "README.md" def run_node(script: str) -> str: return subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout def test_mobile_attachment_prepares_comment_and_reuses_confirmed_binary_upload(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); const calls = []; const file = new Blob(['png-bytes'],{{type:'image/png'}}); file.name='checkout.png'; const controller = attachment.create({{ createOperationId: () => 'attachment-comment-471', readDataUrl: async () => {{ throw new Error('base64 conversion must not run'); }}, upload: async payload => {{ calls.push({{ repository:payload.repository,number:payload.number,filename:payload.filename, content_type:payload.content_type,isBlob:payload.blob instanceof Blob, text:await payload.blob.text(),operation_id:payload.operation_id }}); return {{markdown:'![checkout.png](https://forge.example/a.png)'}}; }}, }}); controller.select(file); (async()=>{{ const first = await controller.prepareComment({{repository:'stackchain/api', number:17}}, 'Layout breaks'); const second = await controller.prepareComment({{repository:'stackchain/api', number:17}}, 'Layout breaks'); process.stdout.write(JSON.stringify({{first,second,calls,state:controller.state()}})); }})().catch(error=>{{ console.error(error); process.exit(1); }}); """ output = json.loads(run_node(script)) assert output["first"] == ( "Layout breaks\n\n![checkout.png](https://forge.example/a.png)" ) assert output["second"] == output["first"] assert output["calls"] == [{ "repository": "stackchain/api", "number": 17, "filename": "checkout.png", "content_type": "image/png", "isBlob": True, "text": "png-bytes", "operation_id": "attachment-comment-471", }] assert output["state"] == { "name": "checkout.png", "size": 9, "uploaded": True } def test_mobile_attachment_retry_reuses_operation_key_until_file_changes(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); const keys=[]; let sequence=0; let fail=true; const controller=attachment.create({{ createOperationId:()=> 'attachment-' + (++sequence), readDataUrl:async file=>'data:'+file.type+';base64,iVBORw0KGgo=', upload:async payload=>{{keys.push(payload.operation_id);if(fail){{fail=false;throw new Error('offline');}}return {{markdown:'![ok](https://forge.example/a)'}};}}, }}); controller.select({{name:'first.png',type:'image/png',size:8}}); (async()=>{{ try{{await controller.prepareComment({{repository:'stackchain/api',number:17}},'Evidence');}}catch(_error){{}} await controller.prepareComment({{repository:'stackchain/api',number:17}},'Evidence'); controller.select({{name:'replacement.png',type:'image/png',size:8}}); await controller.prepareComment({{repository:'stackchain/api',number:17}},'Replacement'); process.stdout.write(JSON.stringify(keys)); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert json.loads(run_node(script)) == [ "attachment-1", "attachment-1", "attachment-2" ] def test_selected_screenshot_serializes_as_binary_blob_without_base64_expansion(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); const calls=[]; const file=new Blob(['binary-image'],{{type:'image/webp'}}); file.name='phone.webp'; const controller=attachment.create({{ readDataUrl:async()=>{{throw new Error('base64 conversion must not run');}}, upload:async()=>{{calls.push('upload');}}, }}); controller.select(file); controller.serialize().then(async value=>process.stdout.write(JSON.stringify({{ filename:value.filename,contentType:value.contentType, isBlob:value.blob instanceof Blob,size:value.blob.size,text:await value.blob.text(),calls }}))); """ output = json.loads(run_node(script)) assert output == { "filename": "phone.webp", "contentType": "image/webp", "isBlob": True, "size": 12, "text": "binary-image", "calls": [], } def test_oversized_screenshot_is_optimized_before_preview_and_durable_serialization(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); const original = new Blob([new Uint8Array(attachment.MAX_BYTES + 200)], {{type:'image/png'}}); original.name = 'phone.png'; const calls = []; const controller = attachment.create({{ optimizeImage: async file => {{ calls.push({{name:file.name,type:file.type,size:file.size}}); const optimized = new Blob(['optimized-png'], {{type:'image/png'}}); optimized.name = file.name; return optimized; }}, upload: async () => {{ throw new Error('must not upload during selection'); }}, }}); (async()=>{{ await controller.select(original); const serialized = await controller.serialize(); process.stdout.write(JSON.stringify({{ calls,state:controller.state(),name:serialized.filename, contentType:serialized.contentType,size:serialized.blob.size, text:await serialized.blob.text(),sameBlob:serialized.blob===original, }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = json.loads(run_node(script)) assert output == { "calls": [{ "name": "phone.png", "type": "image/png", "size": 2 * 1024 * 1024 + 200, }], "state": {"name": "phone.png", "size": 13, "uploaded": False}, "name": "phone.png", "contentType": "image/png", "size": 13, "text": "optimized-png", "sameBlob": False, } def test_browser_optimizer_downscales_until_encoded_image_fits_limit(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); const original = new Blob([new Uint8Array(attachment.MAX_BYTES + 1)], {{type:'image/jpeg'}}); original.name = 'camera.jpg'; const attempts = []; let closed = false; const canvas = {{ width: 0, height: 0, getContext: () => ({{drawImage: () => {{}}}}), toBlob: callback => {{ attempts.push([canvas.width, canvas.height]); const size = attempts.length === 1 ? attachment.MAX_BYTES + 50 : attachment.MAX_BYTES - 50; callback(new Blob([new Uint8Array(size)], {{type:'image/jpeg'}})); }}, }}; (async()=>{{ const result = await attachment.optimizeImage(original, {{ createImageBitmap: async () => ({{width:2000,height:1000,close:()=>{{closed=true;}}}}), createCanvas: () => canvas, }}); process.stdout.write(JSON.stringify({{ attempts,closed,size:result.size,type:result.type,name:result.name, }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ output = json.loads(run_node(script)) assert len(output["attempts"]) == 2 assert output["attempts"][1][0] < output["attempts"][0][0] assert output["attempts"][1][1] < output["attempts"][0][1] assert output["closed"] is True assert output["size"] == 2 * 1024 * 1024 - 50 assert output["type"] == "image/jpeg" assert output["name"] == "camera.jpg" def test_binary_screenshot_builds_multipart_body_with_original_bytes(): script = f""" const attachment=require({json.dumps(str(ATTACHMENT))}); const blob=new Blob(['original-bytes'],{{type:'image/png'}}); const form=attachment.multipart({{filename:'phone.png',contentType:'image/png',blob}}); const file=form.get('file'); (async()=>process.stdout.write(JSON.stringify({{ filename:file.name,type:file.type,size:file.size,text:await file.text() }})))(); """ assert json.loads(run_node(script)) == { "filename": "phone.png", "type": "image/png", "size": 14, "text": "original-bytes", } 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_metadata_only_attachment_cannot_render_as_an_empty_screenshot(): script = f""" const attachment=require({json.dumps(str(ATTACHMENT))}); const controller=attachment.create({{readDataUrl:async()=>'',upload:async()=>{{}}}}); try {{ controller.restore({{filename:'saved.png',contentType:'image/png',stored:true}}); process.stdout.write(JSON.stringify({{restored:true,state:controller.state()}})); }} catch (error) {{ process.stdout.write(JSON.stringify({{restored:false,message:error.message,state:controller.state()}})); }} """ output = json.loads(run_node(script)) assert output["restored"] is False assert "saved screenshot" in output["message"].lower() assert output["state"] is None def test_issue_composer_renders_thumb_reachable_screenshot_preview(): html = INDEX.read_text() css = CSS.read_text() assert 'id="issue-attachment"' in html assert 'type="file"' in html assert 'accept="image/png,image/jpeg,image/webp"' in html assert 'for="issue-attachment"' in html assert 'id="issue-attachment-preview"' in html assert 'id="remove-issue-attachment"' in html assert '.issue-attachment-trigger' in css assert '.issue-attachment-preview' in css 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(hydrated.attachment);" in source assert "createIssueAttachmentController.clear();" in source assert ".create-issue-attachment" in css assert "overflow-x:hidden" in css def test_queued_screenshot_is_hydrated_before_the_issue_editor_opens_and_failure_is_retryable(): source = DASHBOARD.read_text() handler = re.search( r"list\.querySelectorAll\('\.draft-edit'\).*?addEventListener\('click', async \(\) => \{(?P.*?)\n \}\);", source, re.DOTALL, ) assert handler is not None body = handler.group("body") assert "await issueOutbox.hydrateForEdit(queued.id)" in body assert body.index("await issueOutbox.hydrateForEdit(queued.id)") < body.index( "openCreateIssueSheet()" ) assert "catch (error)" in body assert "saved screenshot" in body.lower() def test_attachment_view_keeps_invalid_draft_and_removes_preview(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); class Element {{ constructor() {{ this.listeners={{}}; this.hidden=true; this.value=''; this.files=[]; this.textContent=''; this.src=''; }} addEventListener(type, fn) {{ this.listeners[type]=fn; }} dispatch(type) {{ return this.listeners[type]({{target:this}}); }} }} const input=new Element(), preview=new Element(), image=new Element(), meta=new Element(), remove=new Element(), status=new Element(); const revoked=[]; const controller=attachment.mount({{ input,preview,image,meta,remove,status, createObjectURL:()=> 'blob:preview', revokeObjectURL:url=>revoked.push(url), readDataUrl:async()=>'', upload:async()=>{{}}, }}); input.files=[{{name:'payload.svg',type:'image/svg+xml',size:20}}]; input.dispatch('change'); const invalid={{message:status.textContent,hidden:preview.hidden}}; input.files=[{{name:'screen.png',type:'image/png',size:2048}}]; input.dispatch('change'); const selected={{src:image.src,meta:meta.textContent,hidden:preview.hidden,state:controller.state()}}; remove.dispatch('click'); process.stdout.write(JSON.stringify({{invalid,selected,removed:{{hidden:preview.hidden,src:image.src,revoked,state:controller.state()}}}})); """ output = json.loads(run_node(script)) assert output["invalid"] == { "message": "Choose a PNG, JPEG, or WebP screenshot.", "hidden": True } assert output["selected"]["src"] == "blob:preview" assert output["selected"]["hidden"] is False assert "screen.png" in output["selected"]["meta"] assert output["removed"] == { "hidden": True, "src": "", "revoked": ["blob:preview"], "state": None } def test_oversized_attachment_view_shows_progress_then_previews_optimized_blob(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); class Element {{ constructor() {{ this.listeners={{}};this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false; }} addEventListener(type,fn) {{ this.listeners[type]=fn; }} dispatch(type) {{ return this.listeners[type]({{target:this}}); }} }} const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element(); const original=new Blob([new Uint8Array(attachment.MAX_BYTES+10)],{{type:'image/png'}}); original.name='large.png'; const optimized=new Blob(['small'],{{type:'image/png'}}); optimized.name='large.png'; let finish; const gate=new Promise(resolve=>{{finish=resolve;}}); const previewed=[]; attachment.mount({{ input,preview,image,meta,remove,status, optimizeImage:async()=>{{await gate;return optimized;}}, createObjectURL:blob=>{{previewed.push(blob===optimized);return 'blob:optimized';}}, revokeObjectURL:()=>{{}},upload:async()=>{{}}, }}); input.files=[original]; const pending=input.dispatch('change'); const during={{message:status.textContent,disabled:input.disabled,hidden:preview.hidden}}; finish(); Promise.resolve(pending).then(()=>process.stdout.write(JSON.stringify({{ during,after:{{message:status.textContent,disabled:input.disabled,hidden:preview.hidden, src:image.src,meta:meta.textContent}},previewed }}))).catch(error=>{{console.error(error);process.exit(1);}}); """ output = json.loads(run_node(script)) assert output["during"] == { "message": "Optimizing screenshot…", "disabled": True, "hidden": True } assert output["after"]["message"] == "Screenshot optimized and ready to upload." assert output["after"]["disabled"] is False assert output["after"]["hidden"] is False assert output["after"]["src"] == "blob:optimized" assert "large.png" in output["after"]["meta"] assert output["previewed"] == [True] def test_removing_screenshot_during_optimization_cancels_stale_selection(): script = f""" const attachment = require({json.dumps(str(ATTACHMENT))}); class Element {{ constructor() {{ this.listeners={{}};this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false; }} addEventListener(type,fn) {{ this.listeners[type]=fn; }} dispatch(type) {{ return this.listeners[type]({{target:this}}); }} }} const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element(); const original=new Blob([new Uint8Array(attachment.MAX_BYTES+10)],{{type:'image/webp'}}); original.name='large.webp'; const optimized=new Blob(['small'],{{type:'image/webp'}}); optimized.name='large.webp'; let finish; const gate=new Promise(resolve=>{{finish=resolve;}}); const controller=attachment.mount({{ input,preview,image,meta,remove,status, optimizeImage:async()=>{{await gate;return optimized;}}, createObjectURL:()=> 'blob:stale',revokeObjectURL:()=>{{}},upload:async()=>{{}}, }}); input.files=[original]; const pending=input.dispatch('change'); remove.dispatch('click'); finish(); Promise.resolve(pending).then(async()=>process.stdout.write(JSON.stringify({{ state:controller.state(),serialized:await controller.serialize(),disabled:input.disabled, hidden:preview.hidden,src:image.src,status:status.textContent, }}))).catch(error=>{{console.error(error);process.exit(1);}}); """ output = json.loads(run_node(script)) assert output == { "state": None, "serialized": None, "disabled": False, "hidden": True, "src": "", "status": "Screenshot removed. Your comment is unchanged.", } def test_pending_comment_admission_locks_attachment_replacement_and_remove_controls(): script = f""" const attachment=require({json.dumps(str(ATTACHMENT))}); class Element {{ constructor() {{ this.listeners={{}};this.hidden=true;this.value='';this.files=[];this.textContent='';this.src='';this.disabled=false; }} addEventListener(type,fn) {{ this.listeners[type]=fn; }} }} const input=new Element(),preview=new Element(),image=new Element(),meta=new Element(),remove=new Element(),status=new Element(); const controller=attachment.mount({{input,preview,image,meta,remove,status, createObjectURL:()=> 'blob:preview',revokeObjectURL:()=>{{}},upload:async()=>{{}}}}); controller.setBusy(true); const pending={{input:input.disabled,remove:remove.disabled}}; controller.setBusy(false); process.stdout.write(JSON.stringify({{pending,released:{{input:input.disabled,remove:remove.disabled}}}})); """ assert json.loads(run_node(script)) == { "pending": {"input": True, "remove": True}, "released": {"input": False, "remove": False}, } def test_issue_comment_actions_upload_binary_multipart_before_posting_and_clear_after_acceptance(): source = DASHBOARD.read_text() assert "issueAttachment.mount({" in source assert "'/attachments'" in source assert "issueAttachmentController.prepareComment(item, body)" in source assert "issueAttachmentController.prepareComment(selectedIssue, body)" in source assert source.count("issueAttachmentController.clear();") >= 2 assert "'Idempotency-Key': payload.operation_id" in source assert "body: issueAttachment.multipart(payload)" in source def test_closing_issue_sheet_cannot_carry_a_screenshot_to_another_issue(): source = DASHBOARD.read_text() close_body = re.search( r"function closeIssueSheet\(navigate = true\) \{(?P.*?)\n \}", source, re.DOTALL, ).group("body") assert "issueAttachmentController.clear();" in close_body def test_attachment_runtime_is_available_in_the_offline_app_shell(): assert "static/issue-attachment.js" in SERVICE_WORKER.read_text() def test_readme_documents_mobile_screenshot_limits_and_delivery_order(): readme = README.read_text() assert "PNG, JPEG, or WebP" in readme assert "2 MB" in readme assert "automatically optimizes oversized screenshots" 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 def test_issue_screenshot_comments_queue_serialized_bytes_before_clearing_or_advancing(): source = DASHBOARD.read_text() assert "async function queueIssueScreenshotComment" in source assert "attachment: await issueAttachmentController.serialize()" in source assert "await authoredOutbox.enqueueDurably(message)" in source assert "await issueCommentNext.admit(item, message)" in source assert "navigator.onLine === false" in source def test_assigned_pull_composer_offers_screenshot_preview_remove_and_reuses_optimizer(): html = INDEX.read_text() source = DASHBOARD.read_text() assert 'id="pull-attachment"' in html assert 'accept="image/png,image/jpeg,image/webp"' in html assert 'for="pull-attachment"' in html assert 'id="pull-attachment-preview"' in html assert 'id="pull-attachment-image"' in html assert 'id="remove-pull-attachment"' in html assert "const pullAttachmentController = issueAttachment.mount({" in source assert "pullAttachmentController.clear();" in source def test_retrying_same_pull_load_preserves_screenshot_but_target_change_clears_it(): source = DASHBOARD.read_text() open_body = source.split("async function openPullSheet(item, trigger, offlineDetail = null) {", 1)[1].split( "\n function closePullSheet", 1 )[0] assert "if (!sameWorkTarget(selectedPull, item)) pullAttachmentController.clear();" in open_body def test_pull_screenshot_comment_uploads_or_durably_admits_before_clearing_draft(): source = DASHBOARD.read_text() assert "async function queuePullScreenshotComment" in source assert "attachment: await pullAttachmentController.serialize()" in source assert "await authoredOutbox.enqueueDurably(message)" in source assert "navigator.onLine === false" in source assert "Your comment and screenshot are safe; retry." in source def test_pull_screenshot_foreground_and_replay_share_the_durable_operation_pipeline(): source = DASHBOARD.read_text() standard = source.split( "qs('#send-pull-comment').addEventListener('click', async () => {", 1 )[1].split("\n });", 1)[0] comment_next = source.split("async function submitCommentAndNext(kind) {", 1)[1].split( "\n }\n qs('#send-issue-comment-next')", 1 )[0] assert "await queuePullScreenshotComment(item, body, operationId, false, true)" in standard assert "pullAttachmentController.prepareComment" not in standard assert "attachmentController.state() && (kind === 'pull' || navigator.onLine === false)" in comment_next