import json
import subprocess
from pathlib import Path

import pytest

from tests.dashboard_bundle import dashboard


OUTBOX = Path(__file__).parents[1] / "frontend" / "authored-outbox.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_repeated_operation_id_rejects_a_different_comment_payload():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map();
const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=> 'timmy'}});
const original={{kind:'issue-comment',repository:'o/r',number:7,body:'Done',operationId:'stable',attachments:[{{filename:'before.jpg',contentType:'image/jpeg',note:'Before',operationId:'photo-1'}}]}};
const identical=outbox.enqueue(original);
let error='';
try {{ outbox.enqueue({{...original,attachments:[{{filename:'after.jpg',contentType:'image/jpeg',note:'After',operationId:'photo-2'}}]}}); }}
catch (caught) {{ error=caught.message; }}
process.stdout.write(JSON.stringify({{identical,error,items:outbox.list()}}));
"""
    output = run_node(script)
    assert output["identical"]["operationId"] == "stable"
    assert output["error"] == "This operation ID is already bound to a different queued action."
    assert len(output["items"]) == 1
    assert output["items"][0]["attachment"]["operationId"] == "photo-1"


def test_repeated_pull_review_operation_uses_the_default_comment_decision():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map();
const storage = {{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=> 'timmy'}});
const review={{kind:'pull-review',repository:'o/r',number:8,body:'Notes',expectedHeadSha:'abc',operationId:'review-op'}};
outbox.enqueue(review);
let error='';
try {{ outbox.enqueue(review); }} catch (caught) {{ error=caught.message; }}
process.stdout.write(JSON.stringify({{error,items:outbox.list()}}));
"""
    output = run_node(script)
    assert output["error"] == ""
    assert len(output["items"]) == 1
    assert output["items"][0]["decision"] == "comment"


def test_authored_outbox_quarantines_legacy_and_cross_account_messages():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify({{version:1,items:[
  {{id:'legacy',operationId:'legacy',kind:'issue-comment',repository:'o/r',number:1,body:'Old',status:'queued'}}
]}})]]);
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const calls = [];
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=>'timmy',
  fetchJson:async url => {{calls.push(url); return {{id:1}};}},
}});
const bound = outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:2,body:'Mine',operationId:'mine'}});
(async()=>{{
  const wrong = await outbox.flush('alexander');
  const right = await outbox.flush('timmy');
  process.stdout.write(JSON.stringify({{bound,wrong,right,calls,remaining:outbox.list()}}));
}})();
"""
    output = run_node(script)

    assert output["bound"]["ownerLogin"] == "timmy"
    assert output["wrong"]["blocked"] == 2
    assert len(output["right"]["confirmed"]) == 1
    assert [item["id"] for item in output["remaining"]] == ["legacy"]
    assert len(output["calls"]) == 1


def test_authored_outbox_persists_each_message_kind_and_flushes_sequentially():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map();
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const calls = [];
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=>'timmy', now:() => 1234,
  fetchJson:async (url, options) => {{
    calls.push({{url,key:options.headers['Idempotency-Key'],body:JSON.parse(options.body)}});
    return {{id:calls.length}};
  }},
}});
outbox.enqueue({{kind:'issue-comment',repository:'stackchain/api',number:7,body:'Issue note',operationId:'issue-op'}});
outbox.enqueue({{kind:'pull-comment',repository:'stackchain/web',number:8,body:'PR note',operationId:'pull-op'}});
outbox.enqueue({{kind:'update-reply',notificationId:9,body:'Update note',operationId:'update-op'}});
const persisted = createAuthoredOutbox({{storage,fetchJson:outbox.fetchJson}}).list();
outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{persisted,calls,result,remaining:outbox.list()}})));
"""
    output = run_node(script)

    assert [item["kind"] for item in output["persisted"]] == [
        "issue-comment", "pull-comment", "update-reply"
    ]
    assert [item["operationId"] for item in output["persisted"]] == ["issue-op", "pull-op", "update-op"]
    assert output["calls"] == [
        {"url": "api/v1/repos/stackchain/api/issues/7/comments", "key": "issue-op", "body": {"body": "Issue note"}},
        {"url": "api/v1/repos/stackchain/web/pulls/8/comments", "key": "pull-op", "body": {"body": "PR note"}},
        {"url": "api/v1/notifications/9/reply", "key": "update-op", "body": {"body": "Update note"}},
    ]
    assert len(output["result"]["confirmed"]) == 3
    assert output["remaining"] == []


def test_durable_conversation_photo_captions_stay_paired_without_local_binary_storage():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const mirrors = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  backgroundSync:{{reconcile:async items=>mirrors.push(items),requestSync:async()=>{{}}}},
}});
const photo=(filename,note)=>({{filename,contentType:'image/png',blob:new Blob([filename]),note}});
(async()=>{{
  await outbox.enqueueDurably({{
    kind:'issue-comment',repository:'o/r',number:9,body:'Before and after',operationId:'bundle-note',
    attachments:[photo('before.png','Before repair'),photo('after.png','After *repair*')],
  }});
  const metadata=JSON.parse(values.get('stackchain.authored-outbox.v1')).items[0].attachments;
  const durable=mirrors[0][0].attachments;
  process.stdout.write(JSON.stringify({{
    metadata,durable:durable.map(x=>({{filename:x.filename,note:x.note,text:x.data||null,hasBlob:x.blob instanceof Blob}})),
  }}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = run_node(script)

    assert output["metadata"] == [
        {"filename": "before.png", "contentType": "image/png", "stored": True, "note": "Before repair"},
        {"filename": "after.png", "contentType": "image/png", "stored": True, "note": "After *repair*"},
    ]
    assert output["durable"] == [
        {"filename": "before.png", "note": "Before repair", "text": None, "hasBlob": True},
        {"filename": "after.png", "note": "After *repair*", "text": None, "hasBlob": True},
    ]


def test_search_preview_reply_is_durably_bound_to_exact_kind_with_upload_checkpoints():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const mirrors = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  backgroundSync:{{reconcile:async items=>mirrors.push(items),requestSync:async()=>{{}}}},
}});
(async()=>{{
  const result=await outbox.enqueueDurably({{
    kind:'search-reply',targetKind:'pull',repository:'stackchain/web',number:42,
    body:'Ready offline.',operationId:'search-42',attachments:[{{
      filename:'proof.jpg',contentType:'image/jpeg',blob:new Blob(['proof']),note:'After repair',
      operationId:'photo-stage-1',confirmed:{{markdown:'![proof](saved)'}},
    }}],
  }});
  const metadata=outbox.list()[0]; const durable=mirrors[0][0];
  process.stdout.write(JSON.stringify({{
    item:result.item,metadata,
    durable:{{targetKind:durable.targetKind,operationId:durable.attachment.operationId,
      markdown:durable.attachment.confirmed?.markdown,hasBlob:durable.attachment.blob instanceof Blob}},
  }}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
    output = run_node(script)

    assert output["item"]["kind"] == "search-reply"
    assert output["item"]["targetKind"] == "pull"
    assert output["metadata"]["attachment"] == {
        "filename": "proof.jpg", "contentType": "image/jpeg", "stored": True,
        "note": "After repair", "operationId": "photo-stage-1",
        "confirmed": {"markdown": "![proof](saved)"},
    }
    assert output["durable"] == {
        "targetKind": "pull", "operationId": "photo-stage-1",
        "markdown": "![proof](saved)", "hasBlob": True,
    }


def test_authored_outbox_persists_and_delivers_revision_checked_issue_content():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const calls = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  fetchJson: async (url, options) => {{
    calls.push({{url,method:options.method,key:options.headers['Idempotency-Key'],body:JSON.parse(options.body)}});
    return {{repository:'stackchain/dashboard',number:17,title:'Ship',body:'- [x] Test',updated_at:'2026-08-15T11:00:00Z'}};
  }},
}});
const queued = outbox.enqueue({{
  kind:'issue-content',repository:'stackchain/dashboard',number:17,operationId:'check-op',
  title:'Ship',body:'- [x] Test',expectedUpdatedAt:'2026-08-15T10:00:00Z',
}});
const restored = createAuthoredOutbox({{storage}}).list()[0];
outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{queued,restored,calls,result,remaining:outbox.list()}})));
"""
    output = run_node(script)

    assert output["queued"]["expectedUpdatedAt"] == "2026-08-15T10:00:00Z"
    assert output["restored"]["title"] == "Ship"
    assert output["calls"] == [{
        "url": "api/v1/repos/stackchain/dashboard/issues/17/content",
        "method": "PATCH",
        "key": "check-op",
        "body": {
            "title": "Ship", "body": "- [x] Test",
            "expected_updated_at": "2026-08-15T10:00:00Z",
        },
    }]
    assert output["result"]["confirmed"][0]["body"] == "- [x] Test"
    assert output["remaining"] == []


def test_durable_issue_content_admission_coalesces_latest_body_on_original_revision():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const mirrors = []; const ids = ['check-1', 'check-2'];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy', createOperationId:()=>ids.shift(),
  backgroundSync:{{
    reconcile:async items=>mirrors.push(items.map(item=>({{...item}}))),
    requestSync:async()=>{{}},
  }},
}});
(async()=>{{
  await outbox.enqueueDurably({{kind:'issue-content',repository:'stackchain/dashboard',number:17,
    title:'Ship',body:'- [x] Build\\n- [ ] Test',expectedUpdatedAt:'server-revision'}});
  const second = await outbox.enqueueDurably({{kind:'issue-content',repository:'stackchain/dashboard',number:17,
    title:'Ship',body:'- [x] Build\\n- [x] Test',expectedUpdatedAt:'pending-local-revision'}});
  process.stdout.write(JSON.stringify({{items:outbox.list(),second,mirrors}}));
}})();
"""
    output = run_node(script)

    assert len(output["items"]) == 1
    assert output["items"][0]["id"] == "check-1"
    assert output["items"][0]["operationId"] == "check-2"
    assert output["items"][0]["body"] == "- [x] Build\n- [x] Test"
    assert output["items"][0]["expectedUpdatedAt"] == "server-revision"
    assert output["second"]["item"] == output["items"][0]
    assert len(output["mirrors"][1]) == 1
    assert output["mirrors"][1][0]["operationId"] == "check-2"


def test_durable_issue_content_coalesces_ordered_checklist_management_intent():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const ids = ['manage-1','manage-2'];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage,getOwnerLogin:()=> 'timmy',createOperationId:()=>ids.shift(),
  backgroundSync:{{reconcile:async()=>{{}},requestSync:async()=>{{}}}},
}});
(async()=>{{
  await outbox.enqueueDurably({{kind:'issue-content',repository:'o/r',number:9,title:'Ship',
    baseBody:'- [ ] Build\\n- [ ] Test',body:'- [ ] Compile\\n- [ ] Test',expectedUpdatedAt:'old',
    checklistOperation:{{action:'rename',index:0,label:'Compile'}}}});
  await outbox.enqueueDurably({{kind:'issue-content',repository:'o/r',number:9,title:'Ship',
    baseBody:'- [ ] Compile\\n- [ ] Test',body:'- [ ] Test\\n- [ ] Compile',expectedUpdatedAt:'old',
    checklistOperation:{{action:'move-later',index:0}}}});
  process.stdout.write(JSON.stringify(outbox.list()[0]));
}})();
"""
    output = run_node(script)

    assert output["baseBody"] == "- [ ] Build\n- [ ] Test"
    assert output["body"] == "- [ ] Test\n- [ ] Compile"
    assert output["checklistOperations"] == [
        {"action": "rename", "index": 0, "label": "Compile"},
        {"action": "move-later", "index": 0},
    ]


def test_failed_durable_issue_content_replacement_restores_previous_intent():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const ids = ['check-1', 'check-2']; let admissions = 0;
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy', createOperationId:()=>ids.shift(),
  backgroundSync:{{
    reconcile:async()=>{{if (++admissions === 2) throw new Error('IndexedDB unavailable');}},
    requestSync:async()=>{{}},
  }},
}});
(async()=>{{
  await outbox.enqueueDurably({{kind:'issue-content',repository:'o/r',number:9,
    title:'Ship',body:'- [x] Build\\n- [ ] Test',expectedUpdatedAt:'server-revision'}});
  let error = '';
  try {{
    await outbox.enqueueDurably({{kind:'issue-content',repository:'o/r',number:9,
      title:'Ship',body:'- [x] Build\\n- [x] Test',expectedUpdatedAt:'server-revision'}});
  }} catch (caught) {{ error = caught.message; }}
  process.stdout.write(JSON.stringify({{error,items:outbox.list()}}));
}})();
"""
    output = run_node(script)

    assert output["error"] == "IndexedDB unavailable"
    assert len(output["items"]) == 1
    assert output["items"][0]["operationId"] == "check-1"
    assert output["items"][0]["body"] == "- [x] Build\n- [ ] Test"


def test_issue_content_conflict_resolution_rebases_task_delta_and_mirrors_before_retry():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const mergeChecklistConflict = require({json.dumps(str(Path(__file__).parents[1] / 'frontend' / 'checklist-conflict.js'))});
const initial = {{version:2,items:[{{
  id:'check-1',operationId:'old-op',kind:'issue-content',ownerLogin:'timmy',status:'attention',
  repository:'o/r',number:9,title:'Ship',baseBody:'Intro\\n- [ ] Build\\n- [ ] Test',
  body:'Intro\\n- [x] Build\\n- [ ] Test',expectedUpdatedAt:'old-revision',error:'Issue changed',
}}]}};
const values = new Map([['stackchain.authored-outbox.v1', JSON.stringify(initial)]]);
const mirrors = []; let syncs = 0;
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'rebased-op', mergeChecklistConflict,
  backgroundSync:{{reconcile:async items=>mirrors.push(items.map(item=>({{...item}}))),requestSync:async()=>{{syncs += 1;}}}},
}});
(async()=>{{
  const result = await outbox.resolveIssueContentConflict('check-1', {{
    title:'Ship safely', body:'Remote intro\\n- [ ] Test\\n- [ ] Build', updated_at:'latest-revision',
  }}, 'timmy');
  process.stdout.write(JSON.stringify({{result,item:outbox.list()[0],mirrors,syncs}}));
}})();
"""
    output = run_node(script)

    assert output["result"]["changes"] == [{"label": "Build", "checked": True}]
    assert output["item"]["status"] == "queued"
    assert output["item"]["operationId"] == "rebased-op"
    assert output["item"]["title"] == "Ship safely"
    assert output["item"]["body"] == "Remote intro\n- [ ] Test\n- [x] Build"
    assert output["item"]["baseBody"] == "Remote intro\n- [ ] Test\n- [ ] Build"
    assert output["item"]["expectedUpdatedAt"] == "latest-revision"
    assert "error" not in output["item"]
    assert output["mirrors"][-1][0] == output["item"]
    assert output["syncs"] == 1


def test_authored_outbox_persists_and_delivers_desired_blocker_state():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const calls = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  fetchJson: async (url, options) => {{
    calls.push({{url,method:options.method,key:options.headers['Idempotency-Key'],body:JSON.parse(options.body)}});
    return {{repository:'stackchain/dashboard',number:17,dependencies_available:true,dependencies:[]}};
  }},
}});
const queued = outbox.enqueue({{
  kind:'issue-blocker',repository:'stackchain/dashboard',number:17,operationId:'blocker-op',
  blockerRepository:'stackchain/api',blockerNumber:9,present:false,
}});
const restored = createAuthoredOutbox({{storage}}).list()[0];
outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{queued,restored,calls,result,remaining:outbox.list()}})));
"""
    output = run_node(script)

    assert output["queued"]["blockerRepository"] == "stackchain/api"
    assert output["queued"]["blockerNumber"] == 9
    assert output["queued"]["present"] is False
    assert output["restored"]["present"] is False
    assert output["calls"] == [{
        "url": "api/v1/repos/stackchain/dashboard/issues/17/blockers",
        "method": "PATCH",
        "key": "blocker-op",
        "body": {"repository": "stackchain/api", "number": 9, "present": False},
    }]
    assert output["result"]["confirmed"][0]["dependencies"] == []
    assert output["remaining"] == []


def test_authored_outbox_exposes_sending_then_preserves_transient_attempt_details():
    script = f"""
const createAuthoredOutbox = 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 release; const gate = new Promise(resolve => release = resolve);
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=>'timmy', now:()=>777,
  fetchJson:async()=>{{await gate; const error=new Error('Network unavailable');error.status=503;throw error;}},
}});
outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Wait',operationId:'attempt'}});
const pending=outbox.flush('timmy');
const sending=outbox.list()[0];
release();
pending.then(()=>process.stdout.write(JSON.stringify({{sending,after:outbox.list()[0]}})));
"""
    output = run_node(script)

    assert output["sending"]["status"] == "sending"
    assert output["after"]["status"] == "queued"
    assert output["after"]["lastAttemptAt"] == 777
    assert output["after"]["lastAttemptError"] == "Network unavailable"


def test_authored_outbox_persists_and_delivers_complete_pull_review_payload():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const calls = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  fetchJson: async (url, options) => {{ calls.push({{url, options}}); return {{id:44}}; }},
}});
let queued;
try {{
  queued = outbox.enqueue({{
    kind:'pull-review', repository:'stackchain/web', number:8, operationId:'review-op',
    body:'Looks good', decision:'approve', expectedHeadSha:'abc123',
    comments:[{{path:'app.js',body:'Nice',new_position:4}}],
    draftKey:'stackchain.review-draft.v1:stackchain/web#8@abc123',
    progressKey:'stackchain.review-progress.v1:stackchain/web#8@abc123',
    draftFingerprint:'draft', progressFingerprint:'progress',
  }});
}} catch (error) {{
  process.stdout.write(JSON.stringify({{error:error.message}}));
}}
if (queued) {{
  storage.setItem(queued.draftKey, 'draft'); storage.setItem(queued.progressKey, 'progress');
  outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{
    queued, calls:calls.map(call=>({{url:call.url,key:call.options.headers['Idempotency-Key'],body:JSON.parse(call.options.body)}})),
    result, remaining:outbox.list(), draft:storage.getItem(queued.draftKey), progress:storage.getItem(queued.progressKey),
  }})));
}}
"""
    output = run_node(script)

    assert "error" not in output, output.get("error")

    assert output["queued"]["kind"] == "pull-review"
    assert output["queued"]["comments"] == [{"path": "app.js", "body": "Nice", "new_position": 4}]
    assert output["calls"] == [{
        "url": "api/v1/repos/stackchain/web/pulls/8/review",
        "key": "review-op",
        "body": {
            "body": "Looks good", "decision": "approve", "expected_head_sha": "abc123",
            "comments": [{"path": "app.js", "body": "Nice", "new_position": 4}],
        },
    }]
    assert output["remaining"] == []
    assert output["draft"] is None
    assert output["progress"] is None


def test_authored_outbox_requires_explicit_retry_to_authorize_issue_closure():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const calls = [];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  fetchJson: async (url, options) => {{ calls.push({{url,options}}); return {{number:27,state:'closed'}}; }},
}});
const queued = outbox.enqueue({{
  kind:'issue-close',repository:'stackchain/dashboard',number:27,operationId:'close-op'
}});
(async()=>{{
  const automatic = await outbox.flush('timmy');
  const awaitingAuthorization = outbox.list();
  const explicit = await outbox.retry(queued.id, 'timmy');
  process.stdout.write(JSON.stringify({{
    queued,automatic,awaitingAuthorization,explicit,calls,remaining:outbox.list()
  }}));
}})();
"""
    output = run_node(script)

    assert output["queued"]["kind"] == "issue-close"
    assert output["automatic"]["confirmed"] == []
    assert output["awaitingAuthorization"][0]["status"] == "queued"
    assert output["calls"] == [{
        "url": "api/v1/repos/stackchain/dashboard/issues/27/close",
        "options": {
            "method": "PATCH",
            "headers": {"Accept": "application/json", "Idempotency-Key": "close-op"},
        },
    }]
    assert output["explicit"]["confirmed"] == [{"number": 27, "state": "closed"}]
    assert output["remaining"] == []


def test_queued_closure_stays_durable_through_background_skip_then_closes_from_explicit_retry():
    sync = Path(__file__).parents[1] / "frontend" / "background-issue-sync.js"
    script = f"""
const createAuthoredOutbox=require({json.dumps(str(OUTBOX))});
const createBackgroundIssueSync=require({json.dumps(str(sync))});
const values=new Map();const records=new Map();let tail=Promise.resolve();const calls=[];
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const transaction=work=>{{const run=tail.then(()=>work({{
  get:async id=>records.get(id),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;}};
const fetchJson=async(url,options={{}})=>{{
  calls.push({{url,key:options.headers?.['Idempotency-Key'] || ''}});
  return url==='api/v1/background-identity'?{{login:'timmy'}}:{{number:27,state:'closed'}};
}};
(async()=>{{
  const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
  const background=createBackgroundIssueSync({{store,fetchJson}});background.requestSync=async()=>{{}};
  const outbox=createAuthoredOutbox({{storage,fetchJson,getOwnerLogin:()=>'timmy',backgroundSync:background}});
  const admitted=await outbox.enqueueDurably({{kind:'issue-close',repository:'stackchain/dashboard',number:27,operationId:'close-op'}});
  const automatic=await background.flush();
  outbox.reconcileBackground(await background.snapshot());
  const foregroundFlush=await outbox.flush('timmy');
  const explicit=await outbox.retry(admitted.item.id,'timmy');
  process.stdout.write(JSON.stringify({{automatic,foregroundFlush,explicit,calls,remaining:outbox.list(),snapshot:await background.snapshot()}}));
}})();
"""
    output = run_node(script)

    assert output["automatic"]["confirmed"] == []
    assert output["foregroundFlush"]["confirmed"] == []
    assert output["calls"] == [
        {"url": "api/v1/background-identity", "key": ""},
        {"url": "api/v1/repos/stackchain/dashboard/issues/27/close", "key": "close-op"},
    ]
    assert output["explicit"]["confirmed"] == [{"number": 27, "state": "closed"}]
    assert output["remaining"] == []
    assert output["snapshot"][0]["status"] == "sent"


def test_authored_outbox_classifies_failures_and_continues_past_attention_items():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
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 phase = 'permanent';
const calls = [];
const outbox = createAuthoredOutbox({{
  storage,
  getOwnerLogin:()=>'timmy',
  fetchJson:async (_url, options) => {{
    const body = JSON.parse(options.body).body; calls.push(body);
    if (body === 'Bad' && phase === 'permanent') {{ const e = new Error('Reply rejected'); e.status=422; throw e; }}
    if (body === 'Later' && phase === 'transient') {{ const e = new Error('Offline'); e.status=503; throw e; }}
    return {{id:calls.length}};
  }},
}});
const bad=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Bad',operationId:'a'}});
outbox.enqueue({{kind:'pull-comment',repository:'o/r',number:2,body:'Good',operationId:'b'}});
outbox.flush('timmy').then(async first => {{
  phase='transient';
  outbox.enqueue({{kind:'update-reply',notificationId:3,body:'Later',operationId:'c'}});
  const second=await outbox.flush('timmy');
  process.stdout.write(JSON.stringify({{first,second,calls,remaining:outbox.list(),badId:bad.id}}));
}});
"""
    output = run_node(script)

    assert output["calls"] == ["Bad", "Good", "Later"]
    assert len(output["first"]["confirmed"]) == 1
    assert output["remaining"][0]["id"] == output["badId"]
    assert output["remaining"][0]["status"] == "attention"
    assert output["remaining"][0]["error"] == "Reply rejected"
    assert output["remaining"][1]["status"] == "queued"


def test_authored_outbox_requires_deliberate_rekey_after_uncertain_delivery():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); let sequence=0; const calls=[];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=>'timmy', createOperationId:()=> 'verified-' + (++sequence),
  fetchJson:async (_url, options) => {{
    const key=options.headers['Idempotency-Key']; calls.push(key);
    if (key === 'uncertain-key') {{ const e=new Error('Verify it was not posted before retrying.'); e.status=422; e.code='delivery_uncertain'; throw e; }}
    return {{id:calls.length}};
  }},
}});
const uncertain=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Check',operationId:'uncertain-key'}});
outbox.enqueue({{kind:'pull-comment',repository:'o/r',number:2,body:'Continue',operationId:'other-key'}});
(async()=>{{
  const first=await outbox.flush('timmy');
  const attention=outbox.list()[0];
  const retried=await outbox.retry(uncertain.id,'timmy');
  process.stdout.write(JSON.stringify({{first,attention,retried,calls,remaining:outbox.list()}}));
}})();
"""
    output = run_node(script)

    assert output["calls"] == ["uncertain-key", "other-key", "verified-1"]
    assert len(output["first"]["confirmed"]) == 1
    assert output["attention"]["status"] == "attention"
    assert output["attention"]["deliveryState"] == "uncertain"
    assert output["remaining"] == []


def test_authored_outbox_retry_is_single_flight_and_edit_rotates_identity():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
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 release; const gate=new Promise(resolve=>release=resolve); let calls=0; let sequence=0;
const outbox=createAuthoredOutbox({{
  storage, getOwnerLogin:()=>'timmy', createOperationId:()=> 'new-' + (++sequence),
  fetchJson:async()=>{{calls++; await gate; return {{id:1}};}},
}});
const item=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Old',operationId:'old'}});
const edited=outbox.update(item.id,{{body:'New'}});
const flush=outbox.flush('timmy'); const retry=outbox.retry(item.id, 'timmy'); release();
Promise.all([flush,retry]).then(results=>process.stdout.write(JSON.stringify({{edited,calls,results,remaining:outbox.list()}})));
"""
    output = run_node(script)

    assert output["edited"]["body"] == "New"
    assert output["edited"]["operationId"] == "new-1"
    assert output["calls"] == 1
    assert output["remaining"] == []


def test_authored_outbox_deduplicates_repeated_queue_attempts_by_operation_id():
    script = f"""
const createAuthoredOutbox = 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=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy'}});
const first=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Same',operationId:'stable'}});
const second=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Same',operationId:'stable'}});
process.stdout.write(JSON.stringify({{first,second,items:outbox.list()}}));
"""
    output = run_node(script)

    assert output["first"] == output["second"]
    assert len(output["items"]) == 1


def test_authored_outbox_deduplicates_pull_review_for_same_head_after_reload():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values=new Map();let sequence=0;
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const make=()=>createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',createOperationId:()=> 'op-' + (++sequence)}});
const first=make().enqueue({{kind:'pull-review',repository:'o/r',number:7,expectedHeadSha:'abc',decision:'approve',body:'Ready'}});
const second=make().enqueue({{kind:'pull-review',repository:'o/r',number:7,expectedHeadSha:'abc',decision:'approve',body:'Ready'}});
process.stdout.write(JSON.stringify({{first,second,items:make().list()}}));
"""
    output = run_node(script)

    assert output["first"] == output["second"]
    assert len(output["items"]) == 1


def test_authored_outbox_rejects_changed_feedback_while_same_head_is_queued():
    script = f"""
const createAuthoredOutbox = 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=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy'}});
outbox.enqueue({{kind:'pull-review',repository:'o/r',number:7,expectedHeadSha:'abc',decision:'approve',body:'First'}});
let error='';try{{outbox.enqueue({{kind:'pull-review',repository:'o/r',number:7,expectedHeadSha:'abc',decision:'request_changes',body:'Changed'}});}}catch(e){{error=e.message;}}
process.stdout.write(JSON.stringify({{error,items:outbox.list()}}));
"""
    output = run_node(script)

    assert output["error"] == "A review for this saved head is already queued. Open Drafts to inspect or discard it first."
    assert len(output["items"]) == 1


def test_confirmed_review_keeps_draft_edits_made_after_it_was_queued():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values=new Map();const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const draftKey='draft-key';const progressKey='progress-key';storage.setItem(draftKey,'old-draft');storage.setItem(progressKey,'old-progress');
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',fetchJson:async()=>({{id:1}})}});
outbox.enqueue({{kind:'pull-review',repository:'o/r',number:7,expectedHeadSha:'abc',body:'Queued',draftKey,progressKey,draftFingerprint:'old-draft',progressFingerprint:'old-progress'}});
storage.setItem(draftKey,'newer-draft');storage.setItem(progressKey,'newer-progress');
outbox.flush('timmy').then(()=>process.stdout.write(JSON.stringify({{draft:storage.getItem(draftKey),progress:storage.getItem(progressKey)}})));
"""
    output = run_node(script)

    assert output == {"draft": "newer-draft", "progress": "newer-progress"}


def test_authored_outbox_mirrors_to_background_sync_and_reconciles_worker_results():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const mirrors=[]; let requested=0;
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const backgroundSync = {{
  reconcile: async (items, lane) => mirrors.push({{items:items.map(item=>({{...item}})),lane}}),
  requestSync: async () => {{requested += 1;}},
}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',backgroundSync}});
const sent=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Sent',operationId:'sent'}});
const failed=outbox.enqueue({{kind:'update-reply',notificationId:2,body:'Fix me',operationId:'failed'}});
setTimeout(() => {{
  const reconciled=outbox.reconcileBackground([
    {{...sent,status:'sent'}},
    {{...failed,status:'attention',error:'Reply rejected'}},
  ]);
  setTimeout(() => process.stdout.write(JSON.stringify({{mirrors,requested,reconciled}})), 0);
}}, 0);
"""
    output = run_node(script)

    assert output["mirrors"][0]["lane"] == "authored"
    assert [item["operationId"] for item in output["mirrors"][-1]["items"]] == ["failed"]
    assert output["requested"] >= 1
    assert [(item["operationId"], item["status"]) for item in output["reconciled"]] == [
        ("failed", "attention")
    ]


def test_authored_outbox_reconciles_uncertain_worker_delivery_state():
    script = f"""
const createAuthoredOutbox = 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=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy'}});
const item=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:7,body:'Maybe',operationId:'maybe'}});
const reconciled=outbox.reconcileBackground([{{...item,status:'attention',deliveryState:'uncertain',error:'Verify first'}}]);
process.stdout.write(JSON.stringify(reconciled[0]));
"""
    output = run_node(script)

    assert output["status"] == "attention"
    assert output["deliveryState"] == "uncertain"


def test_authored_outbox_waits_for_durable_background_admission():
    script = f"""
const createAuthoredOutbox = 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 release; const gate = new Promise(resolve => release = resolve); const events = [];
const outbox = createAuthoredOutbox({{
  storage, getOwnerLogin:()=> 'timmy',
  backgroundSync: {{
    reconcile: async (_items, lane) => {{ events.push('mirror:' + lane); await gate; events.push('committed'); }},
    requestSync: async () => events.push('registered'),
  }},
}});
const admission = outbox.enqueueDurably({{
  kind:'issue-comment',repository:'o/r',number:1,body:'Do not lose',operationId:'message-1'
}}).then(result => {{ events.push('confirmed'); return result; }});
Promise.resolve().then(async () => {{
  const pending = events.slice(); release(); const result = await admission;
  process.stdout.write(JSON.stringify({{pending,events,result,items:outbox.list()}}));
}});
"""
    output = run_node(script)

    assert output["pending"] == ["mirror:authored"]
    assert output["events"] == ["mirror:authored", "committed", "registered", "confirmed"]
    assert output["result"]["durability"] == "background"
    assert output["items"][0]["operationId"] == "message-1"


def test_authored_outbox_foreground_send_uses_atomic_background_delivery():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); let backgroundCalls=0; let directCalls=0;
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const backgroundSync = {{
  reconcile: async()=>{{}}, requestSync:async()=>{{}},
  send: async (item, owner) => {{backgroundCalls += 1; return {{message:{{id:3,owner}}}};}},
}};
const outbox=createAuthoredOutbox({{
  storage,getOwnerLogin:()=>'timmy',backgroundSync,
  fetchJson:async()=>{{directCalls += 1; return {{id:4}};}},
}});
outbox.enqueue({{kind:'pull-comment',repository:'o/r',number:2,body:'Review',operationId:'once'}});
(async()=>{{
  const result=await outbox.flush('timmy');
  process.stdout.write(JSON.stringify({{backgroundCalls,directCalls,result,remaining:outbox.list()}}));
}})();
"""
    output = run_node(script)

    assert output["backgroundCalls"] == 1
    assert output["directCalls"] == 0
    assert output["result"]["confirmed"] == [{"id": 3, "owner": "timmy"}]
    assert output["remaining"] == []


def test_authorization_hold_can_resume_in_foreground_with_original_review_identity():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values=new Map();const sent=[];
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox=createAuthoredOutbox({{
  storage,getOwnerLogin:()=>'timmy',
  backgroundSync:{{reconcile:async()=>{{}},requestSync:async()=>{{}},
    send:async item=>{{sent.push({{operationId:item.operationId,status:item.status,
      head:item.expectedHeadSha,decision:item.decision}});return{{message:{{id:91}}}};}}}},
}});
outbox.enqueue({{kind:'pull-review',repository:'o/r',number:7,operationId:'review-stable',
  body:'Ship it',decision:'approve',expectedHeadSha:'abc123'}});
outbox.reconcileBackground([{{id:'review-stable',kind:'pull-review',status:'authorization',
  error:'Fresh authorization required'}}]);
(async()=>{{const held=outbox.list()[0];const result=await outbox.retry(held.id,'timmy');
process.stdout.write(JSON.stringify({{held,sent,result,remaining:outbox.list()}}));}})();
"""
    output = run_node(script)

    assert output["held"]["status"] == "authorization"
    assert output["sent"] == [{
        "operationId": "review-stable", "status": "authorization",
        "head": "abc123", "decision": "approve",
    }]
    assert output["result"]["confirmed"] == [{"id": 91}]
    assert output["remaining"] == []


@pytest.mark.anyio
async def test_mobile_dashboard_loads_and_operates_authored_message_outbox():
    html = await dashboard()

    assert '<script src="static/authored-outbox.js"></script>' in html
    assert "const authoredOutbox = createAuthoredOutbox({" in html
    assert "activeFlushLogin = contextIdentityFresh ?" in html
    assert "flushAuthoredOutbox();" in html
    assert "await authoredOutbox.enqueueDurably" in html
    assert "Saving for background delivery…" in html
    assert "Saved for next launch; background delivery unavailable." in html
    assert "authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html
    assert "authoredOutbox.discard(item.outbox_id)" in html
    assert "if (result?.queued)" in html
    assert "backgroundSync: backgroundIssueSync" in html
    assert "authoredOutbox.reconcileBackground(records)" in html


def test_screenshot_comment_admits_blob_to_indexeddb_without_putting_bytes_in_localstorage():
    script = f"""
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
const values = new Map(); const mirrors=[];
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox=createAuthoredOutbox({{
  storage,getOwnerLogin:()=>'timmy',
  backgroundSync:{{reconcile:async(items,lane)=>mirrors.push({{items,lane}}),requestSync:async()=>{{}}}},
}});
(async()=>{{
  const blob=new Blob(['PRIVATE-IMAGE-BYTES'],{{type:'image/png'}});
  const admission=await outbox.enqueueDurably({{
    kind:'issue-comment',repository:'stackchain/dashboard',number:477,
    body:'Broken at 320px',operationId:'comment-image-477',
    attachment:{{filename:'phone.png',contentType:'image/png',blob}},
  }});
  const durable=mirrors[0].items[0].attachment;
  process.stdout.write(JSON.stringify({{admission,local:values.get('stackchain.authored-outbox.v1'),
    mirrored:{{lane:mirrors[0].lane,isBlob:durable.blob instanceof Blob,size:durable.blob?.size,text:await durable.blob?.text()}}}}));
}})();
"""
    output = run_node(script)

    assert "PRIVATE-IMAGE-BYTES" not in output["local"]
    local_item = json.loads(output["local"])["items"][0]
    assert local_item["attachment"] == {
        "filename": "phone.png", "contentType": "image/png", "stored": True
    }
    assert output["mirrored"] == {
        "lane": "authored", "isBlob": True, "size": 19, "text": "PRIVATE-IMAGE-BYTES"
    }
    assert output["admission"]["durability"] == "background"


def test_screenshot_comment_failed_indexeddb_admission_is_not_accepted_locally():
    script = f"""
const createAuthoredOutbox=require({json.dumps(str(OUTBOX))});
const values=new Map();
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',backgroundSync:{{
  reconcile:async()=>{{throw new Error('IndexedDB unavailable');}},requestSync:async()=>{{}},
}}}});
(async()=>{{
  let error='';try{{await outbox.enqueueDurably({{kind:'issue-comment',repository:'o/r',number:1,
    body:'Keep me',operationId:'image-op',attachment:{{filename:'a.png',contentType:'image/png',data:'abc'}}}});}}
  catch(caught){{error=caught.message;}}
  process.stdout.write(JSON.stringify({{error,items:outbox.list()}}));
}})();
"""
    output = run_node(script)
    assert output == {"error": "IndexedDB unavailable", "items": []}


def test_pull_screenshot_comment_persists_blob_only_in_indexeddb():
    script = f"""
const createAuthoredOutbox=require({json.dumps(str(OUTBOX))});
const values=new Map();const mirrors=[];
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',backgroundSync:{{
  reconcile:async items=>mirrors.push(items),requestSync:async()=>{{}},
}}}});
(async()=>{{const blob=new Blob(['PULL-PRIVATE-BYTES'],{{type:'image/webp'}});
  await outbox.enqueueDurably({{kind:'pull-comment',repository:'stackchain/web',number:31,
    body:'Mobile proof',operationId:'pull-image-31',
    attachment:{{filename:'proof.webp',contentType:'image/webp',blob}}}});
  const local=values.get('stackchain.authored-outbox.v1');const durable=mirrors[0][0].attachment;
  process.stdout.write(JSON.stringify({{local,metadata:JSON.parse(local).items[0].attachment,
    durable:{{isBlob:durable.blob instanceof Blob,text:await durable.blob.text()}}}}));
}})();
"""
    output = run_node(script)

    assert "PULL-PRIVATE-BYTES" not in output["local"]
    assert output["metadata"] == {
        "filename": "proof.webp", "contentType": "image/webp", "stored": True
    }
    assert output["durable"] == {"isBlob": True, "text": "PULL-PRIVATE-BYTES"}


def test_conversation_photo_bundle_keeps_only_metadata_in_localstorage_and_all_blobs_in_indexeddb():
    script = f"""
const createAuthoredOutbox=require({json.dumps(str(OUTBOX))});
const values=new Map();const mirrors=[];
const storage={{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',backgroundSync:{{
  reconcile:async items=>mirrors.push(items),requestSync:async()=>{{}},
}}}});
const photo=(name,bytes)=>({{filename:name,contentType:'image/jpeg',blob:new Blob([bytes],{{type:'image/jpeg'}})}});
(async()=>{{
  await outbox.enqueueDurably({{kind:'update-reply-read',notificationId:943,body:'Before and after',
    operationId:'reply-bundle-943',attachments:[photo('before.jpg','PRIVATE-BEFORE'),photo('after.jpg','PRIVATE-AFTER')]}});
  const local=values.get('stackchain.authored-outbox.v1');
  const durable=mirrors[0][0].attachments;
  process.stdout.write(JSON.stringify({{local,metadata:JSON.parse(local).items[0].attachments,
    durable:await Promise.all(durable.map(async value=>({{filename:value.filename,isBlob:value.blob instanceof Blob,text:await value.blob.text()}})))}}));
}})();
"""
    output = run_node(script)

    assert "PRIVATE-BEFORE" not in output["local"]
    assert "PRIVATE-AFTER" not in output["local"]
    assert output["metadata"] == [
        {"filename": "before.jpg", "contentType": "image/jpeg", "stored": True},
        {"filename": "after.jpg", "contentType": "image/jpeg", "stored": True},
    ]
    assert output["durable"] == [
        {"filename": "before.jpg", "isBlob": True, "text": "PRIVATE-BEFORE"},
        {"filename": "after.jpg", "isBlob": True, "text": "PRIVATE-AFTER"},
    ]
