feat: carry mobile review feedback into Gitea (#129)
CI / lint (pull_request) Successful in 10s
CI / build-frontend (pull_request) Successful in 5s

This commit is contained in:
timmy committed 2026-08-06 18:56:06 +00:00
1 parent bd0ae619db
commit 331eb99c1e
3 files changed
+266

No files matched your search

+105
View File
@@ -260,6 +260,89 @@ process.stdout.write(JSON.stringify(changed));
}
def test_review_feedback_draft_restores_notes_summary_and_decision_for_same_head():
script = f"""
const reviewSheet = require({json.dumps(str(REVIEW_SHEET))});
const values = new Map();
const storage = {{
getItem(key) {{ return values.has(key) ? values.get(key) : null; }},
setItem(key, value) {{ values.set(key, value); }}
}};
const options = {{
storage, repository: 'stackchain/api', number: 7, headSha: 'abc123',
files: [{{filename:'src/a.py'}}, {{filename:'src/b.py'}}]
}};
const first = reviewSheet.createDraft(options);
first.setNote('src/a.py', 'Handle the empty state.');
first.setSummary('One blocker remains.');
first.setDecision('request_changes');
const restored = reviewSheet.createDraft(options).snapshot();
process.stdout.write(JSON.stringify(restored));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"notes": {"src/a.py": "Handle the empty state."},
"summary": "One blocker remains.",
"decision": "request_changes",
}
def test_review_feedback_formats_non_empty_file_notes_in_changed_file_order():
script = f"""
const reviewSheet = require({json.dumps(str(REVIEW_SHEET))});
const markdown = reviewSheet.formatFeedback({{
decision: 'request_changes', summary: 'One blocker remains.',
notes: {{'src/b.py':'Second note', 'src/a.py':'First note', 'README.md':''}}
}}, [{{filename:'src/a.py'}}, {{filename:'README.md'}}, {{filename:'src/b.py'}}]);
process.stdout.write(markdown);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert result.stdout == (
"## Intended decision\nRequest changes\n\n"
"## Summary\nOne blocker remains.\n\n"
"## File notes\n### `src/a.py`\nFirst note\n\n"
"### `src/b.py`\nSecond note"
)
def test_review_handoff_opens_gitea_only_after_copy_and_preserves_failed_copy():
script = f"""
const reviewSheet = require({json.dumps(str(REVIEW_SHEET))});
const events = [];
const success = reviewSheet.copyAndContinue({{
text: 'feedback', url: 'https://forge.example/pulls/7',
copy: async text => events.push('copy:' + text),
open: url => events.push('open:' + url),
fallback: text => events.push('fallback:' + text),
}});
const failedEvents = [];
const failure = reviewSheet.copyAndContinue({{
text: 'keep me', url: 'https://forge.example/pulls/8',
copy: async () => {{ throw new Error('denied'); }},
open: url => failedEvents.push('open:' + url),
fallback: text => failedEvents.push('fallback:' + text),
}});
Promise.all([success, failure]).then(results => process.stdout.write(JSON.stringify({{
events, failedEvents, results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"events": ["copy:feedback", "open:https://forge.example/pulls/7"],
"failedEvents": ["fallback:keep me"],
"results": [True, False],
}
@pytest.mark.anyio
async def test_review_requests_open_an_accessible_mobile_detail_sheet():
html = await dashboard()
@@ -295,6 +378,28 @@ async def test_review_sheet_loads_details_and_preserves_safe_gitea_handoff():
assert "reviewController.submit" not in html
@pytest.mark.anyio
async def test_mobile_review_sheet_captures_and_safely_hands_off_feedback():
html = await dashboard()
review_script = REVIEW_SHEET.read_text()
assert 'class="review-note"' in review_script
assert 'id="review-decision"' in html
assert 'id="review-summary"' in html
assert 'id="copy-review-feedback"' in html
assert 'id="review-copy-fallback"' in html
assert '.review-note' in html and 'min-height:88px' in html
assert '.review-handoff' in html and 'position:sticky' in html
assert "createReviewController.createDraft" in html
assert "draft.setNote" in html
assert "draft?.setSummary" in html
assert "draft?.setDecision" in html
assert "createReviewController.formatFeedback" in html
assert "createReviewController.copyAndContinue" in html
assert "navigator.clipboard.writeText" in html
assert "window.open(url, '_blank', 'noopener,noreferrer')" in html
@pytest.mark.anyio
async def test_mobile_review_failure_offers_an_in_place_retry_for_the_same_item():
html = await dashboard()