feat: show conversations in mobile search preview (Closes #803)
CI / lint (pull_request) Successful in 1m45s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 06:04:25 +00:00
1 parent 2406d24cb8
commit 51fe9e5af2
7 files changed
+281 -16

No files matched your search

+99
View File
@@ -386,6 +386,26 @@ process.stdout.write(JSON.stringify({{numbers:loaded.items.map(item => item.numb
assert json.loads(result.stdout) == {"numbers": [1, 2], "more": False}
def test_mobile_search_preview_progressively_renders_a_retryable_paged_conversation():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
preview_source = SEARCH_PREVIEW.read_text()
assert 'id="search-preview-conversation"' in html
assert 'id="search-preview-comments"' in html
assert 'id="search-preview-conversation-status"' in html
assert 'id="retry-search-preview-conversation"' in html
assert 'id="load-older-search-preview-comments"' in html
assert 'aria-live="polite"' in html
assert "fetchConversation:" in html
assert "api.retryConversation()" in preview_source
assert "api.loadOlderConversation()" in preview_source
assert "renderMarkdown(comment.body || '')" in preview_source
assert ".search-preview-conversation" in css
assert ".search-preview-comment" in css
assert "min-height:44px" in css
def test_mobile_search_preview_renders_session_navigation_and_advances_after_planning():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
@@ -440,6 +460,85 @@ if (ready.length !== 1 || ready[0].detail.number !== 2) {{
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
def test_search_preview_publishes_detail_before_progressive_conversation_and_ignores_stale_comments():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
const pending = new Map();
const states = [];
const preview = createSearchPreview({{
fetchJson: item => Promise.resolve({{...item, title:'Issue ' + item.number}}),
fetchConversation: item => new Promise(resolve => pending.set(item.number, resolve)),
mutate: () => Promise.resolve(),
onState: state => states.push(state),
}});
await preview.open({{repository:'stackchain/api', number:1, kind:'issue'}});
const firstReady = states.find(state => state.status === 'ready' && state.detail?.number === 1);
if (!firstReady || firstReady.conversation?.status !== 'loading') throw new Error('detail waited for conversation');
await preview.open({{repository:'stackchain/api', number:2, kind:'issue'}});
pending.get(1)({{comments:[{{id:1,body:'stale'}}], page:1, older_page:null}});
pending.get(2)({{comments:[{{id:2,body:'current'}}], page:1, older_page:null}});
await new Promise(resolve => setTimeout(resolve, 0));
const final = states.at(-1);
process.stdout.write(JSON.stringify({{
number:final.detail.number,
status:final.conversation.status,
comments:final.conversation.comments.map(comment => comment.body),
}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"number": 2,
"status": "ready",
"comments": ["current"],
}
def test_search_preview_retries_failed_conversation_and_prepends_older_messages_without_duplicates():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
(async () => {{
let calls = 0;
const states = [];
const preview = createSearchPreview({{
fetchJson: item => Promise.resolve({{...item, title:'Issue'}}),
fetchConversation: (_item, page) => {{
calls += 1;
if (calls === 1) return Promise.reject(new Error('offline'));
if (page === 1) return Promise.resolve({{comments:[{{id:1,body:'old'}},{{id:3,body:'duplicate'}}], older_page:null}});
return Promise.resolve({{comments:[{{id:3,body:'newer'}},{{id:4,body:'newest'}}], older_page:1}});
}},
mutate: () => Promise.resolve(),
onState: state => states.push(state),
}});
await preview.open({{repository:'stackchain/api', number:42, kind:'issue'}});
await new Promise(resolve => setTimeout(resolve, 0));
if (states.at(-1).conversation.status !== 'error') throw new Error('failure unavailable');
await preview.retryConversation();
await preview.loadOlderConversation();
const final = states.at(-1).conversation;
process.stdout.write(JSON.stringify({{
calls,
status:final.status,
ids:final.comments.map(comment => comment.id),
olderPage:final.olderPage,
}}));
}})().catch(error => {{ console.error(error); process.exit(1); }});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"calls": 3,
"status": "ready",
"ids": [1, 3, 4],
"olderPage": None,
}
def test_search_preview_claim_is_single_flight_and_reports_confirmation():
script = f"""
const createSearchPreview = require({json.dumps(str(SEARCH_PREVIEW))});
+37
View File
@@ -186,6 +186,43 @@ async def test_global_search_preview_returns_normalized_action_context(monkeypat
assert response.json()["claimable"] is True
@pytest.mark.anyio
@pytest.mark.parametrize("kind", ["issue", "pull"])
async def test_global_search_preview_conversation_returns_a_bounded_authorized_page(
monkeypatch, kind
):
requested = []
async def conversation(repository, number, page, limit):
requested.append((repository, number, page, limit))
return {
"comments": [{
"id": 7,
"author": "alexander",
"body": "Current **decision**",
"created_at": "2026-08-14T12:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42#issuecomment-7",
}],
"page": 3,
"older_page": 2,
"total": 41,
}
monkeypatch.setattr(
main.gitea_proxy, "issue_conversation_page", conversation, raising=False
)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
f"/api/v1/repos/stackchain/api/issues/42/preview/conversation?kind={kind}&limit=20"
)
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.json()["comments"][0]["body"] == "Current **decision**"
assert requested == [("stackchain/api", 42, None, 20)]
@pytest.mark.anyio
async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results():
requests = []