feat: reprioritize assigned issues from mobile (#173)
This commit is contained in:
1 parent
b673911e36
commit
390ad0d4d2
7 files changed
+418
No files matched your search
@@ -479,3 +479,107 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_update_assigned_issue_labels_validates_and_returns_confirmed_labels(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def assigned(repository, number):
|
||||
return (repository, number) == ("stackchain/api", 7)
|
||||
|
||||
async def labels(repository):
|
||||
assert repository == "stackchain/api"
|
||||
return [
|
||||
{"id": 3, "name": "P0", "color": "d73a4a"},
|
||||
{"id": 8, "name": "frontend", "color": "1d76db"},
|
||||
]
|
||||
|
||||
async def update(repository, number, label_ids):
|
||||
calls.append((repository, number, label_ids))
|
||||
return {"number": number, "labels": ["P0", "frontend"]}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
||||
monkeypatch.setattr(main.gitea_proxy, "repo_labels", labels)
|
||||
monkeypatch.setattr(main.gitea_proxy, "update_issue_labels", update, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.patch(
|
||||
"/api/v1/repos/stackchain/api/issues/7/labels",
|
||||
json={"label_ids": [3, 8]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json() == {"number": 7, "labels": ["P0", "frontend"]}
|
||||
assert calls == [("stackchain/api", 7, [3, 8])]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_assigned_issue_label_options_do_not_depend_on_repository_listing(monkeypatch):
|
||||
calls = []
|
||||
|
||||
async def assigned(repository, number):
|
||||
calls.append(("assigned", repository, number))
|
||||
return True
|
||||
|
||||
async def labels(repository):
|
||||
calls.append(("labels", repository))
|
||||
return [{"id": 3, "name": "P0", "color": "d73a4a", "description": "Urgent"}]
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
||||
monkeypatch.setattr(main.gitea_proxy, "repo_labels", labels)
|
||||
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/issues/7/labels")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json()[0]["name"] == "P0"
|
||||
assert calls == [
|
||||
("assigned", "stackchain/api", 7),
|
||||
("labels", "stackchain/api"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_update_issue_labels_patches_ids_and_normalizes_confirmation():
|
||||
requests = []
|
||||
|
||||
async def handler(request):
|
||||
requests.append(request)
|
||||
return httpx.Response(200, json={
|
||||
"number": 7,
|
||||
"labels": [
|
||||
{"id": 3, "name": "P0"},
|
||||
{"id": 8, "name": "frontend"},
|
||||
],
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.update_issue_labels("stackchain/api", 7, [3, 8])
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert len(requests) == 1
|
||||
assert requests[0].method == "PATCH"
|
||||
assert requests[0].url.path == "/api/v1/repos/stackchain/api/issues/7"
|
||||
assert requests[0].content == b'{"labels":[3,8]}'
|
||||
assert result == {"number": 7, "labels": ["P0", "frontend"]}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_gitea_update_issue_labels_rejects_unconfirmed_label_set():
|
||||
async def handler(_request):
|
||||
return httpx.Response(200, json={
|
||||
"number": 7,
|
||||
"labels": [{"id": 8, "name": "frontend"}],
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
with pytest.raises(ValueError, match="confirm"):
|
||||
await gitea_proxy.update_issue_labels("stackchain/api", 7, [3])
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
@@ -77,6 +77,38 @@ process.stdout.write(JSON.stringify(queue));
|
||||
assert queue[2]["reason"] == "Assigned to you"
|
||||
|
||||
|
||||
def test_confirmed_issue_labels_replace_snapshot_and_reprioritize_queue():
|
||||
payload = {
|
||||
"user": {"login": "timmy"},
|
||||
"issues": [
|
||||
{"number": 1, "title": "Older", "repository": "stackchain/api",
|
||||
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T10:00:00Z"},
|
||||
{"number": 2, "title": "Newer", "repository": "stackchain/api",
|
||||
"labels": [], "assignees": ["timmy"], "updated_at": "2026-08-06T12:00:00Z"},
|
||||
],
|
||||
"pull_requests": [],
|
||||
}
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
const original = {json.dumps(payload)};
|
||||
const updated = buildMyWork.replaceIssueLabels(original, 'stackchain/api', 1, ['P0']);
|
||||
process.stdout.write(JSON.stringify({{
|
||||
titles: buildMyWork(updated).map(item => item.title),
|
||||
labels: updated.issues[0].labels,
|
||||
original: original.issues[0].labels,
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"titles": ["Older", "Newer"],
|
||||
"labels": ["P0"],
|
||||
"original": [],
|
||||
}
|
||||
|
||||
|
||||
def test_my_work_reviews_filter_and_summary_are_actionable():
|
||||
items = [
|
||||
{"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True},
|
||||
@@ -746,6 +778,30 @@ controller.load({{repository:'stackchain/api', number:7}}).then(detail =>
|
||||
}
|
||||
|
||||
|
||||
def test_issue_sheet_loads_labels_through_assigned_issue_boundary():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
let request;
|
||||
const controller = createIssueSheet({{ fetchJson: async (url, options) => {{
|
||||
request = {{url, accept:options.headers.Accept}};
|
||||
return [{{id:3, name:'P0'}}];
|
||||
}} }});
|
||||
controller.loadLabels({{repository:'stackchain/api', number:7}}).then(labels =>
|
||||
process.stdout.write(JSON.stringify({{request, labels}}))
|
||||
);
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
assert json.loads(result.stdout) == {
|
||||
"request": {
|
||||
"url": "api/v1/repos/stackchain/api/issues/7/labels",
|
||||
"accept": "application/json",
|
||||
},
|
||||
"labels": [{"id": 3, "name": "P0"}],
|
||||
}
|
||||
|
||||
|
||||
def test_issue_sheet_comment_is_single_flight_and_preserves_draft_until_success():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
@@ -785,6 +841,51 @@ Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.string
|
||||
assert output["results"] == [{"id": 82, "body": "Ready"}, {"id": 82, "body": "Ready"}]
|
||||
|
||||
|
||||
def test_issue_sheet_label_save_is_single_flight_and_keeps_selection_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),
|
||||
}};
|
||||
const calls = [];
|
||||
let release;
|
||||
const controller = createIssueSheet({{
|
||||
storage,
|
||||
fetchJson: (url, options={{}}) => {{
|
||||
calls.push({{url, method:options.method || 'GET', body:options.body ? JSON.parse(options.body) : null}});
|
||||
return new Promise(resolve => {{ release = () => resolve({{number:7, labels:['P0']}}); }});
|
||||
}},
|
||||
}});
|
||||
const item = {{repository:'stackchain/api', number:7}};
|
||||
const first = controller.updateLabels(item, [3]);
|
||||
const duplicate = controller.updateLabels(item, [3]);
|
||||
const during = controller.loadLabelDraft(item);
|
||||
release();
|
||||
Promise.all([first, duplicate]).then(results => process.stdout.write(JSON.stringify({{
|
||||
calls, during, after:controller.loadLabelDraft(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/7/labels",
|
||||
"method": "PATCH",
|
||||
"body": {"label_ids": [3]},
|
||||
}]
|
||||
assert output["during"] == [3]
|
||||
assert output["after"] == []
|
||||
assert output["results"] == [
|
||||
{"number": 7, "labels": ["P0"]},
|
||||
{"number": 7, "labels": ["P0"]},
|
||||
]
|
||||
|
||||
|
||||
def test_issue_sheet_close_is_single_flight_and_waits_for_confirmed_closed_state():
|
||||
script = f"""
|
||||
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||
@@ -920,6 +1021,21 @@ async def test_assigned_issues_open_accessible_mobile_action_sheet_with_safe_mut
|
||||
assert "e.key === 'Escape' && selectedIssue" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_issue_sheet_edits_labels_and_repaints_confirmed_priority():
|
||||
html = await dashboard()
|
||||
|
||||
assert 'id="issue-label-editor"' in html
|
||||
assert 'aria-describedby="issue-label-status"' in html
|
||||
assert 'id="issue-label-list"' in html
|
||||
assert 'id="save-issue-labels"' in html
|
||||
assert '.issue-label-option { min-height:44px;' in html
|
||||
assert 'max-width:100%;' in html
|
||||
assert "issueController.updateLabels(selectedIssue" in html
|
||||
assert "buildMyWork.replaceIssueLabels" in html
|
||||
assert "paintMyWork(lastContextSnapshot)" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet():
|
||||
html = await dashboard()
|
||||
|
||||
Reference in new issue
Block a user