feat: post reviewed Today summaries to Gitea (Closes #1130)
This commit is contained in:
1 parent
8794200c2a
commit
a0da8e9fb2
6 files changed
+295
-6
No files matched your search
@@ -58,6 +58,9 @@ def test_release_artifact_reviews_and_shares_a_private_mobile_today_summary(
|
||||
storage:localStorage,getLogin:()=> 'timmy',
|
||||
share:payload=>navigator.share(payload),
|
||||
copy:text=>navigator.clipboard.writeText(text),
|
||||
createOperationId:()=> 'release-summary-op',
|
||||
resolveTarget:async target=>({...target,kind:'issue',title:'Daily status',state:'open'}),
|
||||
enqueueDurably:async message=>{ window.__queuedSummary=message; return {item:message,durability:'background'}; },
|
||||
});
|
||||
const view=createTodaySummaryView({summary,qs:selector=>document.querySelector(selector),escapeHtml:value=>String(value)});
|
||||
window.__summaryTest={summary,view};
|
||||
@@ -80,6 +83,42 @@ def test_release_artifact_reviews_and_shares_a_private_mobile_today_summary(
|
||||
assert bounds and bounds["height"] >= 44
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||
|
||||
page.locator("#today-summary-destination").fill("acme/mobile#41")
|
||||
page.evaluate(
|
||||
"""
|
||||
() => {
|
||||
window.__summaryTest.summary.setDestination(document.querySelector('#today-summary-destination').value);
|
||||
return window.__summaryTest.view.validateDestination(document.querySelector('#validate-today-summary-destination'));
|
||||
}
|
||||
"""
|
||||
)
|
||||
expect(page.locator("#today-summary-target")).to_contain_text(
|
||||
"acme/mobile#41 · issue · Daily status"
|
||||
)
|
||||
expect(page.locator("#post-today-summary")).to_be_enabled()
|
||||
page.evaluate(
|
||||
"window.__summaryTest.view.postDraft(document.querySelector('#post-today-summary'))"
|
||||
)
|
||||
expect(sheet).to_be_hidden()
|
||||
assert page.evaluate("window.__queuedSummary") == {
|
||||
"kind": "search-reply",
|
||||
"repository": "acme/mobile",
|
||||
"number": 41,
|
||||
"targetKind": "issue",
|
||||
"body": "Today\n- Ship mobile capture\n\nTomorrow\n- Review release",
|
||||
"operationId": "release-summary-op",
|
||||
}
|
||||
|
||||
page.evaluate(
|
||||
"""
|
||||
window.__summaryTest.view.open(
|
||||
[{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',context:'acme/mobile#41',actual_minutes:42}],
|
||||
[{identity:'issue:acme/mobile:42:',title:'Review release',context:'acme/mobile#42'}]
|
||||
)
|
||||
"""
|
||||
)
|
||||
expect(sheet).to_be_visible()
|
||||
|
||||
page.evaluate(
|
||||
"""
|
||||
() => {
|
||||
|
||||
@@ -116,6 +116,109 @@ process.stdout.write(JSON.stringify({{
|
||||
assert output["keptAfterCopy"] is False
|
||||
|
||||
|
||||
def test_summary_validates_an_exact_gitea_thread_before_durable_posting():
|
||||
script = f"""
|
||||
const createSummary = require({json.dumps(str(TODAY_SUMMARY))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
const resolved=[];
|
||||
const admitted=[];
|
||||
const summary=createSummary({{
|
||||
storage,getLogin:()=> 'timmy',createOperationId:()=> 'summary-op-1',
|
||||
resolveTarget:async target=>{{resolved.push(target);return{{...target,kind:'issue',title:'Daily status',state:'open'}};}},
|
||||
enqueueDurably:async message=>{{admitted.push(message);return{{item:message,durability:'background'}};}},
|
||||
}});
|
||||
summary.begin([{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',actual_minutes:42}}], []);
|
||||
const parsed=summary.setDestination(' StackChain/Status #42 ');
|
||||
const target=await summary.validateDestination();
|
||||
const result=await summary.postSummary();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
parsed,target,result,resolved,admitted,snapshot:summary.snapshot(),
|
||||
kept:values.has('stackchain.today-summary-draft.v1.timmy'),
|
||||
}}));
|
||||
"""
|
||||
output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})")
|
||||
assert output["parsed"] == {
|
||||
"repository": "stackchain/status", "number": 42, "label": "stackchain/status#42"
|
||||
}
|
||||
assert output["resolved"] == [{"repository": "stackchain/status", "number": 42}]
|
||||
assert output["target"] == {
|
||||
"repository": "stackchain/status", "number": 42, "kind": "issue",
|
||||
"title": "Daily status", "state": "open",
|
||||
}
|
||||
assert output["admitted"] == [{
|
||||
"kind": "search-reply", "repository": "stackchain/status", "number": 42,
|
||||
"targetKind": "issue", "body": "Today\n- Ship mobile capture",
|
||||
"operationId": "summary-op-1",
|
||||
}]
|
||||
assert output["result"]["status"] == "queued"
|
||||
assert output["result"]["durability"] == "background"
|
||||
assert output["snapshot"] is None
|
||||
assert output["kept"] is False
|
||||
|
||||
|
||||
def test_summary_keeps_reviewed_destination_and_operation_when_admission_fails():
|
||||
script = f"""
|
||||
const createSummary = require({json.dumps(str(TODAY_SUMMARY))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
const options={{
|
||||
storage,getLogin:()=> 'timmy',createOperationId:()=> 'stable-op',
|
||||
resolveTarget:async target=>({{...target,kind:'pull',title:'Release candidate',state:'open'}}),
|
||||
enqueueDurably:async()=>{{throw new Error('storage unavailable');}},
|
||||
}};
|
||||
const summary=createSummary(options);
|
||||
summary.begin([{{identity:'pull:acme/mobile:9:',title:'Review release',actual_minutes:18}}], []);
|
||||
summary.setDestination('acme/mobile#9');
|
||||
await summary.validateDestination();
|
||||
let error='';
|
||||
try {{ await summary.postSummary(); }} catch (caught) {{ error=caught.message; }}
|
||||
const restored=createSummary(options);
|
||||
process.stdout.write(JSON.stringify({{
|
||||
error,restored:restored.restore(),snapshot:restored.snapshot(),text:restored.text(),
|
||||
}}));
|
||||
"""
|
||||
output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})")
|
||||
assert output["error"] == "storage unavailable"
|
||||
assert output["restored"] is True
|
||||
assert output["snapshot"]["operation_id"] == "stable-op"
|
||||
assert output["snapshot"]["target"]["kind"] == "pull"
|
||||
assert output["text"] == "Today\n- Review release"
|
||||
|
||||
|
||||
def test_summary_coalesces_concurrent_post_taps_into_one_durable_admission():
|
||||
script = f"""
|
||||
const createSummary = require({json.dumps(str(TODAY_SUMMARY))});
|
||||
let release;
|
||||
const gate=new Promise(resolve=>release=resolve);
|
||||
const admitted=[];
|
||||
const summary=createSummary({{
|
||||
getLogin:()=> 'timmy',createOperationId:()=> 'one-operation',
|
||||
resolveTarget:async target=>({{...target,kind:'issue',title:'Status',state:'open'}}),
|
||||
enqueueDurably:async message=>{{admitted.push(message);await gate;return{{item:message,durability:'background'}};}},
|
||||
}});
|
||||
summary.begin([{{identity:'issue:acme/mobile:41:',title:'Ship mobile capture',actual_minutes:42}}], []);
|
||||
summary.setDestination('acme/mobile#41');
|
||||
await summary.validateDestination();
|
||||
const first=summary.postSummary();
|
||||
const second=summary.postSummary();
|
||||
release();
|
||||
const results=await Promise.all([first,second]);
|
||||
process.stdout.write(JSON.stringify({{admitted,results}}));
|
||||
"""
|
||||
output = run_node(f"(async()=>{{{script}}})().catch(error=>{{console.error(error);process.exit(1);}})")
|
||||
assert len(output["admitted"]) == 1
|
||||
assert output["results"][0] == output["results"][1]
|
||||
|
||||
|
||||
def test_dashboard_connects_recap_and_wrap_up_to_a_mobile_summary_review_sheet():
|
||||
from src import main
|
||||
|
||||
@@ -130,13 +233,21 @@ def test_dashboard_connects_recap_and_wrap_up_to_a_mobile_summary_review_sheet()
|
||||
assert 'id="today-summary-tomorrow"' in html
|
||||
assert 'id="today-summary-include-actuals"' in html
|
||||
assert 'id="share-today-summary"' in html
|
||||
assert 'id="today-summary-destination"' in html
|
||||
assert 'id="validate-today-summary-destination"' in html
|
||||
assert 'id="post-today-summary"' in html
|
||||
assert 'id="today-summary-target"' in html
|
||||
assert "static/today-summary.js" in main.FRONTEND_BUILD.page_sources
|
||||
assert "setupTodaySummary({" in dashboard
|
||||
assert "enqueueDurably:message => authoredOutbox.enqueueDurably(message)" in dashboard
|
||||
assert "fetchJson:api" in dashboard
|
||||
assert "/preview?kind=issue" in (ROOT / "frontend" / "today-summary.js").read_text()
|
||||
assert "todaySummaryView.open(workedItems, tomorrowItems)" in dashboard
|
||||
assert "openWrapUp(handoff.actual_minutes, workedItems)" in recap
|
||||
assert "onComplete(result, actualMinutes, workedItems, tomorrowItems)" in wrap_up
|
||||
assert ".today-summary-actions button { min-height:44px;" in css
|
||||
assert ".today-summary-panel" in css and "overflow-x:hidden" in css
|
||||
assert ".today-summary-destination" in css and "overflow-wrap:anywhere" in css
|
||||
assert "tests/e2e/test_mobile_today_summary_release.py" in (
|
||||
ROOT / ".gitea" / "workflows" / "ci.yml"
|
||||
).read_text()
|
||||
|
||||
Reference in new issue
Block a user