diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index e97ccb0..2b69086 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -57,7 +57,7 @@ jobs:
pip install -r requirements-e2e.txt
python3 -m playwright install --with-deps chromium
- name: Exercise packaged mobile work journeys
- run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_search_week_plan.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_sign_out_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_today_summary_release.py tests/e2e/test_mobile_tomorrow_conflict_release.py tests/e2e/test_mobile_week_ahead_release.py tests/e2e/test_mobile_today_week_reschedule_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py tests/e2e/test_mobile_following_release.py tests/e2e/test_mobile_detail_watch_release.py tests/e2e/test_mobile_pull_reviewer_status_release.py tests/e2e/test_mobile_pull_reviewer_feedback_release.py -q tests/e2e/test_mobile_address_review_feedback_release.py tests/e2e/test_mobile_cancel_pull_review_request_release.py
+ run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_search_week_plan.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_sign_out_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_today_summary_release.py tests/e2e/test_mobile_tomorrow_conflict_release.py tests/e2e/test_mobile_week_ahead_release.py tests/e2e/test_mobile_today_week_reschedule_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py tests/e2e/test_mobile_following_release.py tests/e2e/test_mobile_detail_watch_release.py tests/e2e/test_mobile_pull_reviewer_status_release.py tests/e2e/test_mobile_pull_reviewer_feedback_release.py -q tests/e2e/test_mobile_address_review_feedback_release.py tests/e2e/test_mobile_cancel_pull_review_request_release.py tests/e2e/test_mobile_authored_pull_queue_release.py
release-candidate:
runs-on: ubuntu-latest
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index f8ee785..c0a5183 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -328,7 +328,7 @@
let launchFilterResolved = false;
try {
const savedFilter = sessionStorage.getItem(WORK_FILTER_KEY);
- if (['all', 'today', 'agenda', 'attention', 'filed', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)) {
+ if (['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)) {
selectedWorkFilter = savedFilter;
savedWorkFilter = savedFilter;
launchFilterResolved = true;
@@ -3498,10 +3498,10 @@
if (selectedWorkFilter === 'agenda') return ['issue'];
if (selectedWorkFilter === 'attention') return ['issue', 'pull', 'review'];
if (selectedWorkFilter === 'filed') return ['filed'];
- if (selectedWorkFilter === 'issue') return ['issue'];
- if (selectedWorkFilter === 'pull') return ['pull'];
- if (selectedWorkFilter === 'review') return ['review'];
- if (selectedWorkFilter === 'all') return ['issue', 'filed', 'pull', 'review'];
+ if (['issue', 'pull', 'review', 'authored'].includes(selectedWorkFilter)) {
+ return [selectedWorkFilter];
+ }
+ if (selectedWorkFilter === 'all') return ['issue', 'filed', 'pull', 'review', 'authored'];
return [];
}
@@ -3747,7 +3747,7 @@
}
function updateWorkPaginationControls() {
- const labels = { issue: 'issues', filed: 'filed issues', pull: 'pull requests', review: 'review requests' };
+ const labels = { issue: 'issues', filed: 'filed issues', pull: 'pull requests', review: 'review requests', authored: 'pull requests' };
const streams = activeWorkStreams();
const incomplete = streams.filter(stream => workPagination[stream]?.has_more);
const summaries = streams.flatMap(stream => {
diff --git a/frontend/index.html b/frontend/index.html
index 95bbf9c..6c3eb20 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -197,6 +197,7 @@
+
@@ -2085,6 +2086,7 @@
+
diff --git a/frontend/my-work.js b/frontend/my-work.js
index bee668c..88dec86 100644
--- a/frontend/my-work.js
+++ b/frontend/my-work.js
@@ -71,6 +71,7 @@ function buildMyWork(data, now = new Date()) {
const assigned = (item.assignees || []).includes(login);
const isReview = (item.work_reasons || []).includes('review_requested');
const isFiled = (item.work_reasons || []).includes('created_by_me');
+ const isAuthored = (item.work_reasons || []).includes('authored_by_me');
const isCompleted = isFiled && item.state === 'closed';
const due = item.kind === 'issue' ? issueDueState(item.due_date, now) : null;
const normalized = {
@@ -78,6 +79,7 @@ function buildMyWork(data, now = new Date()) {
key: (item.repository || 'unknown') + '#' + item.number,
is_review: isReview,
is_filed: isFiled,
+ is_authored: isAuthored,
is_completed: isCompleted,
is_assigned: assigned,
has_update: false,
@@ -85,7 +87,8 @@ function buildMyWork(data, now = new Date()) {
reason: priorityLabel ? priorityLabel + ' priority' :
(due && due.priority < 4 ? due.label :
(isReview ? 'Needs your review' : (isCompleted ? 'Completed · review outcome' :
- (assigned ? 'Assigned to you' : (isFiled ? 'Filed by you' : 'Open work'))))),
+ (assigned ? 'Assigned to you' : (isFiled ? 'Filed by you' :
+ (isAuthored ? 'Authored by you' : 'Open work')))))),
_priority: priorityLabel ? 0 :
(due && due.priority < 4 ? due.priority : (isReview ? 3 : (isCompleted ? 3.5 : (assigned ? 4 : 5)))),
};
@@ -657,6 +660,7 @@ function filterMyWork(items, selectedFilter, selectedMilestone = 'all') {
let filtered = items;
if (selectedFilter === 'attention') filtered = items.filter(needsAttention);
else if (selectedFilter === 'filed') filtered = items.filter((item) => item.is_filed);
+ else if (selectedFilter === 'authored') filtered = items.filter((item) => item.is_authored);
else if (selectedFilter === 'review') filtered = items.filter((item) => item.is_review);
else if (selectedFilter === 'update') filtered = items.filter((item) => item.has_update);
else if (selectedFilter !== 'all') filtered = items.filter((item) => item.kind === selectedFilter);
@@ -1080,6 +1084,7 @@ function countMyWork(items) {
all: items.length,
attention: items.filter(needsAttention).length,
filed: items.filter((item) => item.is_filed).length,
+ authored: items.filter((item) => item.is_authored).length,
issue: items.filter((item) => item.kind === 'issue').length,
pull: items.filter((item) => item.kind === 'pull' && !item.is_review).length,
review: items.filter((item) => item.is_review).length,
diff --git a/frontend/work-route.js b/frontend/work-route.js
index b6873c8..836dcf7 100644
--- a/frontend/work-route.js
+++ b/frontend/work-route.js
@@ -6,7 +6,7 @@
'use strict';
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
- const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft', 'following'];
+ const queueFilters = ['today', 'agenda', 'attention', 'filed', 'authored', 'update', 'later', 'draft', 'following'];
const sections = {
issue: ['overview', 'conversation', 'reply', 'actions'],
filed: ['overview', 'conversation', 'reply', 'actions'],
diff --git a/src/gitea_proxy.py b/src/gitea_proxy.py
index 4bed2fb..22c87ea 100644
--- a/src/gitea_proxy.py
+++ b/src/gitea_proxy.py
@@ -370,6 +370,7 @@ WORK_SEARCHES = {
"filed": ("created=true", "issues", "created_by_me"),
"pull": ("assigned=true", "pulls", "assigned_to_me"),
"review": ("review_requested=true", "pulls", "review_requested"),
+ "authored": ("created=true", "pulls", "authored_by_me"),
}
@@ -2768,29 +2769,34 @@ async def is_open_authored_issue(repository: str, number: int) -> bool:
async def pull_requests() -> WorkItems:
- assigned, review_requested = await asyncio.gather(
- work_page("pull"),
- work_page("review"),
+ streams = ("pull", "review", "authored")
+ outcomes = await asyncio.gather(
+ *(work_page(stream) for stream in streams),
+ return_exceptions=True,
)
+ for outcome in outcomes:
+ if isinstance(outcome, asyncio.CancelledError):
+ raise outcome
+ if all(isinstance(outcome, BaseException) for outcome in outcomes):
+ raise outcomes[0]
+
merged: dict[int, dict] = {}
- for result in (
- assigned,
- review_requested,
- ):
- for pull in result["items"]:
+ pagination = {}
+ for stream, outcome in zip(streams, outcomes, strict=True):
+ if isinstance(outcome, BaseException):
+ pagination[stream] = {
+ "page": 1, "total": 0, "has_more": False, "unavailable": True,
+ }
+ continue
+ pagination[stream] = _page_metadata(outcome)
+ for pull in outcome["items"]:
identity = pull.get("id")
if identity not in merged:
merged[identity] = {**pull, "work_reasons": []}
for reason in pull.get("work_reasons", []):
if reason not in merged[identity]["work_reasons"]:
merged[identity]["work_reasons"].append(reason)
- return WorkItems(
- list(merged.values()),
- {
- "pull": _page_metadata(assigned),
- "review": _page_metadata(review_requested),
- },
- )
+ return WorkItems(list(merged.values()), pagination)
async def is_requested_review(repository: str, number: int) -> bool:
diff --git a/src/main.py b/src/main.py
index 7803775..3254025 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1329,7 +1329,7 @@ def _context_payload(user_data, repo_data, issues_data, prs_data) -> dict:
user=p["user"].get("login", "") if isinstance(p.get("user"), dict) else "",
labels=[label.get("name", "") for label in (p.get("labels") or []) if isinstance(label, dict)],
assignees=[assignee.get("login", "") for assignee in (p.get("assignees") or []) if isinstance(assignee, dict)],
- work_reasons=[reason for reason in (p.get("work_reasons") or []) if reason in ("assigned_to_me", "review_requested")],
+ work_reasons=[reason for reason in (p.get("work_reasons") or []) if reason in ("assigned_to_me", "review_requested", "authored_by_me")],
repository=p["repository"].get("full_name", "") if isinstance(p.get("repository"), dict) else "",
updated_at=p.get("updated_at") or "", url=p["html_url"],
)
@@ -1370,7 +1370,11 @@ def _normalize_work_items(stream: str, items: list[dict]) -> list[dict]:
for item in items
if all(field in item for field in ("id", "number", "title", "state", "html_url"))
]
- reason = "assigned_to_me" if stream == "pull" else "review_requested"
+ reason = {
+ "pull": "assigned_to_me",
+ "review": "review_requested",
+ "authored": "authored_by_me",
+ }[stream]
return [
PullRequest(
id=item["id"], number=item["number"], title=item["title"],
@@ -4156,7 +4160,7 @@ async def resolve_work_route(
@app.get("/api/v1/work/{stream}")
async def paged_work(
- stream: Literal["issue", "filed", "pull", "review"],
+ stream: Literal["issue", "filed", "pull", "review", "authored"],
page: int = Query(ge=2, le=100),
) -> JSONResponse:
try:
diff --git a/tests/e2e/test_mobile_authored_pull_queue_release.py b/tests/e2e/test_mobile_authored_pull_queue_release.py
new file mode 100644
index 0000000..508cf0c
--- /dev/null
+++ b/tests/e2e/test_mobile_authored_pull_queue_release.py
@@ -0,0 +1,89 @@
+import os
+from pathlib import Path
+
+import pytest
+
+if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
+ pytest.skip("packaged mobile My PRs journey runs only in its gated CI job", allow_module_level=True)
+pytest.importorskip("playwright.sync_api")
+from playwright.sync_api import expect, sync_playwright
+
+
+ROOT = Path(__file__).parents[2]
+FRONTEND = ROOT / "frontend"
+
+
+@pytest.mark.parametrize("viewport", [
+ {"width": 320, "height": 568},
+ {"width": 390, "height": 844},
+])
+def test_authored_pull_queue_is_phone_usable_and_opens_the_existing_pull_route(viewport):
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch(headless=True)
+ page = browser.new_page(viewport=viewport)
+ page.set_content((FRONTEND / "index.html").read_text())
+ page.add_style_tag(path=FRONTEND / "dashboard.css")
+ 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.evaluate("document.querySelector('#mobile-queue-sheet').showModal()")
+
+ row = page.locator('[data-mobile-queue="authored"]')
+ expect(row).to_have_count(1)
+ expect(row).to_contain_text("My PRs")
+ expect(row).to_contain_text("Pull requests you authored")
+ assert row.bounding_box()["height"] >= 44
+
+ result = page.evaluate("""() => {
+ const work = buildMyWork({
+ user:{login:'timmy'},
+ pull_requests:[{
+ number:41,title:'Ship the authored mobile flow',repository:'stackchain/dashboard',
+ state:'open',user:'timmy',assignees:[],work_reasons:['authored_by_me'],
+ url:'https://forge.example/git/stackchain/dashboard/pulls/41'
+ }]
+ });
+ const authored = filterMyWork(work, 'authored');
+ let selected = '';
+ let route = '';
+ const action = document.createElement('button');
+ action.className = 'my-work-card-main';
+ action.addEventListener('click', () => { route = createWorkRoute.serialize(authored[0]); });
+ const launcher = createMobileQueueLauncher({
+ selectFilter:name => { selected = name; },
+ firstAction:() => action,
+ announce:() => {},
+ });
+ const opened = launcher.open('authored');
+ return {selected, route, opened, count:authored.length};
+ }""")
+ assert result == {
+ "selected": "authored",
+ "route": "#/my-work/pull/stackchain/dashboard/41",
+ "opened": "opened",
+ "count": 1,
+ }
+ assert page.evaluate(
+ "document.documentElement.scrollWidth > document.documentElement.clientWidth"
+ ) is False
+ browser.close()
+
+
+def test_empty_authored_queue_announces_the_standard_empty_message():
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch(headless=True)
+ page = browser.new_page(viewport={"width": 320, "height": 568})
+ page.set_content((FRONTEND / "index.html").read_text())
+ page.add_script_tag(path=FRONTEND / "mobile-queue-launcher.js")
+
+ message = page.evaluate("""() => {
+ let announcement = '';
+ const launcher = createMobileQueueLauncher({
+ selectFilter:() => {}, firstAction:() => null,
+ announce:value => { announcement = value; },
+ });
+ launcher.open('authored');
+ return announcement;
+ }""")
+ assert message == "No work is ready to open."
+ browser.close()
diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py
index ec6c6b7..732e637 100644
--- a/tests/test_ci_workflow.py
+++ b/tests/test_ci_workflow.py
@@ -93,3 +93,10 @@ def test_browser_job_gates_latest_pull_review_mobile_journeys():
assert "tests/e2e/test_mobile_address_review_feedback_release.py" in browser
assert "tests/e2e/test_mobile_cancel_pull_review_request_release.py" in browser
+
+
+def test_browser_job_gates_authored_pull_queue_journey():
+ text = WORKFLOW.read_text()
+ browser = text[text.index(" browser-journey:") : text.index(" release-candidate:")]
+
+ assert "tests/e2e/test_mobile_authored_pull_queue_release.py" in browser
diff --git a/tests/test_gitea_work_search.py b/tests/test_gitea_work_search.py
index 484235f..331143f 100644
--- a/tests/test_gitea_work_search.py
+++ b/tests/test_gitea_work_search.py
@@ -100,6 +100,32 @@ async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_dist
}
+@pytest.mark.anyio
+async def test_authored_pull_page_loads_open_created_pulls_with_a_distinct_reason():
+ requests = []
+
+ def upstream(request):
+ requests.append(str(request.url))
+ return httpx.Response(200, headers={"X-Total-Count": "3"}, json=[{
+ "id": 901, "number": 41, "title": "Ship mobile flow", "state": "open",
+ "user": {"login": "timmy"},
+ "repository": {"full_name": "stackchain/dashboard"},
+ "html_url": "http://127.0.0.1:3000/stackchain/dashboard/pulls/41",
+ }])
+
+ gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
+ try:
+ result = await gitea_proxy.work_page("authored", page=1)
+ finally:
+ await gitea_proxy.stop_client()
+
+ assert requests == [
+ "http://127.0.0.1:3000/api/v1/repos/issues/search?state=open&created=true&type=pulls&limit=50&page=1"
+ ]
+ assert result["total"] == 3
+ assert result["items"][0]["work_reasons"] == ["authored_by_me"]
+
+
@pytest.mark.anyio
async def test_issue_context_merges_self_assigned_and_filed_streams_without_duplicates(monkeypatch):
async def page(stream, page=1, limit=50):
@@ -628,7 +654,7 @@ async def test_confirmed_claim_is_removed_from_retained_available_snapshot(monke
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, "filed": 62, "pull": 61, "review": 73}
+ totals = {"issue": 84, "filed": 62, "pull": 61, "review": 73, "authored": 12}
return {
"items": [], "page": 1, "total": totals[stream],
"has_more": True, "stream": stream,
@@ -646,6 +672,7 @@ async def test_initial_work_collections_expose_independent_pagination(monkeypatc
assert pulls.pagination == {
"pull": {"page": 1, "total": 61, "has_more": True},
"review": {"page": 1, "total": 73, "has_more": True},
+ "authored": {"page": 1, "total": 12, "has_more": True},
}
@@ -666,11 +693,12 @@ async def test_work_collections_include_supported_review_request_search(monkeypa
("filed", 1, 50),
("pull", 1, 50),
("review", 1, 50),
+ ("authored", 1, 50),
]
@pytest.mark.anyio
-async def test_pull_requests_merge_assignment_and_review_responsibilities(monkeypatch):
+async def test_pull_requests_merge_assignment_review_and_authored_responsibilities(monkeypatch):
assigned = {
"id": 11,
"number": 7,
@@ -683,10 +711,24 @@ async def test_pull_requests_merge_assignment_and_review_responsibilities(monkey
"title": "Review mobile",
"repository": {"full_name": "stackchain/mobile"},
}
+ authored_only = {
+ "id": 13,
+ "number": 9,
+ "title": "Ship authored flow",
+ "repository": {"full_name": "stackchain/dashboard"},
+ }
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"
+ items = {
+ "pull": [assigned],
+ "review": [assigned.copy(), review_only],
+ "authored": [assigned.copy(), authored_only],
+ }[stream]
+ reason = {
+ "pull": "assigned_to_me",
+ "review": "review_requested",
+ "authored": "authored_by_me",
+ }[stream]
return {
"stream": stream,
"items": [{**item, "work_reasons": [reason]} for item in items],
@@ -697,9 +739,44 @@ async def test_pull_requests_merge_assignment_and_review_responsibilities(monkey
pulls = await gitea_proxy.pull_requests()
- assert [pull["id"] for pull in pulls] == [11, 12]
- assert pulls[0]["work_reasons"] == ["assigned_to_me", "review_requested"]
+ assert [pull["id"] for pull in pulls] == [11, 12, 13]
+ assert pulls[0]["work_reasons"] == [
+ "assigned_to_me", "review_requested", "authored_by_me",
+ ]
assert pulls[1]["work_reasons"] == ["review_requested"]
+ assert pulls[2]["work_reasons"] == ["authored_by_me"]
+ assert pulls.pagination["authored"] == {
+ "page": 1, "total": 2, "has_more": False,
+ }
+
+
+@pytest.mark.anyio
+async def test_authored_pull_failure_keeps_other_pull_work_and_marks_only_that_stream_unavailable(monkeypatch):
+ async def fake_page(stream, page=1, limit=50):
+ if stream == "authored":
+ raise httpx.ConnectError("authored search unavailable")
+ item = {
+ "id": 11 if stream == "pull" else 12,
+ "number": 7 if stream == "pull" else 8,
+ "title": "Assigned" if stream == "pull" else "Review",
+ "repository": {"full_name": "stackchain/dashboard"},
+ "work_reasons": ["assigned_to_me" if stream == "pull" else "review_requested"],
+ }
+ return {
+ "stream": stream, "items": [item], "page": page,
+ "total": 1, "has_more": False,
+ }
+
+ monkeypatch.setattr(gitea_proxy, "work_page", fake_page)
+
+ pulls = await gitea_proxy.pull_requests()
+
+ assert [pull["id"] for pull in pulls] == [11, 12]
+ assert pulls.pagination["pull"].get("unavailable", False) is False
+ assert pulls.pagination["review"].get("unavailable", False) is False
+ assert pulls.pagination["authored"] == {
+ "page": 1, "total": 0, "has_more": False, "unavailable": True,
+ }
@pytest.mark.anyio
diff --git a/tests/test_my_work.py b/tests/test_my_work.py
index ef46665..0803b11 100644
--- a/tests/test_my_work.py
+++ b/tests/test_my_work.py
@@ -100,6 +100,53 @@ process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)})));
assert result[1]["reason"] == "Filed by you"
+def test_authored_pull_is_classified_for_the_my_prs_queue():
+ payload = {
+ "user": {"login": "timmy"},
+ "pull_requests": [{
+ "number": 41, "title": "Ship mobile flow", "repository": "stackchain/dashboard",
+ "state": "open", "user": "timmy", "assignees": [],
+ "work_reasons": ["authored_by_me"], "updated_at": "2026-08-24T12:00:00Z",
+ }],
+ }
+ script = f"""
+const buildMyWork = require({json.dumps(str(MY_WORK))});
+process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)})));
+"""
+ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert completed.returncode == 0, completed.stderr
+ result = json.loads(completed.stdout)
+ assert result[0]["is_authored"] is True
+ assert result[0]["reason"] == "Authored by you"
+ assert result[0]["kind"] == "pull"
+
+
+def test_my_prs_filter_and_count_include_only_authored_pulls():
+ script = f"""
+const buildMyWork = require({json.dumps(str(MY_WORK))});
+const items = buildMyWork({{
+ user:{{login:'timmy'}},
+ pull_requests:[
+ {{number:41,title:'Authored',repository:'stackchain/dashboard',state:'open',user:'timmy',
+ assignees:[],work_reasons:['authored_by_me']}},
+ {{number:42,title:'Review',repository:'stackchain/dashboard',state:'open',user:'alex',
+ assignees:[],work_reasons:['review_requested']}},
+ ],
+}});
+process.stdout.write(JSON.stringify({{
+ authored:buildMyWork.filterMyWork(items,'authored').map(item=>item.number),
+ counts:buildMyWork.countMyWork(items),
+}}));
+"""
+ completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert completed.returncode == 0, completed.stderr
+ result = json.loads(completed.stdout)
+ assert result["authored"] == [41]
+ assert result["counts"]["authored"] == 1
+
+
def test_completed_child_resolves_only_a_canonical_reciprocal_parent_task():
script = f"""
const sheet = require({json.dumps(str(ISSUE_SHEET))});
@@ -1059,6 +1106,24 @@ async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_sta
assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html
+@pytest.mark.anyio
+async def test_my_prs_is_a_thumb_safe_mobile_queue_and_desktop_filter():
+ html = await dashboard()
+ markup = (Path(__file__).resolve().parents[1] / "frontend" / "index.html").read_text()
+ css = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.css").read_text()
+
+ assert 'data-work-filter="authored"' in markup
+ assert 'data-work-count="authored"' in markup
+ assert 'data-mobile-queue="authored"' in markup
+ assert 'data-mobile-queue-count="authored"' in markup
+ assert 'My PRs' in markup
+ assert ".mobile-queue-list button" in css and "min-height:56px" in css
+ assert "['all', 'today', 'agenda', 'attention', 'filed', 'authored'" in html
+ assert "['issue', 'pull', 'review', 'authored'].includes(selectedWorkFilter)" in html
+ assert "authored: 'pull requests'" in html
+
+
+
@pytest.mark.anyio
async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search():
html = await dashboard()
@@ -4988,7 +5053,8 @@ process.stdout.write(JSON.stringify(buildMyWork.countMyWork({json.dumps(items)})
)
assert json.loads(result.stdout) == {
- "all": 3, "attention": 1, "filed": 0, "issue": 1, "pull": 1, "review": 1, "update": 0
+ "all": 3, "attention": 1, "filed": 0, "authored": 0,
+ "issue": 1, "pull": 1, "review": 1, "update": 0
}
@@ -6165,7 +6231,8 @@ process.stdout.write(JSON.stringify({{
assert output["updates"][1]["kind"] == "update"
assert output["updates"][1]["update_reason"] == ""
assert output["counts"] == {
- "all": 2, "attention": 2, "filed": 0, "issue": 1, "pull": 0, "review": 0, "update": 2
+ "all": 2, "attention": 2, "filed": 0, "authored": 0,
+ "issue": 1, "pull": 0, "review": 0, "update": 2
}
assert output["summary"] == "2 unread updates · 0 reviews · 1 assigned"
@@ -8480,7 +8547,7 @@ async def test_mobile_filters_wrap_show_counts_and_persist_for_the_session():
assert 'data-work-count="review"' in html
assert 'data-work-count="update"' in html
assert 'data-work-count="later"' in html
- assert "['all', 'today', 'agenda', 'attention', 'filed', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)" in html
+ assert "['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)" in html
assert 'data-work-count="draft"' in html
assert 'sessionStorage.getItem(WORK_FILTER_KEY)' in html
assert 'sessionStorage.setItem(WORK_FILTER_KEY, selectedWorkFilter)' in html
diff --git a/tests/test_work_pages.py b/tests/test_work_pages.py
index 59e50a7..9ef537f 100644
--- a/tests/test_work_pages.py
+++ b/tests/test_work_pages.py
@@ -90,6 +90,47 @@ async def test_filed_work_page_endpoint_preserves_authored_reason(monkeypatch):
assert response.json()["items"][0]["work_reasons"] == ["created_by_me"]
+@pytest.mark.anyio
+async def test_authored_pull_page_endpoint_preserves_authorship_reason(monkeypatch):
+ async def page_loader(stream, page):
+ assert (stream, page) == ("authored", 2)
+ return {
+ "stream": stream, "page": page, "total": 51, "has_more": False,
+ "items": [{
+ "id": 901, "number": 41, "title": "Ship mobile flow", "state": "open",
+ "user": {"login": "timmy"}, "labels": [], "assignees": [],
+ "work_reasons": ["authored_by_me"],
+ "repository": {"full_name": "stackchain/dashboard"},
+ "html_url": "https://forge.example/pulls/41",
+ }],
+ }
+
+ 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/authored?page=2")
+
+ assert response.status_code == 200
+ assert response.json()["items"][0]["work_reasons"] == ["authored_by_me"]
+
+
+def test_initial_context_preserves_authored_pull_reason():
+ payload = main._context_payload(
+ {"id": 1, "login": "timmy"},
+ [],
+ [],
+ [{
+ "id": 901, "number": 41, "title": "Ship mobile flow", "state": "open",
+ "user": {"login": "timmy"}, "labels": [], "assignees": [],
+ "work_reasons": ["authored_by_me"],
+ "repository": {"full_name": "stackchain/dashboard"},
+ "html_url": "https://forge.example/pulls/41",
+ }],
+ )
+
+ assert payload["pull_requests"][0]["work_reasons"] == ["authored_by_me"]
+
+
@pytest.mark.anyio
async def test_pwa_assets_expose_root_scoped_share_target_without_caching_api_data():
transport = httpx.ASGITransport(app=main.app)