feat: schedule assigned issues from mobile (#191)
This commit is contained in:
1 parent
5406ee2297
commit
c639b09aaf
11 files changed
+378
-6
No files matched your search
@@ -421,6 +421,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work(
|
||||
"assignees": [{"login": "timmy"}],
|
||||
"repository": {"full_name": "stackchain/mobile"},
|
||||
"updated_at": "2026-08-06T12:00:00Z",
|
||||
"due_date": "2026-08-09T23:59:59Z",
|
||||
"html_url": "https://forge.example/stackchain/mobile/issues/7",
|
||||
}]
|
||||
|
||||
@@ -448,6 +449,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work(
|
||||
|
||||
assert payload["issues"][0]["repository"] == "stackchain/mobile"
|
||||
assert payload["issues"][0]["updated_at"] == "2026-08-06T12:00:00Z"
|
||||
assert payload["issues"][0]["due_date"] == "2026-08-09T23:59:59Z"
|
||||
assert payload["pull_requests"][0]["repository"] == "stackchain/api"
|
||||
assert payload["pull_requests"][0]["updated_at"] == "2026-08-06T11:00:00Z"
|
||||
assert payload["pull_requests"][0]["labels"] == ["priority-high"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -6,6 +7,87 @@ import pytest
|
||||
from src import gitea_proxy, main
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("due_date", "expected_payload", "confirmed_due_date"),
|
||||
[
|
||||
("2026-08-09T23:59:59Z", {"due_date": "2026-08-09T23:59:59Z"}, "2026-08-09T23:59:59Z"),
|
||||
(None, {"unset_due_date": True}, None),
|
||||
],
|
||||
)
|
||||
async def test_gitea_due_date_update_revalidates_assignment_and_confirms_result(
|
||||
due_date, expected_payload, confirmed_due_date
|
||||
):
|
||||
requests = []
|
||||
|
||||
async def handler(request):
|
||||
requests.append(request)
|
||||
if request.url.path == "/api/v1/user":
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
if request.method == "GET":
|
||||
return httpx.Response(200, json={
|
||||
"number": 17, "state": "open", "pull_request": None,
|
||||
"assignees": [{"login": "timmy"}],
|
||||
})
|
||||
return httpx.Response(200, json={
|
||||
"number": 17, "state": "open", "due_date": confirmed_due_date,
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.update_assigned_issue_due_date(
|
||||
"stackchain/api", 17, due_date
|
||||
)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert [(request.method, request.url.path) for request in requests] == [
|
||||
("GET", "/api/v1/user"),
|
||||
("GET", "/api/v1/repos/stackchain/api/issues/17"),
|
||||
("PATCH", "/api/v1/repos/stackchain/api/issues/17"),
|
||||
]
|
||||
assert json.loads(requests[2].content) == expected_payload
|
||||
assert result == {
|
||||
"repository": "stackchain/api", "number": 17,
|
||||
"state": "open", "due_date": confirmed_due_date,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_due_date_endpoint_sets_or_clears_assigned_issue(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def update(repository, number, due_date):
|
||||
calls.append((repository, number, due_date))
|
||||
return {
|
||||
"repository": repository, "number": number, "state": "open",
|
||||
"due_date": due_date,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
main.gitea_proxy, "update_assigned_issue_due_date", update, raising=False
|
||||
)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
set_response = await client.patch(
|
||||
"/api/v1/repos/stackchain/api/issues/17/due-date",
|
||||
json={"due_date": "2026-08-09T23:59:59Z"},
|
||||
)
|
||||
clear_response = await client.patch(
|
||||
"/api/v1/repos/stackchain/api/issues/17/due-date",
|
||||
json={"due_date": None},
|
||||
)
|
||||
|
||||
assert [set_response.status_code, clear_response.status_code] == [200, 200]
|
||||
assert set_response.headers["cache-control"] == "no-store"
|
||||
assert set_response.json()["due_date"] == "2026-08-09T23:59:59Z"
|
||||
assert clear_response.json()["due_date"] is None
|
||||
assert calls == [
|
||||
("stackchain/api", 17, "2026-08-09T23:59:59Z"),
|
||||
("stackchain/api", 17, None),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_edit_assigned_issue_updates_title_and_body_at_expected_revision(monkeypatch):
|
||||
calls = []
|
||||
@@ -909,6 +991,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||
"state": "open",
|
||||
"body": "Full issue context",
|
||||
"updated_at": "2026-08-07T09:59:00Z",
|
||||
"due_date": None,
|
||||
"url": "https://forge.example/stackchain/api/issues/7",
|
||||
"labels": ["P1"],
|
||||
"assignees": ["timmy"],
|
||||
|
||||
@@ -78,6 +78,38 @@ process.stdout.write(JSON.stringify(queue));
|
||||
assert queue[2]["reason"] == "Assigned to you"
|
||||
|
||||
|
||||
def test_my_work_surfaces_and_ranks_due_issues_after_p0_before_ordinary_work():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
"issues": [
|
||||
{"number": 1, "title": "Ordinary", "repository": "stackchain/api",
|
||||
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-07T12:00:00Z"},
|
||||
{"number": 2, "title": "Due today", "repository": "stackchain/api",
|
||||
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-07T23:59:59Z"},
|
||||
{"number": 3, "title": "Overdue", "repository": "stackchain/api",
|
||||
"labels": [], "assignees": ["timmy"], "due_date": "2026-08-06T23:59:59Z"},
|
||||
{"number": 4, "title": "P0 future", "repository": "stackchain/api",
|
||||
"labels": ["P0"], "assignees": ["timmy"], "due_date": "2026-08-10T23:59:59Z"},
|
||||
],
|
||||
"pull_requests": [],
|
||||
}
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
const queue = buildMyWork({json.dumps(payload)}, new Date('2026-08-07T12:00:00Z'));
|
||||
process.stdout.write(JSON.stringify(queue.map(item => ({{title:item.title,reason:item.reason,due_label:item.due_label}}))));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == [
|
||||
{"title": "P0 future", "reason": "P0 priority", "due_label": "Due Aug 10"},
|
||||
{"title": "Overdue", "reason": "Overdue", "due_label": "Overdue"},
|
||||
{"title": "Due today", "reason": "Due today", "due_label": "Due today"},
|
||||
{"title": "Ordinary", "reason": "Assigned to you"},
|
||||
]
|
||||
|
||||
|
||||
def test_confirmed_issue_labels_replace_snapshot_and_reprioritize_queue():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
@@ -234,6 +266,61 @@ Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.string
|
||||
assert output["results"][0]["updated_at"] == "2026-08-07T10:01:00Z"
|
||||
|
||||
|
||||
def test_issue_due_date_update_is_single_flight_and_keeps_draft_until_confirmed():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.get(key) || null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let calls = [];
|
||||
let finish;
|
||||
const controller = createIssueSheet({{
|
||||
storage,
|
||||
fetchJson:(url, options) => {{
|
||||
calls.push({{url, options}});
|
||||
return new Promise(resolve => {{ finish = resolve; }});
|
||||
}},
|
||||
}});
|
||||
const item = {{repository:'stackchain/api', number:17}};
|
||||
const first = controller.updateDueDate(item, '2026-08-09T23:59:59Z');
|
||||
const duplicate = controller.updateDueDate(item, '2026-08-09T23:59:59Z');
|
||||
const during = controller.loadDueDateDraft(item);
|
||||
finish({{repository:'stackchain/api',number:17,state:'open',due_date:'2026-08-09T23:59:59Z'}});
|
||||
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
|
||||
calls:calls.map(call => ({{url:call.url,method:call.options.method,body:JSON.parse(call.options.body)}})),
|
||||
same:first === duplicate, during, after:controller.loadDueDateDraft(item), results
|
||||
}})));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
output = json.loads(result.stdout)
|
||||
assert output["calls"] == [{
|
||||
"url": "api/v1/repos/stackchain/api/issues/17/due-date",
|
||||
"method": "PATCH",
|
||||
"body": {"due_date": "2026-08-09T23:59:59Z"},
|
||||
}]
|
||||
assert output["same"] is True
|
||||
assert output["during"] == "2026-08-09T23:59:59Z"
|
||||
assert output["after"] is None
|
||||
assert output["results"][0]["due_date"] == "2026-08-09T23:59:59Z"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_issue_sheet_exposes_touch_sized_due_date_editor_and_card_badge():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="issue-due-date" type="date"' in html
|
||||
assert 'id="save-issue-due-date"' in html
|
||||
assert 'id="clear-issue-due-date"' in html
|
||||
assert 'class="pill due-badge"' in html
|
||||
assert '.issue-due-editor input, .issue-due-editor button { min-height:44px;' in html
|
||||
assert 'padding-bottom:calc(10px + env(safe-area-inset-bottom))' in html
|
||||
|
||||
|
||||
def test_my_work_reviews_filter_and_summary_are_actionable():
|
||||
items = [
|
||||
{"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True},
|
||||
|
||||
@@ -20,6 +20,7 @@ async def test_work_page_endpoint_normalizes_requested_page_and_is_not_cacheable
|
||||
"labels": [], "assignees": [{"login": "timmy"}],
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"updated_at": "2026-08-07T10:00:00Z",
|
||||
"due_date": "2026-08-09T23:59:59Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/51",
|
||||
}],
|
||||
}
|
||||
@@ -38,6 +39,7 @@ async def test_work_page_endpoint_normalizes_requested_page_and_is_not_cacheable
|
||||
"id": 51, "number": 51, "title": "Older issue", "state": "open",
|
||||
"labels": [], "assignees": ["timmy"], "repository": "stackchain/api",
|
||||
"updated_at": "2026-08-07T10:00:00Z",
|
||||
"due_date": "2026-08-09T23:59:59Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/51",
|
||||
}],
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user