From 83788c6b8221c7027932cdbe4200a3db94784442 Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 6 Aug 2026 17:09:40 +0000 Subject: [PATCH] feat: add mobile review readiness inspector (#121) --- frontend/index.html | 94 +++++++++++++++++++++++++++++++-- frontend/review-sheet.js | 20 +++++++ src/gitea_proxy.py | 64 ++++++++++++++++++++++ src/main.py | 41 ++++++++++++-- tests/test_gitea_work_search.py | 67 +++++++++++++++++++++++ tests/test_my_work.py | 53 +++++++++++++++++++ tests/test_review_api.py | 61 +++++++++++++++++++++ 7 files changed, 391 insertions(+), 9 deletions(-) create mode 100644 frontend/review-sheet.js create mode 100644 tests/test_review_api.py diff --git a/frontend/index.html b/frontend/index.html index 0dad9a2..4de7e07 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -61,15 +61,24 @@ textarea { resize: vertical; min-height: 120px; } .work-filter[aria-pressed="true"] { border-color:var(--accent); background:#1d4f7a; } .my-work-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; } .my-work-card { min-height: 44px; display:block; padding:12px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; color:var(--text); } +.my-work-card.review-trigger { width:100%; text-align:left; font:inherit; } .my-work-card:hover { border-color:var(--accent); } .my-work-card-title { display:block; margin:5px 0; font-weight:650; } .my-work[data-stale="true"] { border-color:#fcd34d; } +.review-sheet { position:fixed; inset:0; z-index:50; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); } +.review-sheet.open { display:flex; } +.review-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; } +.review-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; } +.review-sheet-body { white-space:pre-wrap; overflow-wrap:anywhere; } +.review-file, .review-history { padding:8px 0; border-bottom:1px solid #1b2d45; overflow-wrap:anywhere; } +.review-action { min-height:44px; } @media (max-width: 600px) { header { align-items:flex-start; } .my-work { margin:0; } .my-work-list { grid-template-columns:1fr; } .work-filters { width:100%; } .work-filter { flex:1 1 calc(50% - 8px); } + .review-sheet-panel { width:100%; border-left:0; padding:14px; } } .obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,') center/contain no-repeat; display:inline-block; } .footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; } @@ -189,12 +198,32 @@ textarea { resize: vertical; min-height: 120px; } + + + ' in html + assert '@media (max-width: 600px)' in html + assert '.review-sheet-panel' in html and 'width:100%' in html + assert '.review-action' in html and 'min-height:44px' in html + + +@pytest.mark.anyio +async def test_review_sheet_loads_details_and_preserves_safe_gitea_handoff(): + html = await dashboard() + + assert 'data-review-index' in html + assert "reviewController.load(selectedReview)" in html + assert "review-files" in html + assert "review-history" in html + assert "open-review-gitea" in html + assert "reviewController.submit" not in html diff --git a/tests/test_review_api.py b/tests/test_review_api.py new file mode 100644 index 0000000..fda4ed8 --- /dev/null +++ b/tests/test_review_api.py @@ -0,0 +1,61 @@ +import httpx +import pytest + +from src import main + + +@pytest.mark.anyio +async def test_review_detail_endpoint_returns_normalized_no_store_payload(monkeypatch): + async def requested(repository, number): + return (repository, number) == ("stackchain/api", 7) + + async def detail(repository, number): + assert (repository, number) == ("stackchain/api", 7) + return { + "repository": repository, + "number": number, + "title": "Review API", + "body": "Check retries", + "url": "https://forge.example/stackchain/api/pulls/7", + "author": "alex", + "ci_state": "success", + "files": [{"filename": "src/api.py", "status": "modified", "additions": 8, "deletions": 2}], + "reviews": [{"user": {"login": "sam"}, "state": "APPROVED", "body": "Good"}], + } + + monkeypatch.setattr(main, "is_requested_review", requested) + monkeypatch.setattr(main, "pull_review_detail", detail) + transport = httpx.ASGITransport(app=main.app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/api/v1/repos/stackchain/api/pulls/7/review") + + assert response.status_code == 200 + assert response.headers["cache-control"] == "no-store" + assert response.json()["files"][0]["filename"] == "src/api.py" + + +@pytest.mark.anyio +async def test_review_detail_rejects_pulls_not_requested_from_service_user(monkeypatch): + async def requested(repository, number): + return False + + monkeypatch.setattr(main, "is_requested_review", requested) + transport = httpx.ASGITransport(app=main.app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/api/v1/repos/private/secret/pulls/9/review") + + assert response.status_code == 404 + assert response.headers["cache-control"] == "no-store" + + +@pytest.mark.anyio +async def test_public_review_endpoint_does_not_expose_service_token_mutations(): + transport = httpx.ASGITransport(app=main.app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/api/v1/repos/stackchain/api/pulls/7/review", + json={"action": "approve"}, + ) + + assert response.status_code == 405 + -- 2.43.0