548 lines
26 KiB
Python
548 lines
26 KiB
Python
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_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_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"] == []
|
|
|
|
|
|
@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_bytes_to_indexeddb_without_putting_them_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 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',data:'PRIVATE-IMAGE-BYTES'}},
|
|
}});
|
|
process.stdout.write(JSON.stringify({{admission,local:values.get('stackchain.authored-outbox.v1'),mirrored:mirrors[0]}}));
|
|
}})();
|
|
"""
|
|
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"
|
|
assert output["mirrored"]["items"][0]["attachment"]["data"] == "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": []}
|