feat: paginate mobile My Work queues (#169)
CI / lint (pull_request) Successful in 12s
CI / build-frontend (pull_request) Successful in 4s

This commit is contained in:
timmy committed 2026-08-07 05:35:45 +00:00
1 parent 29a786cea3
commit 625e65e7e4
8 files changed
+588 -84

No files matched your search

+1
View File
@@ -19,4 +19,5 @@ def test_api_requests_resolve_inside_dashboard_subpath():
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications/",
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications?page=",
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications/read",
"https://forge.alexanderwhitestone.com/dashboard/api/v1/work/",
}
+122 -38
View File
@@ -2,27 +2,86 @@ import asyncio
import json
import pytest
import httpx
from src import gitea_proxy
from src import main
@pytest.mark.anyio
async def test_work_page_preserves_total_and_reason_without_loading_other_pages():
requests = []
def upstream(request):
requests.append(str(request.url))
return httpx.Response(
200,
headers={"X-Total-Count": "84"},
json=[{
"id": 51,
"number": 51,
"title": "Older review",
"state": "open",
"repository": {"full_name": "stackchain/api"},
"html_url": "https://forge.example/stackchain/api/pulls/51",
}],
)
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
try:
result = await gitea_proxy.work_page("review", page=2)
finally:
await gitea_proxy.stop_client()
assert requests == [
"http://127.0.0.1:3000/api/v1/repos/issues/search?state=open&review_requested=true&type=pulls&limit=50&page=2"
]
assert result["page"] == 2
assert result["total"] == 84
assert result["has_more"] is False
assert result["items"][0]["work_reasons"] == ["review_requested"]
@pytest.mark.anyio
async def test_initial_work_collections_expose_independent_pagination(monkeypatch):
async def fake_page(stream, page=1, limit=50):
assert page == 1
totals = {"issue": 84, "pull": 61, "review": 73}
return {
"items": [], "page": 1, "total": totals[stream],
"has_more": True, "stream": stream,
}
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
assigned_issues = await gitea_proxy.issues()
pulls = await gitea_proxy.pull_requests()
assert assigned_issues.pagination == {
"issue": {"page": 1, "total": 84, "has_more": True}
}
assert pulls.pagination == {
"pull": {"page": 1, "total": 61, "has_more": True},
"review": {"page": 1, "total": 73, "has_more": True},
}
@pytest.mark.anyio
async def test_work_collections_include_supported_review_request_search(monkeypatch):
requested_paths = []
requested_streams = []
async def fake_fetch(path):
requested_paths.append(path)
return []
async def fake_page(stream, page=1, limit=50):
requested_streams.append((stream, page, limit))
return {"stream": stream, "items": [], "page": page, "total": 0, "has_more": False}
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
assert await gitea_proxy.issues() == []
assert await gitea_proxy.pull_requests() == []
assert requested_paths == [
"repos/issues/search?state=open&assigned=true&type=issues&limit=50",
"repos/issues/search?state=open&assigned=true&type=pulls&limit=50",
"repos/issues/search?state=open&review_requested=true&type=pulls&limit=50",
assert requested_streams == [
("issue", 1, 50),
("pull", 1, 50),
("review", 1, 50),
]
@@ -41,12 +100,16 @@ async def test_pull_requests_merge_assignment_and_review_responsibilities(monkey
"repository": {"full_name": "stackchain/mobile"},
}
async def fake_fetch(path):
if "assigned=true" in path:
return [assigned]
return [assigned.copy(), review_only]
async def fake_page(stream, page=1, limit=50):
items = [assigned] if stream == "pull" else [assigned.copy(), review_only]
reason = "assigned_to_me" if stream == "pull" else "review_requested"
return {
"stream": stream,
"items": [{**item, "work_reasons": [reason]} for item in items],
"page": page, "total": len(items), "has_more": False,
}
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
pulls = await gitea_proxy.pull_requests()
@@ -60,13 +123,13 @@ async def test_pull_request_searches_start_concurrently(monkeypatch):
started = [asyncio.Event(), asyncio.Event()]
release = asyncio.Event()
async def fake_fetch(path):
index = 0 if "assigned=true" in path else 1
async def fake_page(stream, page=1, limit=50):
index = 0 if stream == "pull" else 1
started[index].set()
await release.wait()
return []
return {"stream": stream, "items": [], "page": page, "total": 0, "has_more": False}
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
task = asyncio.create_task(gitea_proxy.pull_requests())
try:
await asyncio.wait_for(
@@ -80,30 +143,51 @@ async def test_pull_request_searches_start_concurrently(monkeypatch):
@pytest.mark.anyio
async def test_requested_review_guard_uses_only_dedicated_review_search(monkeypatch):
requested_paths = []
requested = []
async def fake_fetch(path):
requested_paths.append(path)
return [
{
"number": 7,
"repository": {"full_name": "stackchain/api"},
},
{
"number": 8,
"repository": {"full_name": "stackchain/api"},
},
]
async def fake_page(stream, page=1, limit=50):
requested.append((stream, page))
items = [] if page == 1 else [{
"number": 77,
"repository": {"full_name": "stackchain/api"},
}]
return {
"stream": stream, "items": items, "page": page,
"total": 51, "has_more": page == 1,
}
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
assert await gitea_proxy.is_requested_review("stackchain/api", 7) is True
assert await gitea_proxy.is_requested_review("stackchain/api", 77) is True
assert await gitea_proxy.is_requested_review("stackchain/api", 9) is False
assert await gitea_proxy.is_requested_review("stackchain/private", 7) is False
assert requested_paths == [
"repos/issues/search?state=open&review_requested=true&type=pulls&limit=50",
"repos/issues/search?state=open&review_requested=true&type=pulls&limit=50",
"repos/issues/search?state=open&review_requested=true&type=pulls&limit=50",
assert requested == [
("review", 1), ("review", 2),
("review", 1), ("review", 2),
]
@pytest.mark.anyio
async def test_assignment_guards_find_eligible_items_beyond_first_page(monkeypatch):
requested = []
async def fake_page(stream, page=1, limit=50):
requested.append((stream, page))
return {
"stream": stream,
"items": [] if page == 1 else [{
"number": 77,
"repository": {"full_name": "stackchain/api"},
}],
"page": page, "total": 51, "has_more": page == 1,
}
monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
assert await gitea_proxy.is_assigned_issue("stackchain/api", 77) is True
assert await gitea_proxy.is_assigned_pull("stackchain/api", 77) is True
assert requested == [
("issue", 1), ("issue", 2),
("pull", 1), ("pull", 2),
]
+96
View File
@@ -119,6 +119,102 @@ process.stdout.write(JSON.stringify(buildMyWork.countMyWork({json.dumps(items)})
assert json.loads(result.stdout) == {"all": 3, "issue": 1, "pull": 1, "review": 1, "update": 0}
def test_work_pager_is_single_flight_and_unions_pull_responsibilities():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
let calls = 0;
let release;
const pages = [];
const states = [];
const statuses = [];
const pager = buildMyWork.createWorkPager({{
load: (stream, page) => {{
calls += 1;
return new Promise(resolve => {{ release = () => resolve({{
stream, page, total: 51, has_more: false,
items: [
{{id:1, title:'shared', work_reasons:['review_requested']}},
{{id:51, title:'older', work_reasons:['review_requested']}}
],
}}); }});
}},
onItems: (stream, items) => states.push({{stream, items}}),
onPagination: pagination => pages.push(pagination),
onStatus: status => statuses.push(status),
}});
pager.reset({{review:{{page:1,total:51,has_more:true}}}});
const existing = [{{id:1,title:'shared',work_reasons:['assigned_to_me']}}];
const first = pager.loadMore('review', existing);
const duplicate = pager.loadMore('review', existing);
release();
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
calls, pages, states, statuses, results
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == 1
assert output["states"] == [{
"stream": "review",
"items": [
{"id": 1, "title": "shared", "work_reasons": ["assigned_to_me", "review_requested"]},
{"id": 51, "title": "older", "work_reasons": ["review_requested"]},
],
}]
assert output["pages"][-1]["review"] == {"page": 2, "total": 51, "has_more": False}
assert output["statuses"] == [
"Loading older review requests…", "51 of 51 review requests loaded."
]
assert output["results"] == [True, False]
def test_work_pager_keeps_items_and_retries_same_page_after_failure():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
const requested = [];
const states = [];
const statuses = [];
const pager = buildMyWork.createWorkPager({{
load: async (stream, page) => {{ requested.push([stream,page]); throw new Error('offline'); }},
onItems: (stream, items) => states.push(items),
onPagination: () => {{}},
onStatus: status => statuses.push(status),
}});
pager.reset({{issue:{{page:2,total:125,has_more:true}}}});
pager.loadMore('issue', [{{id:1}}]).then(result =>
pager.loadMore('issue', [{{id:1}}]).then(retry =>
process.stdout.write(JSON.stringify({{requested,states,statuses,result,retry}}))
)
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
assert json.loads(result.stdout) == {
"requested": [["issue", 3], ["issue", 3]],
"states": [],
"statuses": [
"Loading older issues…", "Could not load older issues. Retry.",
"Loading older issues…", "Could not load older issues. Retry.",
],
"result": False,
"retry": False,
}
@pytest.mark.anyio
async def test_mobile_my_work_exposes_truthful_work_pagination_control():
html = await dashboard()
assert 'id="work-page-status"' in html
assert 'id="load-more-work"' in html
assert '>Load older work<' in html
assert '.load-more-work { min-height:44px;' in html
def test_issue_capture_is_single_flight_and_keeps_draft_until_confirmed_success():
script = f"""
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
+60
View File
@@ -0,0 +1,60 @@
import httpx
import pytest
from src import main
@pytest.mark.anyio
async def test_work_page_endpoint_normalizes_requested_page_and_is_not_cacheable(monkeypatch):
requested = []
async def page_loader(stream, page):
requested.append((stream, page))
return {
"stream": stream,
"page": page,
"total": 84,
"has_more": False,
"items": [{
"id": 51, "number": 51, "title": "Older issue", "state": "open",
"labels": [], "assignees": [{"login": "timmy"}],
"repository": {"full_name": "stackchain/api"},
"updated_at": "2026-08-07T10:00:00Z",
"html_url": "https://forge.example/stackchain/api/issues/51",
}],
}
monkeypatch.setattr(main.gitea_proxy, "work_page", page_loader)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/work/issue?page=2")
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert requested == [("issue", 2)]
assert response.json() == {
"stream": "issue", "page": 2, "total": 84, "has_more": False,
"items": [{
"id": 51, "number": 51, "title": "Older issue", "state": "open",
"labels": [], "assignees": ["timmy"], "repository": "stackchain/api",
"updated_at": "2026-08-07T10:00:00Z",
"url": "https://forge.example/stackchain/api/issues/51",
}],
}
@pytest.mark.anyio
async def test_work_page_endpoint_rejects_unknown_stream_before_upstream_io(monkeypatch):
called = False
async def page_loader(stream, page):
nonlocal called
called = True
monkeypatch.setattr(main.gitea_proxy, "work_page", page_loader)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/work/unknown?page=2")
assert response.status_code == 422
assert called is False