feat: complete authored pull workspace access (Closes #1348)
CI / lint (pull_request) Successful in 3m15s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 5m22s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-24 12:10:49 +00:00
1 parent 050390d6f6
commit 2102b0e5da
7 files changed
+264 -44

No files matched your search

@@ -26,6 +26,7 @@ def test_authored_pull_queue_is_phone_usable_and_opens_the_existing_pull_route(v
page.add_script_tag(path=FRONTEND / "my-work.js")
page.add_script_tag(path=FRONTEND / "work-route.js")
page.add_script_tag(path=FRONTEND / "mobile-queue-launcher.js")
page.add_script_tag(path=FRONTEND / "pull-sheet.js")
page.evaluate("document.querySelector('#mobile-queue-sheet').showModal()")
row = page.locator('[data-mobile-queue="authored"]')
@@ -63,6 +64,25 @@ def test_authored_pull_queue_is_phone_usable_and_opens_the_existing_pull_route(v
"opened": "opened",
"count": 1,
}
capabilities = page.evaluate("""() => {
const item = {key:'stackchain/dashboard#41'};
createPullSheet.resetOwnershipControls(
document, item, () => false, {authored:true, assigned:false}
);
const ownership = document.querySelector('#pull-ownership');
return {
hidden: ownership.hidden,
inert: ownership.inert,
releaseDisabled: document.querySelector('#release-pull').disabled,
handoffDisabled: document.querySelector('#load-pull-handoff').disabled,
};
}""")
assert capabilities == {
"hidden": True,
"inert": True,
"releaseDisabled": True,
"handoffDisabled": True,
}
assert page.evaluate(
"document.documentElement.scrollWidth > document.documentElement.clientWidth"
) is False
+33
View File
@@ -7642,6 +7642,39 @@ process.stdout.write(JSON.stringify({{
assert output["released"] == "stackchain/api#7 released. Choose the next ready Today item."
def test_authored_unassigned_pull_hides_assignment_ownership_controls():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
const elements = new Map([
['#pull-ownership', {{open:true,hidden:false,inert:false}}],
['#pull-handoff-recipient', {{innerHTML:'stale',disabled:false}}],
['#confirm-pull-handoff', {{disabled:false,textContent:''}}],
['#load-pull-handoff', {{disabled:false}}],
['#release-pull', {{disabled:false,textContent:''}}],
['#pull-handoff-status', {{textContent:''}}],
['#edit-pull-content', {{hidden:false}}],
['#pull-edit-form', {{hidden:false}}],
['#pull-edit-status', {{textContent:'stale'}}],
]);
const doc = {{querySelector:selector => elements.get(selector)}};
createPullSheet.resetOwnershipControls(
doc,
{{key:'stackchain/api#7'}},
() => false,
{{authored:true,assigned:false}}
);
process.stdout.write(JSON.stringify(Object.fromEntries(elements)));
"""
output = json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout)
assert output["#pull-ownership"]["hidden"] is True
assert output["#pull-ownership"]["inert"] is True
assert output["#load-pull-handoff"]["disabled"] is True
assert output["#release-pull"]["disabled"] is True
def test_pull_sheet_single_flights_handoff_and_release_ownership_mutations():
script = f"""
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
+80 -10
View File
@@ -39,7 +39,7 @@ async def test_pull_author_can_publish_assigned_draft_at_expected_head(monkeypat
@pytest.mark.anyio
async def test_gitea_publishes_only_authored_assigned_draft_and_verifies_ready_head():
async def test_gitea_publishes_authored_unassigned_draft_and_verifies_ready_head():
requests = []
pull = {
"number": 7,
@@ -48,7 +48,7 @@ async def test_gitea_publishes_only_authored_assigned_draft_and_verifies_ready_h
"draft": True,
"merged": False,
"user": {"login": "alex"},
"assignees": [{"login": "alex"}],
"assignees": [],
"head": {"sha": "abc1234"},
}
@@ -131,7 +131,7 @@ async def test_pull_author_can_update_assigned_open_pull_context(monkeypatch):
@pytest.mark.anyio
async def test_gitea_pull_context_update_requires_author_assignment_and_matching_head():
async def test_gitea_pull_context_update_allows_author_without_assignment_at_matching_head():
requests = []
async def handler(request):
@@ -141,7 +141,7 @@ async def test_gitea_pull_context_update_requires_author_assignment_and_matching
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7" and request.method == "GET":
return httpx.Response(200, json={
"number": 7, "title": "Old context", "body": "Old body", "state": "open",
"merged": False, "user": {"login": "alex"}, "assignees": [{"login": "alex"}],
"merged": False, "user": {"login": "alex"}, "assignees": [],
"head": {"sha": "abc1234"},
"html_url": "https://forge.example/stackchain/api/pulls/7",
})
@@ -172,6 +172,69 @@ async def test_gitea_pull_context_update_requires_author_assignment_and_matching
]
@pytest.mark.anyio
async def test_gitea_authored_unassigned_pull_has_workspace_access():
requests = []
async def handler(request):
requests.append((request.method, request.url.path))
if request.url.path == "/api/v1/user":
return httpx.Response(200, json={"login": "alex"})
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7":
return httpx.Response(200, json={
"number": 7,
"state": "open",
"merged": False,
"user": {"login": "alex"},
"assignees": [],
})
raise AssertionError(f"unexpected request: {request.method} {request.url.path}")
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
capabilities = await gitea_proxy.pull_workspace_capabilities("stackchain/api", 7)
finally:
await gitea_proxy.stop_client()
assert capabilities == {"authored": True, "assigned": False}
assert requests == [
("GET", "/api/v1/user"),
("GET", "/api/v1/repos/stackchain/api/pulls/7"),
]
@pytest.mark.anyio
async def test_authored_unassigned_pull_detail_returns_capabilities(monkeypatch):
async def assigned(repository, number):
return False
async def capabilities(repository, number):
assert (repository, number) == ("stackchain/api", 7)
return {"authored": True, "assigned": False}
async def detail(repository, number):
return {
"repository": repository,
"number": number,
"title": "Ship mobile flow",
"body": "Ready for review",
"author": "alex",
"head_sha": "abc1234",
"state": "open",
"conversation": {"comments": [], "page": 1, "older_page": None, "total": 0},
}
monkeypatch.setattr(main.gitea_proxy, "is_assigned_pull", assigned)
monkeypatch.setattr(main.gitea_proxy, "pull_workspace_capabilities", capabilities)
monkeypatch.setattr(main.gitea_proxy, "pull_completion_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/detail")
assert response.status_code == 200
assert response.json()["capabilities"] == {"authored": True, "assigned": False}
@pytest.mark.anyio
async def test_assigned_pull_detail_reports_completion_state(monkeypatch):
async def assigned(repository, number):
@@ -213,7 +276,11 @@ async def test_assigned_pull_detail_rejects_unassigned_pull(monkeypatch):
async def assigned(repository, number):
return False
async def capabilities(repository, number):
return {"authored": False, "assigned": False}
monkeypatch.setattr(main.gitea_proxy, "is_assigned_pull", assigned, raising=False)
monkeypatch.setattr(main.gitea_proxy, "pull_workspace_capabilities", capabilities)
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/detail")
@@ -1208,7 +1275,7 @@ async def test_pull_handoff_candidates_are_bounded_and_exclude_invalid_or_curren
@pytest.mark.anyio
async def test_gitea_pull_review_request_filters_candidates_and_confirms_requested_reviewer():
async def test_gitea_authored_unassigned_pull_review_request_filters_candidates_and_confirms_reviewer():
requests = []
review_requested = False
@@ -1230,8 +1297,8 @@ async def test_gitea_pull_review_request_filters_candidates_and_confirms_request
requested.append({"login": "casey"})
return httpx.Response(200, json={
"number": 7, "state": "open", "merged": False,
"head": {"sha": "abc123"}, "user": {"login": "alex"},
"assignees": [{"login": "timmy"}],
"head": {"sha": "abc123"}, "user": {"login": "timmy"},
"assignees": [],
"requested_reviewers": requested,
})
if request.method == "POST" and request.url.path.endswith("/requested_reviewers"):
@@ -1248,7 +1315,10 @@ async def test_gitea_pull_review_request_filters_candidates_and_confirms_request
finally:
await gitea_proxy.stop_client()
assert candidates == [{"login": "casey", "name": "Casey"}]
assert candidates == [
{"login": "alex", "name": "Alexander"},
{"login": "casey", "name": "Casey"},
]
assert result == {
"repository": "stackchain/api", "number": 7, "head_sha": "abc123",
"requested_reviewers": ["sam", "casey"], "reviewer": "casey",
@@ -1259,7 +1329,7 @@ async def test_gitea_pull_review_request_filters_candidates_and_confirms_request
@pytest.mark.anyio
async def test_gitea_cancel_pending_pull_review_confirms_authoritative_removal():
async def test_gitea_authored_unassigned_pull_can_cancel_pending_review():
requests = []
cancelled = False
@@ -1271,7 +1341,7 @@ async def test_gitea_cancel_pending_pull_review_confirms_authoritative_removal()
if request.url.path.endswith("/pulls/7"):
return httpx.Response(200, json={
"number": 7, "state": "open", "merged": False,
"head": {"sha": "abc123"}, "assignees": [{"login": "timmy"}],
"head": {"sha": "abc123"}, "user": {"login": "timmy"}, "assignees": [],
"requested_reviewers": [{"login": "casey"}] if cancelled else [{"login": "sam"}, {"login": "casey"}],
})
if request.method == "DELETE" and request.url.path.endswith("/requested_reviewers"):
+30
View File
@@ -107,6 +107,36 @@ async def test_resolve_work_route_returns_only_a_requested_review():
}
@pytest.mark.anyio
async def test_resolve_work_route_returns_an_open_authored_unassigned_pull():
async def upstream(request):
if request.url.path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
return httpx.Response(200, json={
"id": 703,
"number": 90,
"title": "Ship authored flow",
"state": "open",
"html_url": gitea_proxy.GITEA_URL + "/stackchain/dashboard/pulls/90",
"assignees": [],
"requested_reviewers": [],
"user": {"login": "timmy"},
})
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
try:
result = await gitea_proxy.resolve_work_route(
"pull", "stackchain/dashboard", 90, None
)
finally:
await gitea_proxy.stop_client()
assert result["kind"] == "pull"
assert result["number"] == 90
assert result["is_assigned"] is False
assert result["work_reasons"] == ["authored_by_me"]
@pytest.mark.anyio
async def test_resolve_work_route_returns_an_open_issue_filed_by_the_current_user():
requests = []