stackchain-dashboard/tests/test_authored_outbox.py
timmy d66c1b48ef
All checks were successful
CI / lint (pull_request) Successful in 22s
CI / build-frontend (pull_request) Successful in 5s
feat: queue authored messages for reconnect (#234)
2026-08-07 22:14:49 +00:00

137 lines
6.1 KiB
Python

import json
import subprocess
from pathlib import Path
import pytest
from src.views 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_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, 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().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_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,
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().then(async first => {{
phase='transient';
outbox.enqueue({{kind:'update-reply',notificationId:3,body:'Later',operationId:'c'}});
const second=await outbox.flush();
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_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, 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(); const retry=outbox.retry(item.id); 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}});
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
@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 "createAuthoredOutbox({ storage: localStorage" in html
assert "window.addEventListener('online', flushAuthoredOutbox)" in html
assert "authoredOutbox.enqueue" in html
assert "authoredOutbox.retry(item.outbox_id)" in html
assert "authoredOutbox.discard(item.outbox_id)" in html
assert "if (result?.queued)" in html