fix: keep Gitea links on configured forge (Closes #1072)
This commit is contained in:
parent
0472e88d91
commit
79f6cecab4
|
|
@ -289,6 +289,11 @@ export STACKCHAIN_PUSH_DB='/var/lib/stackchain-dashboard/push-subscriptions.sqli
|
|||
uvicorn src.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
`GITEA_URL` is also the trust boundary for every Gitea resource link returned
|
||||
to the browser. Configure the externally reachable scheme, host, and subpath
|
||||
(for example `https://forge.example.com/git`); cross-origin, downgraded, and
|
||||
same-host links outside that subpath are discarded.
|
||||
|
||||
For a systemd deployment, keep those values in a root-readable environment file
|
||||
(`chmod 600`), reference it with `EnvironmentFile=`, and keep secrets out of the unit
|
||||
command line and repository. Liveness alone does not prove that operators can use the
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ def _normalize_global_search_item(item: Any, kind: str) -> dict | None:
|
|||
return None
|
||||
repository = item.get("repository")
|
||||
repository = repository if isinstance(repository, dict) else {}
|
||||
url = _safe_web_url(item.get("html_url"))
|
||||
url = _safe_gitea_web_url(item.get("html_url"))
|
||||
if not (
|
||||
isinstance(item.get("number"), int)
|
||||
and isinstance(item.get("title"), str)
|
||||
|
|
@ -614,7 +614,7 @@ async def work_preview(repository: str, kind: str, number: int) -> dict:
|
|||
if isinstance(label, dict) and isinstance(label.get("name"), str)
|
||||
],
|
||||
"assignees": assignee_names,
|
||||
"url": _safe_web_url(issue.get("html_url")),
|
||||
"url": _safe_gitea_web_url(issue.get("html_url")),
|
||||
"claimable": actual_kind == "issue" and state == "open" and not assignee_names,
|
||||
"reopenable": actual_kind == "issue" and state == "closed",
|
||||
"assigned_to_me": bool(login and login in assignee_names),
|
||||
|
|
@ -650,7 +650,7 @@ def _normalize_available_issue(item: Any) -> dict | None:
|
|||
"assignees": [],
|
||||
"updated_at": item.get("updated_at", "")
|
||||
if isinstance(item.get("updated_at"), str) else "",
|
||||
"url": _safe_web_url(item.get("html_url")),
|
||||
"url": _safe_gitea_web_url(item.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -756,13 +756,6 @@ async def issues() -> WorkItems:
|
|||
)
|
||||
|
||||
|
||||
def _safe_web_url(value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
return ""
|
||||
parsed = urlsplit(value)
|
||||
return value if parsed.scheme in {"http", "https"} and parsed.netloc else ""
|
||||
|
||||
|
||||
def _safe_gitea_web_url(value: Any) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
return ""
|
||||
|
|
@ -819,8 +812,8 @@ def _normalize_notifications(threads: Any) -> list[dict]:
|
|||
subject = thread.get("subject")
|
||||
repository = repository if isinstance(repository, dict) else {}
|
||||
subject = subject if isinstance(subject, dict) else {}
|
||||
subject_url = _safe_web_url(subject.get("html_url"))
|
||||
latest_url = _safe_web_url(subject.get("latest_comment_html_url"))
|
||||
subject_url = _safe_gitea_web_url(subject.get("html_url"))
|
||||
latest_url = _safe_gitea_web_url(subject.get("latest_comment_html_url"))
|
||||
number_text = (
|
||||
urlsplit(subject_url).path.rstrip("/").rsplit("/", 1)[-1]
|
||||
if subject_url
|
||||
|
|
@ -1053,8 +1046,8 @@ async def notification_detail(thread_id: int) -> dict:
|
|||
comment = comment if isinstance(comment, dict) else {}
|
||||
user_value = comment.get("user")
|
||||
user = user_value if isinstance(user_value, dict) else {}
|
||||
subject_url = _safe_web_url(subject.get("html_url"))
|
||||
latest_url = _safe_web_url(comment.get("html_url")) or _safe_web_url(
|
||||
subject_url = _safe_gitea_web_url(subject.get("html_url"))
|
||||
latest_url = _safe_gitea_web_url(comment.get("html_url")) or _safe_gitea_web_url(
|
||||
subject.get("latest_comment_html_url")
|
||||
)
|
||||
assignee_values = subject_detail.get("assignees")
|
||||
|
|
@ -1236,7 +1229,7 @@ async def upload_notification_attachment(
|
|||
if not isinstance(attachment, dict):
|
||||
raise ValueError("Gitea attachment response was not an object")
|
||||
name = attachment.get("name")
|
||||
url = _safe_web_url(attachment.get("browser_download_url"))
|
||||
url = _safe_gitea_web_url(attachment.get("browser_download_url"))
|
||||
size = attachment.get("size")
|
||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||
raise ValueError("Gitea did not confirm the attachment")
|
||||
|
|
@ -1275,7 +1268,7 @@ def _normalize_issue_comment(comment: dict) -> dict:
|
|||
"created_at": comment.get("created_at", "")
|
||||
if isinstance(comment.get("created_at"), str)
|
||||
else "",
|
||||
"url": _safe_web_url(comment.get("html_url")),
|
||||
"url": _safe_gitea_web_url(comment.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1357,7 +1350,7 @@ async def upload_assigned_issue_attachment(
|
|||
if not isinstance(attachment, dict):
|
||||
raise ValueError("Gitea attachment response was not an object")
|
||||
name = attachment.get("name")
|
||||
url = _safe_web_url(attachment.get("browser_download_url"))
|
||||
url = _safe_gitea_web_url(attachment.get("browser_download_url"))
|
||||
size = attachment.get("size")
|
||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||
raise ValueError("Gitea did not confirm the attachment")
|
||||
|
|
@ -1383,7 +1376,7 @@ async def upload_preview_attachment(
|
|||
if not isinstance(attachment, dict):
|
||||
raise ValueError("Gitea attachment response was not an object")
|
||||
name = attachment.get("name")
|
||||
url = _safe_web_url(attachment.get("browser_download_url"))
|
||||
url = _safe_gitea_web_url(attachment.get("browser_download_url"))
|
||||
size = attachment.get("size")
|
||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||
raise ValueError("Gitea did not confirm the attachment")
|
||||
|
|
@ -1410,7 +1403,7 @@ async def upload_assigned_pull_attachment(
|
|||
if not isinstance(attachment, dict):
|
||||
raise ValueError("Gitea attachment response was not an object")
|
||||
name = attachment.get("name")
|
||||
url = _safe_web_url(attachment.get("browser_download_url"))
|
||||
url = _safe_gitea_web_url(attachment.get("browser_download_url"))
|
||||
size = attachment.get("size")
|
||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||
raise ValueError("Gitea did not confirm the attachment")
|
||||
|
|
@ -1586,7 +1579,7 @@ async def create_issue(
|
|||
"updated_at": issue.get("updated_at", "")
|
||||
if isinstance(issue.get("updated_at"), str)
|
||||
else "",
|
||||
"url": _safe_web_url(issue.get("html_url")),
|
||||
"url": _safe_gitea_web_url(issue.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1640,7 +1633,7 @@ async def claim_available_issue(repository: str, number: int) -> dict:
|
|||
"assignees": logins,
|
||||
"updated_at": confirmed.get("updated_at", "")
|
||||
if isinstance(confirmed.get("updated_at"), str) else "",
|
||||
"url": _safe_web_url(confirmed.get("html_url")),
|
||||
"url": _safe_gitea_web_url(confirmed.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1700,7 +1693,7 @@ async def reopen_issue(repository: str, number: int) -> dict:
|
|||
"assignees": logins,
|
||||
"updated_at": confirmed.get("updated_at", "")
|
||||
if isinstance(confirmed.get("updated_at"), str) else "",
|
||||
"url": _safe_web_url(confirmed.get("html_url")),
|
||||
"url": _safe_gitea_web_url(confirmed.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2256,7 +2249,7 @@ async def issue_detail(repository: str, number: int) -> dict:
|
|||
and isinstance(issue["milestone"].get("title"), str)
|
||||
else None
|
||||
),
|
||||
"url": _safe_web_url(issue.get("html_url")),
|
||||
"url": _safe_gitea_web_url(issue.get("html_url")),
|
||||
"labels": [
|
||||
label["name"] for label in labels
|
||||
if isinstance(label, dict) and isinstance(label.get("name"), str)
|
||||
|
|
@ -2297,7 +2290,7 @@ async def issue_dependencies(repository: str, number: int, limit: int = 20) -> l
|
|||
"number": dependency_number,
|
||||
"title": item.get("title", "") if isinstance(item.get("title"), str) else "",
|
||||
"state": "open",
|
||||
"url": _safe_web_url(item.get("html_url")),
|
||||
"url": _safe_gitea_web_url(item.get("html_url")),
|
||||
})
|
||||
return dependencies
|
||||
|
||||
|
|
@ -2425,7 +2418,7 @@ async def _update_issue_content(
|
|||
"body": body,
|
||||
"state": issue.get("state", "open"),
|
||||
"updated_at": issue.get("updated_at", ""),
|
||||
"url": _safe_web_url(issue.get("html_url")),
|
||||
"url": _safe_gitea_web_url(issue.get("html_url")),
|
||||
}
|
||||
raise IssueEditConflictError("issue changed upstream")
|
||||
|
||||
|
|
@ -2450,7 +2443,7 @@ async def _update_issue_content(
|
|||
"body": body,
|
||||
"state": confirmed.get("state", "open"),
|
||||
"updated_at": confirmed.get("updated_at", ""),
|
||||
"url": _safe_web_url(confirmed.get("html_url")),
|
||||
"url": _safe_gitea_web_url(confirmed.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2518,7 +2511,7 @@ async def resolve_work_route(
|
|||
"number": number,
|
||||
"title": target.get("title", "") if isinstance(target.get("title"), str) else "",
|
||||
"state": state,
|
||||
"url": _safe_web_url(target.get("html_url")),
|
||||
"url": _safe_gitea_web_url(target.get("html_url")),
|
||||
**({"is_review": True, "work_reasons": ["review_requested"]} if kind == "review" else {}),
|
||||
**({
|
||||
"is_filed": True,
|
||||
|
|
@ -2623,7 +2616,7 @@ async def pull_completion_detail(repository: str, number: int) -> dict:
|
|||
"number": number,
|
||||
"title": pull.get("title") if isinstance(pull.get("title"), str) else "",
|
||||
"body": pull.get("body") if isinstance(pull.get("body"), str) else "",
|
||||
"url": _safe_web_url(pull.get("html_url")),
|
||||
"url": _safe_gitea_web_url(pull.get("html_url")),
|
||||
"author": user.get("login") if isinstance(user.get("login"), str) else "",
|
||||
"head_sha": sha,
|
||||
"state": pull.get("state") if isinstance(pull.get("state"), str) else "",
|
||||
|
|
@ -2865,7 +2858,7 @@ async def submit_pull_review(
|
|||
return {
|
||||
"id": review.get("id"),
|
||||
"state": review.get("state") or "COMMENT",
|
||||
"url": _safe_web_url(review.get("html_url")),
|
||||
"url": _safe_gitea_web_url(review.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ async def test_notification_page_preserves_upstream_total_without_loading_other_
|
|||
"subject": {
|
||||
"title": "Retry failed deploy",
|
||||
"type": "Issue",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
},
|
||||
}],
|
||||
)
|
||||
|
|
@ -170,8 +170,8 @@ async def test_unread_notifications_are_normalized_for_mobile_handoff():
|
|||
"title": "Retry failed deploy",
|
||||
"type": "Issue",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7",
|
||||
"latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
"latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
},
|
||||
},
|
||||
{"id": 43, "repository": None, "subject": None},
|
||||
|
|
@ -193,8 +193,8 @@ async def test_unread_notifications_are_normalized_for_mobile_handoff():
|
|||
"title": "Retry failed deploy",
|
||||
"subject_type": "Issue",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"subject_url": "https://forge.example/stackchain/api/issues/7",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
"subject_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
|
|
@ -245,8 +245,8 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
|
|||
"state": "open",
|
||||
"url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7",
|
||||
"latest_comment_url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/comments/9",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7",
|
||||
"latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
"latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
},
|
||||
})
|
||||
if request.url.path.endswith("/issues/7"):
|
||||
|
|
@ -262,7 +262,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
|
|||
"body": "Logs point to the worker timeout.",
|
||||
"created_at": "2026-08-06T12:30:00Z",
|
||||
"user": {"login": "alexander"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
})
|
||||
if request.url.path.endswith("/issues/7/comments"):
|
||||
return httpx.Response(200, json=[{
|
||||
|
|
@ -270,7 +270,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
|
|||
"body": "Logs point to the worker timeout.",
|
||||
"created_at": "2026-08-06T12:30:00Z",
|
||||
"user": {"login": "alexander"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
}], headers={"X-Total-Count": "1"})
|
||||
return httpx.Response(404)
|
||||
|
||||
|
|
@ -292,13 +292,13 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
|
|||
"title": "Retry failed deploy",
|
||||
"subject_type": "Issue",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
"subject_body": "Deploy fails after **three** retries.",
|
||||
"latest_comment": {
|
||||
"author": "alexander",
|
||||
"body": "Logs point to the worker timeout.",
|
||||
"created_at": "2026-08-06T12:30:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||
},
|
||||
"issue": {"number": 7, "assignees": [], "claimable": True},
|
||||
"acknowledge_supported": True,
|
||||
|
|
@ -362,7 +362,7 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin
|
|||
"user": {"login": "timmy"},
|
||||
"body": "Please retry the worker.",
|
||||
"created_at": "2026-08-07T19:00:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-91",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
|
||||
|
|
@ -388,7 +388,7 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin
|
|||
"author": "timmy",
|
||||
"body": "Please retry the worker.",
|
||||
"created_at": "2026-08-07T19:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-91",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ async def test_work_page_preserves_total_and_reason_without_loading_other_pages(
|
|||
"title": "Older review",
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://forge.example/stackchain/api/pulls/51",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/pulls/51",
|
||||
}],
|
||||
)
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_dist
|
|||
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||
"updated_at": "2026-08-15T12:00:00Z",
|
||||
"repository": {"full_name": "stackchain/dashboard"},
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/878",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/dashboard/issues/878",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(upstream))
|
||||
|
|
@ -94,7 +94,7 @@ async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_dist
|
|||
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||
"updated_at": "2026-08-15T12:00:00Z",
|
||||
"repository": {"full_name": "stackchain/dashboard"},
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/878",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/dashboard/issues/878",
|
||||
"work_reasons": ["created_by_me"],
|
||||
}],
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ async def test_issue_context_merges_self_assigned_and_filed_streams_without_dupl
|
|||
del page, limit
|
||||
common = {
|
||||
"id": 7, "number": 7, "title": "My own filing", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"}, "html_url": "https://forge.example/issues/7",
|
||||
"repository": {"full_name": "stackchain/api"}, "html_url": "http://127.0.0.1:3000/issues/7",
|
||||
}
|
||||
if stream == "issue":
|
||||
return {"items": [{**common, "assignees": [{"login": "timmy"}]}], "page": 1, "total": 1, "has_more": False}
|
||||
|
|
@ -816,7 +816,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work(
|
|||
"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",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/mobile/issues/7",
|
||||
}]
|
||||
|
||||
async def pulls():
|
||||
|
|
@ -831,7 +831,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work(
|
|||
"work_reasons": ["assigned_to_me", "review_requested"],
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"updated_at": "2026-08-06T11:00:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/pulls/7",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/pulls/7",
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
|
|
@ -864,7 +864,7 @@ async def test_pull_review_detail_combines_pr_files_status_and_reviews(monkeypat
|
|||
return {
|
||||
"title": "Review API",
|
||||
"body": "Please check the retry flow.",
|
||||
"html_url": "https://forge.example/stackchain/api/pulls/7",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/pulls/7",
|
||||
"user": {"login": "alex"},
|
||||
"head": {"sha": "abc123"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ async def test_global_search_specific_type_uses_one_full_width_scoped_stream():
|
|||
"title": "Review mobile search",
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -161,7 +161,7 @@ async def test_global_search_endpoint_returns_bounded_normalized_results(monkeyp
|
|||
"number": 42,
|
||||
"title": "Repair mobile queue",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/api/issues/42",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
}],
|
||||
"partial": False,
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ async def test_global_search_endpoint_returns_bounded_normalized_results(monkeyp
|
|||
"number": 42,
|
||||
"title": "Repair mobile queue",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/api/issues/42",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
}], "partial": False}
|
||||
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ async def test_global_search_preview_returns_normalized_action_context(monkeypat
|
|||
"author": "alex",
|
||||
"labels": ["P1"],
|
||||
"assignees": [],
|
||||
"url": "https://forge.example/stackchain/api/issues/42",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
"claimable": True,
|
||||
"assigned_to_me": False,
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ async def test_global_search_preview_conversation_returns_a_bounded_authorized_p
|
|||
"author": "alexander",
|
||||
"body": "Current **decision**",
|
||||
"created_at": "2026-08-14T12:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/42#issuecomment-7",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42#issuecomment-7",
|
||||
}],
|
||||
"page": 3,
|
||||
"older_page": 2,
|
||||
|
|
@ -278,7 +278,7 @@ async def test_global_search_preview_reply_is_idempotent_for_an_authorized_pull(
|
|||
"author": "timmy",
|
||||
"body": body,
|
||||
"created_at": "2026-08-14T12:30:00Z",
|
||||
"url": "https://forge.example/stackchain/api/pulls/42#issuecomment-91",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/pulls/42#issuecomment-91",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
|
|
@ -345,7 +345,7 @@ async def test_global_search_preview_attachment_is_idempotent_for_exact_visible_
|
|||
|
||||
async def upload(repository, number, filename, content_type, content):
|
||||
uploads.append((repository, number, filename, content_type, content))
|
||||
return {"name": filename, "url": "https://forge.example/evidence/photo.png", "size": len(content)}
|
||||
return {"name": filename, "url": "http://127.0.0.1:3000/evidence/photo.png", "size": len(content)}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
monkeypatch.setattr(main.gitea_proxy, "upload_preview_attachment", upload)
|
||||
|
|
@ -367,7 +367,7 @@ async def test_global_search_preview_attachment_is_idempotent_for_exact_visible_
|
|||
|
||||
assert first.status_code == replay.status_code == 201
|
||||
assert replay.json() == first.json()
|
||||
assert first.json()["markdown"] == ""
|
||||
assert first.json()["markdown"] == ""
|
||||
assert previews == [("stackchain/api", "pull", 42)]
|
||||
assert uploads == [("stackchain/api", 42, "photo.png", "image/png", image)]
|
||||
|
||||
|
|
@ -408,11 +408,11 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results()
|
|||
return httpx.Response(200, json=[{
|
||||
"id": 4, "number": 42, "title": "Repair queue", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/42",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
}, {
|
||||
"id": 4, "number": 42, "title": "Repair queue", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/42",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
}, {
|
||||
"id": 5, "number": 43, "title": "Unsafe", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
|
|
@ -421,7 +421,7 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results()
|
|||
return httpx.Response(200, json=[{
|
||||
"id": 8, "number": 9, "title": "Improve search", "state": "closed",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -440,11 +440,11 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results()
|
|||
"items": [{
|
||||
"kind": "issue", "repository": "stackchain/api", "number": 42,
|
||||
"title": "Repair queue", "state": "open",
|
||||
"url": "https://forge.example/stackchain/api/issues/42",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
}, {
|
||||
"kind": "pull", "repository": "stackchain/web", "number": 9,
|
||||
"title": "Improve search", "state": "closed",
|
||||
"url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
}],
|
||||
"partial": False,
|
||||
"has_more": False,
|
||||
|
|
@ -454,6 +454,38 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results()
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_keeps_results_on_the_configured_forge_origin(monkeypatch):
|
||||
monkeypatch.setattr(gitea_proxy, "GITEA_URL", "https://forge.example/git")
|
||||
|
||||
async def handler(request):
|
||||
if request.url.params["type"] == "pulls":
|
||||
return httpx.Response(200, json=[])
|
||||
return httpx.Response(200, json=[{
|
||||
"id": 1, "number": 41, "title": "Foreign", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://attacker.example/stackchain/api/issues/41",
|
||||
}, {
|
||||
"id": 3, "number": 43, "title": "Downgraded", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "http://forge.example/git/stackchain/api/issues/43",
|
||||
}, {
|
||||
"id": 2, "number": 42, "title": "Relative", "state": "open",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "/git/stackchain/api/issues/42#issuecomment-7",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
result = await gitea_proxy.global_search("queue", 10)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert [(item["number"], item["url"]) for item in result["items"]] == [
|
||||
(42, "https://forge.example/git/stackchain/api/issues/42#issuecomment-7")
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
||||
async def handler(request):
|
||||
|
|
@ -464,7 +496,7 @@ async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
|||
"title": "Improve search",
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -480,7 +512,7 @@ async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
|||
"number": 9,
|
||||
"title": "Improve search",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
}],
|
||||
"partial": True,
|
||||
"has_more": True,
|
||||
|
|
@ -509,7 +541,7 @@ async def test_global_search_retries_a_failed_stream_without_advancing_its_page(
|
|||
"title": f'{params["type"]} {number}',
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": f'https://forge.example/stackchain/web/{params["type"]}/{number}',
|
||||
"html_url": f'http://127.0.0.1:3000/stackchain/web/{params["type"]}/{number}',
|
||||
}])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -546,7 +578,7 @@ async def test_global_search_odd_limit_keeps_both_streams_contiguous_across_page
|
|||
"title": f'{params["type"]} {number}',
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": f'https://forge.example/stackchain/web/{params["type"]}/{number}',
|
||||
"html_url": f'http://127.0.0.1:3000/stackchain/web/{params["type"]}/{number}',
|
||||
} for number in range(start, start + stream_limit)])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -579,7 +611,7 @@ async def test_global_search_caps_combined_results_to_requested_limit():
|
|||
"title": f"{kind} {number}",
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": f"https://forge.example/stackchain/web/{kind}/{number}",
|
||||
"html_url": f"http://127.0.0.1:3000/stackchain/web/{kind}/{number}",
|
||||
} for number in (1, 2)]
|
||||
return httpx.Response(200, json=items)
|
||||
|
||||
|
|
@ -607,7 +639,7 @@ async def test_global_search_pages_each_stream_and_balances_results():
|
|||
"title": f"{kind} page {page} result {number}",
|
||||
"state": "open",
|
||||
"repository": {"full_name": "stackchain/web"},
|
||||
"html_url": f"https://forge.example/stackchain/web/{kind}/{page * 10 + number}",
|
||||
"html_url": f"http://127.0.0.1:3000/stackchain/web/{kind}/{page * 10 + number}",
|
||||
} for number in (1, 2, 3, 4)]
|
||||
return httpx.Response(200, json=items)
|
||||
|
||||
|
|
@ -650,7 +682,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i
|
|||
"title": "Repair queue",
|
||||
"body": "Keep mobile operators moving.",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/42",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
"user": {"login": "alex"},
|
||||
"labels": [{"name": "P1"}, None],
|
||||
"assignees": [],
|
||||
|
|
@ -672,7 +704,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i
|
|||
"author": "alex",
|
||||
"labels": ["P1"],
|
||||
"assignees": [],
|
||||
"url": "https://forge.example/stackchain/api/issues/42",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
"claimable": True,
|
||||
"reopenable": False,
|
||||
"assigned_to_me": False,
|
||||
|
|
@ -690,7 +722,7 @@ async def test_work_preview_derives_pull_kind_and_never_offers_issue_claim():
|
|||
"number": 9,
|
||||
"title": "Improve search",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
"pull_request": {"merged": False},
|
||||
"assignees": [],
|
||||
})
|
||||
|
|
@ -722,7 +754,7 @@ async def test_work_preview_marks_authoritative_requested_pull_review_actionable
|
|||
"number": 9,
|
||||
"title": "Improve search",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/web/pulls/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||
"pull_request": {"merged": False},
|
||||
"assignees": [],
|
||||
})
|
||||
|
|
@ -746,7 +778,7 @@ async def test_work_preview_offers_reopen_only_for_closed_issues():
|
|||
"number": 42,
|
||||
"title": "Resume work",
|
||||
"state": "closed",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/42",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||
"assignees": [],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -680,7 +680,7 @@ async def test_gitea_edit_issue_confirms_already_applied_content_after_lost_resp
|
|||
"updated_at": "2026-08-07T10:02:00Z",
|
||||
"assignees": [{"login": "timmy"}],
|
||||
"pull_request": None,
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -699,7 +699,7 @@ async def test_gitea_edit_issue_confirms_already_applied_content_after_lost_resp
|
|||
assert result == {
|
||||
"repository": "stackchain/api", "number": 17, "title": "My draft",
|
||||
"body": "Draft body", "state": "open", "updated_at": "2026-08-07T10:02:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -720,7 +720,7 @@ async def test_gitea_edit_issue_revalidates_assignment_and_confirms_content():
|
|||
return httpx.Response(200, json={
|
||||
"number": 17, "title": "Clarified", "body": "New body", "state": "open",
|
||||
"updated_at": "2026-08-07T10:01:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -740,7 +740,7 @@ async def test_gitea_edit_issue_revalidates_assignment_and_confirms_content():
|
|||
assert result == {
|
||||
"repository": "stackchain/api", "number": 17, "title": "Clarified",
|
||||
"body": "New body", "state": "open", "updated_at": "2026-08-07T10:01:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -804,7 +804,7 @@ async def test_create_issue_endpoint_derives_self_assignment_and_returns_confirm
|
|||
"labels": ["P0"],
|
||||
"assignees": [assignee],
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "current_user", user)
|
||||
|
|
@ -1014,7 +1014,7 @@ async def test_repository_search_endpoint_returns_minimal_visible_matches(monkey
|
|||
"id": 151,
|
||||
"name": "mobile",
|
||||
"full_name": "stackchain/mobile",
|
||||
"html_url": "https://forge.example/stackchain/mobile",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/mobile",
|
||||
"description": "secret context that the picker does not need",
|
||||
"private": True,
|
||||
}]
|
||||
|
|
@ -1042,7 +1042,7 @@ async def test_repository_page_endpoint_and_issue_creation_support_later_reposit
|
|||
return {
|
||||
"items": [{
|
||||
"id": 51, "name": "later", "full_name": "stackchain/later",
|
||||
"description": "", "html_url": "https://forge.example/stackchain/later",
|
||||
"description": "", "html_url": "http://127.0.0.1:3000/stackchain/later",
|
||||
"updated_at": "2026-08-09T12:00:00Z",
|
||||
}],
|
||||
"page": 2, "total": 51, "has_more": False,
|
||||
|
|
@ -1061,7 +1061,7 @@ async def test_repository_page_endpoint_and_issue_creation_support_later_reposit
|
|||
"id": 403, "number": 403, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-09T12:00:00Z",
|
||||
"url": "https://forge.example/stackchain/later/issues/403",
|
||||
"url": "http://127.0.0.1:3000/stackchain/later/issues/403",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "repo_page", page)
|
||||
|
|
@ -1087,7 +1087,7 @@ def test_context_exposes_truthful_repository_pagination():
|
|||
repositories = gitea_proxy.RepositoryItems(
|
||||
[{
|
||||
"id": 1, "name": "api", "full_name": "stackchain/api",
|
||||
"description": "", "html_url": "https://forge.example/stackchain/api",
|
||||
"description": "", "html_url": "http://127.0.0.1:3000/stackchain/api",
|
||||
"updated_at": "2026-08-09T12:00:00Z",
|
||||
}],
|
||||
{"page": 1, "total": 51, "has_more": True},
|
||||
|
|
@ -1124,7 +1124,7 @@ async def test_create_issue_atomically_validates_and_sends_release_plan(monkeypa
|
|||
"milestone": {"id": 9, "title": "August RC"},
|
||||
"due_date": "2026-08-31T23:59:59Z",
|
||||
"updated_at": "2026-08-07T20:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/221",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/221",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "current_user", user)
|
||||
|
|
@ -1176,7 +1176,7 @@ async def test_create_issue_replays_one_upstream_result_for_concurrent_idempoten
|
|||
"id": 81, "number": 17, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "current_user", user)
|
||||
|
|
@ -1222,7 +1222,7 @@ async def test_completed_issue_creation_replays_after_ledger_reconstruction(monk
|
|||
"id": 201, "number": 201, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-07T15:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/201",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/201",
|
||||
}
|
||||
|
||||
database = tmp_path / "issues.sqlite3"
|
||||
|
|
@ -1280,7 +1280,7 @@ async def test_create_issue_rejects_changed_payload_for_an_existing_idempotency_
|
|||
"id": 81, "number": 17, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "current_user", user)
|
||||
|
|
@ -1324,7 +1324,7 @@ async def test_create_issue_retry_recovers_result_after_the_first_request_times_
|
|||
"id": 81, "number": 17, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main, "ISSUE_ACTION_TIMEOUT_SECONDS", 0.01)
|
||||
|
|
@ -1367,7 +1367,7 @@ async def test_create_issue_durable_ledger_evicts_completed_entry_at_size_limit(
|
|||
"id": 81, "number": 17, "title": title, "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": [assignee],
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
ledger = IdempotencyLedger(
|
||||
|
|
@ -1407,7 +1407,7 @@ async def test_gitea_create_issue_posts_self_assignment_and_normalizes_confirmat
|
|||
return httpx.Response(201, json={
|
||||
"id": 81, "number": 17, "title": "Capture mobile work", "state": "open",
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
"assignees": [{"login": "timmy"}],
|
||||
"labels": [{"id": 3, "name": "P0"}],
|
||||
})
|
||||
|
|
@ -1441,7 +1441,7 @@ async def test_gitea_create_issue_omits_assignment_and_confirms_no_owner():
|
|||
return httpx.Response(201, json={
|
||||
"id": 83, "number": 19, "title": "Backlog capture", "state": "open",
|
||||
"updated_at": "2026-08-07T03:00:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/19",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/19",
|
||||
"assignees": [], "labels": [],
|
||||
})
|
||||
|
||||
|
|
@ -1634,7 +1634,7 @@ async def test_gitea_create_issue_requires_confirmed_self_assignment():
|
|||
async def handler(_request):
|
||||
return httpx.Response(201, json={
|
||||
"id": 81, "number": 17, "title": "Capture mobile work", "state": "open",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17", "assignees": [],
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", "assignees": [],
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -1657,14 +1657,14 @@ async def test_gitea_claim_available_issue_rechecks_then_confirms_authenticated_
|
|||
return httpx.Response(200, json={
|
||||
"id": 81, "number": 17, "title": "Available", "state": "open",
|
||||
"assignees": None, "pull_request": None, "labels": [],
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
if request.method == "GET" and request.url.path == "/api/v1/user":
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
return httpx.Response(200, json={
|
||||
"id": 81, "number": 17, "title": "Available", "state": "open",
|
||||
"assignees": [{"login": "timmy"}], "labels": [],
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -1693,14 +1693,14 @@ async def test_gitea_reopen_issue_rechecks_closed_state_and_confirms_self_assign
|
|||
return httpx.Response(200, json={
|
||||
"id": 81, "number": 17, "title": "Resume work", "state": "closed",
|
||||
"assignees": [], "pull_request": None, "labels": [{"name": "P1"}],
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
if request.method == "GET" and request.url.path == "/api/v1/user":
|
||||
return httpx.Response(200, json={"login": "timmy"})
|
||||
return httpx.Response(200, json={
|
||||
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||
"assignees": [{"login": "timmy"}], "labels": [{"name": "P1"}],
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -1719,7 +1719,7 @@ async def test_gitea_reopen_issue_rechecks_closed_state_and_confirms_self_assign
|
|||
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||
"repository": "stackchain/api", "labels": ["P1"],
|
||||
"assignees": ["timmy"], "updated_at": "",
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1734,7 +1734,7 @@ async def test_gitea_reopen_issue_retry_accepts_already_open_self_assigned_issue
|
|||
return httpx.Response(200, json={
|
||||
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||
"assignees": [{"login": "timmy"}], "pull_request": None, "labels": [],
|
||||
"html_url": "https://forge.example/stackchain/api/issues/17",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -1757,7 +1757,7 @@ async def test_claim_available_issue_endpoint_returns_confirmed_work_item(monkey
|
|||
return {
|
||||
"id": 81, "number": number, "title": "Available", "state": "open",
|
||||
"repository": repository, "labels": [], "assignees": ["timmy"],
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "claim_available_issue", claim)
|
||||
|
|
@ -1797,7 +1797,7 @@ async def test_reopen_issue_endpoint_returns_confirmed_resumable_work_item(monke
|
|||
return {
|
||||
"id": 81, "number": number, "title": "Resume work", "state": "open",
|
||||
"repository": repository, "labels": ["P1"], "assignees": ["timmy"],
|
||||
"url": "https://forge.example/stackchain/api/issues/17",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/17",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "reopen_issue", reopen, raising=False)
|
||||
|
|
@ -2564,7 +2564,7 @@ async def test_gitea_issue_comment_posts_body_and_returns_safe_identity():
|
|||
"id": 82,
|
||||
"body": "Ready to ship",
|
||||
"created_at": "2026-08-07T10:10:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-82",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-82",
|
||||
"user": {"login": "timmy"},
|
||||
},
|
||||
)
|
||||
|
|
@ -2584,7 +2584,7 @@ async def test_gitea_issue_comment_posts_body_and_returns_safe_identity():
|
|||
"author": "timmy",
|
||||
"body": "Ready to ship",
|
||||
"created_at": "2026-08-07T10:10:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-82",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-82",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2602,7 +2602,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"number": 3,
|
||||
"title": "Restore signing service",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/platform/issues/3",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/platform/issues/3",
|
||||
"repository": {"full_name": "stackchain/platform"},
|
||||
},
|
||||
{
|
||||
|
|
@ -2621,7 +2621,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"id": 81,
|
||||
"body": "Latest update",
|
||||
"created_at": "2026-08-07T10:00:00Z",
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-81",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81",
|
||||
"user": {"login": "sam"},
|
||||
}
|
||||
],
|
||||
|
|
@ -2635,7 +2635,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"body": "Full issue context",
|
||||
"updated_at": "2026-08-07T09:59:00Z",
|
||||
"milestone": {"id": 9, "title": "August RC", "description": "not exposed"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/7",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
"labels": [{"name": "P1"}],
|
||||
"assignees": [{"login": "timmy"}],
|
||||
},
|
||||
|
|
@ -2661,7 +2661,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"updated_at": "2026-08-07T09:59:00Z",
|
||||
"due_date": None,
|
||||
"milestone": {"id": 9, "title": "August RC"},
|
||||
"url": "https://forge.example/stackchain/api/issues/7",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||
"labels": ["P1"],
|
||||
"assignees": ["timmy"],
|
||||
"dependencies_available": True,
|
||||
|
|
@ -2671,7 +2671,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"number": 3,
|
||||
"title": "Restore signing service",
|
||||
"state": "open",
|
||||
"url": "https://forge.example/stackchain/platform/issues/3",
|
||||
"url": "http://127.0.0.1:3000/stackchain/platform/issues/3",
|
||||
}
|
||||
],
|
||||
"comments": [
|
||||
|
|
@ -2680,7 +2680,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"author": "sam",
|
||||
"body": "Latest update",
|
||||
"created_at": "2026-08-07T10:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-81",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81",
|
||||
}
|
||||
],
|
||||
"conversation": {
|
||||
|
|
@ -2690,7 +2690,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
|||
"author": "sam",
|
||||
"body": "Latest update",
|
||||
"created_at": "2026-08-07T10:00:00Z",
|
||||
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-81",
|
||||
"url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81",
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
|
|
@ -2984,7 +2984,7 @@ async def test_gitea_add_dependency_validates_assignment_candidate_and_confirmat
|
|||
return httpx.Response(200, json=[{
|
||||
"number": 9, "state": "open", "title": "Restore API",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/9",
|
||||
}] if mutated else [])
|
||||
if request.method == "POST" and path.endswith("/dependencies"):
|
||||
return httpx.Response(201, json={})
|
||||
|
|
@ -3003,7 +3003,7 @@ async def test_gitea_add_dependency_validates_assignment_candidate_and_confirmat
|
|||
assert json.loads(mutation.content) == {"owner": "stackchain", "repo": "api", "index": 9}
|
||||
assert result["dependencies"] == [{
|
||||
"repository": "stackchain/api", "number": 9, "title": "Restore API",
|
||||
"state": "open", "url": "https://forge.example/stackchain/api/issues/9",
|
||||
"state": "open", "url": "http://127.0.0.1:3000/stackchain/api/issues/9",
|
||||
}]
|
||||
|
||||
|
||||
|
|
@ -3013,7 +3013,7 @@ async def test_gitea_dependency_retry_returns_canonical_state_without_duplicate_
|
|||
dependency = {
|
||||
"number": 9, "state": "open", "title": "Restore API",
|
||||
"repository": {"full_name": "stackchain/api"},
|
||||
"html_url": "https://forge.example/stackchain/api/issues/9",
|
||||
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/9",
|
||||
}
|
||||
|
||||
async def handler(request):
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ async def test_gitea_attachment_revalidates_assignment_and_sends_multipart():
|
|||
"id": 9,
|
||||
"name": "checkout.png",
|
||||
"size": len(PNG_BYTES),
|
||||
"browser_download_url": "https://forge.example/attachments/checkout.png",
|
||||
"browser_download_url": "http://127.0.0.1:3000/attachments/checkout.png",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -379,7 +379,7 @@ async def test_gitea_attachment_revalidates_assignment_and_sends_multipart():
|
|||
assert PNG_BYTES in upload.content
|
||||
assert result == {
|
||||
"name": "checkout.png",
|
||||
"url": "https://forge.example/attachments/checkout.png",
|
||||
"url": "http://127.0.0.1:3000/attachments/checkout.png",
|
||||
"size": len(PNG_BYTES),
|
||||
}
|
||||
|
||||
|
|
@ -514,7 +514,7 @@ async def test_gitea_pull_attachment_revalidates_assignment_before_exact_pull_up
|
|||
})
|
||||
return httpx.Response(201, json={
|
||||
"name": "pull.png", "size": len(PNG_BYTES),
|
||||
"browser_download_url": "https://forge.example/attachments/pull.png",
|
||||
"browser_download_url": "http://127.0.0.1:3000/attachments/pull.png",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -531,4 +531,4 @@ async def test_gitea_pull_attachment_revalidates_assignment_before_exact_pull_up
|
|||
("POST", "/api/v1/repos/stackchain/web/issues/31/assets"),
|
||||
]
|
||||
assert PNG_BYTES in requests[-1].content
|
||||
assert result["url"] == "https://forge.example/attachments/pull.png"
|
||||
assert result["url"] == "http://127.0.0.1:3000/attachments/pull.png"
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ async def test_gitea_review_submission_checks_head_then_maps_decision_upstream()
|
|||
json={
|
||||
"id": 91,
|
||||
"state": "APPROVED",
|
||||
"html_url": "https://forge.example/reviews/91",
|
||||
"html_url": "http://127.0.0.1:3000/reviews/91",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ async def test_gitea_review_submission_checks_head_then_maps_decision_upstream()
|
|||
assert result == {
|
||||
"id": 91,
|
||||
"state": "APPROVED",
|
||||
"url": "https://forge.example/reviews/91",
|
||||
"url": "http://127.0.0.1:3000/reviews/91",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async def test_notification_attachment_endpoint_resolves_exact_pull_server_side(
|
|||
|
||||
async def upload(thread_id, filename, content_type, content):
|
||||
calls.append((thread_id, filename, content_type, content))
|
||||
return {"name": filename, "url": "https://forge.example/a/proof.webp", "size": len(content)}
|
||||
return {"name": filename, "url": "http://127.0.0.1:3000/a/proof.webp", "size": len(content)}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "upload_notification_attachment", upload, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
|
|
@ -52,7 +52,7 @@ async def test_notification_attachment_endpoint_resolves_exact_pull_server_side(
|
|||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["markdown"] == ""
|
||||
assert response.json()["markdown"] == ""
|
||||
assert calls == [(527, "proof.png", "image/png", PNG)]
|
||||
assert main.request_body_limit("POST", "/api/v1/notifications/527/attachments") == 2 * 1024 * 1024 + 64 * 1024
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ async def test_proxy_notification_upload_trusts_only_matching_gitea_subject_path
|
|||
})
|
||||
return httpx.Response(201, json={
|
||||
"name": "proof.png", "size": len(PNG),
|
||||
"browser_download_url": "https://forge.example/a/proof.png",
|
||||
"browser_download_url": "http://127.0.0.1:3000/a/proof.png",
|
||||
})
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
|
|
@ -86,7 +86,7 @@ async def test_proxy_notification_upload_trusts_only_matching_gitea_subject_path
|
|||
("GET", "/api/v1/notifications/threads/527"),
|
||||
("POST", "/api/v1/repos/stackchain/web/issues/31/assets"),
|
||||
]
|
||||
assert result["url"] == "https://forge.example/a/proof.png"
|
||||
assert result["url"] == "http://127.0.0.1:3000/a/proof.png"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -157,7 +157,7 @@ const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{i
|
|||
complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}};
|
||||
const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}};
|
||||
calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body instanceof FormData?'multipart':options.body?JSON.parse(options.body):null}});
|
||||
if(url.endsWith('/attachments'))return{{markdown:''}};
|
||||
if(url.endsWith('/attachments'))return{{markdown:''}};
|
||||
if(url.endsWith('/reply') && replyAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}}
|
||||
return url.endsWith('/reply')?{{id:8}}:{{status:'read'}};
|
||||
}};
|
||||
|
|
@ -178,5 +178,5 @@ const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity'
|
|||
assert [call["key"] for call in output["calls"][:3]] == [
|
||||
"reply-image:attachment", "reply-image:reply", "reply-image:reply"
|
||||
]
|
||||
assert output["calls"][2]["body"] == {"body": ""}
|
||||
assert output["calls"][2]["body"] == {"body": ""}
|
||||
assert output["result"]["confirmed"] == [{"status": "read"}]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user