import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard UNFILED = Path(__file__).parents[1] / "frontend" / "unfiled-captures.js" def run_node(script: str): result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) return json.loads(result.stdout) def test_unfiled_captures_keep_twenty_newest_account_bound_notes(): script = f""" const createUnfiledCaptures = require({json.dumps(str(UNFILED))}); const values = new Map(); const storage = {{ getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value), removeItem:key => values.delete(key), }}; let sequence = 0; const captures = createUnfiledCaptures({{ storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'', createId:()=>String(++sequence), now:()=>1000 + sequence, }}); for (let index = 1; index <= 22; index += 1) {{ captures.save({{title:'Note ' + index, body:'Context ' + index}}); }} const offline = captures.list(); const restored = createUnfiledCaptures({{ storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy' }}).list(); process.stdout.write(JSON.stringify({{offline, restored}})); """ output = run_node(script) assert len(output["offline"]) == 20 assert output["offline"][0]["title"] == "Note 22" assert output["offline"][-1]["title"] == "Note 3" assert all(item["quarantined"] for item in output["offline"]) assert all(not item["quarantined"] for item in output["restored"]) assert output["restored"][0]["ownerLogin"] == "timmy" def test_unfiled_capture_resume_requires_matching_confirmed_account_and_removes_only_selected_note(): script = f""" const createUnfiledCaptures = require({json.dumps(str(UNFILED))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; let id = 0; const captures = createUnfiledCaptures({{ storage, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy', createId:()=>String(++id), now:()=>id }}); const first = captures.save({{title:'First',body:'One'}}); const second = captures.save({{title:'Second',body:'Two'}}); let mismatch = ''; try {{ captures.resume(first.id, 'alexander'); }} catch (error) {{ mismatch = error.message; }} const resumed = captures.resume(first.id, 'timmy'); captures.completeResume(first.id); process.stdout.write(JSON.stringify({{mismatch,resumed,remaining:captures.list(),second}})); """ output = run_node(script) assert output["mismatch"] == "Reconnect with the account that saved this capture." assert output["resumed"] == {"repository": "", "title": "First", "body": "One", "labelIds": []} assert [item["id"] for item in output["remaining"]] == [output["second"]["id"]] def test_unfiled_capture_durably_restores_screenshot_before_explicit_handoff_completion(): script = f""" const createUnfiledCaptures = require({json.dumps(str(UNFILED))}); const values = new Map(); const blobs = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const attachmentStore = {{ put: async (id, attachment) => blobs.set(id, attachment), get: async id => blobs.get(id) || null, delete: async id => blobs.delete(id), }}; (async () => {{ const captures = createUnfiledCaptures({{ storage, attachmentStore, getCaptureLogin:()=>'timmy', getCurrentLogin:()=>'timmy', createId:()=>'capture-1', now:()=>42, }}); const screenshot = {{filename:'phone.png',contentType:'image/png',blob:new Blob(['proof'],{{type:'image/png'}})}}; const saved = await captures.save({{title:'Broken mobile layout',body:'At 320px',attachment:screenshot}}); const listed = captures.list(); const resumed = await captures.resume(saved.id, 'timmy'); const beforeComplete = captures.list().length; await captures.completeResume(saved.id); process.stdout.write(JSON.stringify({{ listed, beforeComplete, afterComplete:captures.list().length, resumed:{{title:resumed.title,body:resumed.body,filename:resumed.attachment.filename, contentType:resumed.attachment.contentType,size:resumed.attachment.blob.size}}, blobRemoved:!blobs.has(saved.id), }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ output = run_node(script) assert output["listed"][0]["hasAttachment"] is True assert "attachment" not in output["listed"][0] assert output["beforeComplete"] == 1 assert output["afterComplete"] == 0 assert output["blobRemoved"] is True assert output["resumed"] == { "title": "Broken mobile layout", "body": "At 320px", "filename": "phone.png", "contentType": "image/png", "size": 5, } def test_unfiled_capture_rejects_empty_or_identityless_records_without_writing(): script = f""" const createUnfiledCaptures = require({json.dumps(str(UNFILED))}); const values = new Map(); const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}}; const captures = createUnfiledCaptures({{storage,getCaptureLogin:()=>''}}); const errors = []; for (const note of [{{title:'',body:'context'}},{{title:'Work',body:'context'}}]) {{ try {{ captures.save(note); }} catch (error) {{ errors.push(error.message); }} }} process.stdout.write(JSON.stringify({{errors,size:values.size}})); """ output = run_node(script) assert output == { "errors": ["Add a title before saving.", "Offline identity is unavailable. Reconnect once before saving private work."], "size": 0, } @pytest.mark.anyio async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume_flow(): html = await dashboard() assert '' in html assert 'id="save-unfiled-issue"' in html assert 'Save to Drafts' in html assert "createUnfiledCaptures({" in html assert "getCaptureLogin: () => String(lastContextSnapshot?.user?.login || '').trim()" in html assert "unfiledCaptures.save(captureDraft)" in html assert "await unfiledCaptures.resume(item.capture_id, activeFlushLogin)" in html assert "await createIssueAttachmentController.serialize()" in html assert "createUnfiledAttachmentStore()" in html assert "createIssueAttachmentController.restore(resumed.attachment)" in html assert "await unfiledCaptures.completeResume(resumedUnfiledCaptureId)" in html assert "item.hasAttachment ? ' ยท Screenshot attached' : ''" in html assert "issueCapture.saveDraft(resumed)" in html assert "item.kind === 'unfiled-issue'" in html assert '.create-issue-actions button { min-height:44px;' in html assert '@media(max-width:320px)' in html @pytest.mark.anyio async def test_mobile_new_opens_capture_first_and_progressively_reveals_filing_fields(): html = await dashboard() assert 'id="create-issue-heading">Capture work' in html assert 'class="create-issue-filing" id="create-issue-filing" hidden' in html assert 'id="save-unfiled-issue" type="button">Save to Drafts' in html assert 'id="file-new-issue" type="button">File now' in html assert "function setIssueFilingMode(enabled)" in html assert "setIssueFilingMode(Boolean(captureDraft.repository))" in html assert "qs('#create-issue-title').focus();" in html assert "qs('#file-new-issue').addEventListener('click'" in html assert '.create-issue-capture-actions button { min-height:44px;' in html assert '.create-issue-capture-actions[hidden] { display:none;' in html attachment = html.index('class="create-issue-attachment"') filing = html.index('class="create-issue-filing"') assert attachment < filing