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
|
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
|
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
|
(`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
|
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
|
return None
|
||||||
repository = item.get("repository")
|
repository = item.get("repository")
|
||||||
repository = repository if isinstance(repository, dict) else {}
|
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 (
|
if not (
|
||||||
isinstance(item.get("number"), int)
|
isinstance(item.get("number"), int)
|
||||||
and isinstance(item.get("title"), str)
|
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)
|
if isinstance(label, dict) and isinstance(label.get("name"), str)
|
||||||
],
|
],
|
||||||
"assignees": assignee_names,
|
"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,
|
"claimable": actual_kind == "issue" and state == "open" and not assignee_names,
|
||||||
"reopenable": actual_kind == "issue" and state == "closed",
|
"reopenable": actual_kind == "issue" and state == "closed",
|
||||||
"assigned_to_me": bool(login and login in assignee_names),
|
"assigned_to_me": bool(login and login in assignee_names),
|
||||||
|
|
@ -650,7 +650,7 @@ def _normalize_available_issue(item: Any) -> dict | None:
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
"updated_at": item.get("updated_at", "")
|
"updated_at": item.get("updated_at", "")
|
||||||
if isinstance(item.get("updated_at"), str) else "",
|
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:
|
def _safe_gitea_web_url(value: Any) -> str:
|
||||||
if not isinstance(value, str) or not value.strip():
|
if not isinstance(value, str) or not value.strip():
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -819,8 +812,8 @@ def _normalize_notifications(threads: Any) -> list[dict]:
|
||||||
subject = thread.get("subject")
|
subject = thread.get("subject")
|
||||||
repository = repository if isinstance(repository, dict) else {}
|
repository = repository if isinstance(repository, dict) else {}
|
||||||
subject = subject if isinstance(subject, dict) else {}
|
subject = subject if isinstance(subject, dict) else {}
|
||||||
subject_url = _safe_web_url(subject.get("html_url"))
|
subject_url = _safe_gitea_web_url(subject.get("html_url"))
|
||||||
latest_url = _safe_web_url(subject.get("latest_comment_html_url"))
|
latest_url = _safe_gitea_web_url(subject.get("latest_comment_html_url"))
|
||||||
number_text = (
|
number_text = (
|
||||||
urlsplit(subject_url).path.rstrip("/").rsplit("/", 1)[-1]
|
urlsplit(subject_url).path.rstrip("/").rsplit("/", 1)[-1]
|
||||||
if subject_url
|
if subject_url
|
||||||
|
|
@ -1053,8 +1046,8 @@ async def notification_detail(thread_id: int) -> dict:
|
||||||
comment = comment if isinstance(comment, dict) else {}
|
comment = comment if isinstance(comment, dict) else {}
|
||||||
user_value = comment.get("user")
|
user_value = comment.get("user")
|
||||||
user = user_value if isinstance(user_value, dict) else {}
|
user = user_value if isinstance(user_value, dict) else {}
|
||||||
subject_url = _safe_web_url(subject.get("html_url"))
|
subject_url = _safe_gitea_web_url(subject.get("html_url"))
|
||||||
latest_url = _safe_web_url(comment.get("html_url")) or _safe_web_url(
|
latest_url = _safe_gitea_web_url(comment.get("html_url")) or _safe_gitea_web_url(
|
||||||
subject.get("latest_comment_html_url")
|
subject.get("latest_comment_html_url")
|
||||||
)
|
)
|
||||||
assignee_values = subject_detail.get("assignees")
|
assignee_values = subject_detail.get("assignees")
|
||||||
|
|
@ -1236,7 +1229,7 @@ async def upload_notification_attachment(
|
||||||
if not isinstance(attachment, dict):
|
if not isinstance(attachment, dict):
|
||||||
raise ValueError("Gitea attachment response was not an object")
|
raise ValueError("Gitea attachment response was not an object")
|
||||||
name = attachment.get("name")
|
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")
|
size = attachment.get("size")
|
||||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||||
raise ValueError("Gitea did not confirm the attachment")
|
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", "")
|
"created_at": comment.get("created_at", "")
|
||||||
if isinstance(comment.get("created_at"), str)
|
if isinstance(comment.get("created_at"), str)
|
||||||
else "",
|
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):
|
if not isinstance(attachment, dict):
|
||||||
raise ValueError("Gitea attachment response was not an object")
|
raise ValueError("Gitea attachment response was not an object")
|
||||||
name = attachment.get("name")
|
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")
|
size = attachment.get("size")
|
||||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||||
raise ValueError("Gitea did not confirm the attachment")
|
raise ValueError("Gitea did not confirm the attachment")
|
||||||
|
|
@ -1383,7 +1376,7 @@ async def upload_preview_attachment(
|
||||||
if not isinstance(attachment, dict):
|
if not isinstance(attachment, dict):
|
||||||
raise ValueError("Gitea attachment response was not an object")
|
raise ValueError("Gitea attachment response was not an object")
|
||||||
name = attachment.get("name")
|
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")
|
size = attachment.get("size")
|
||||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||||
raise ValueError("Gitea did not confirm the attachment")
|
raise ValueError("Gitea did not confirm the attachment")
|
||||||
|
|
@ -1410,7 +1403,7 @@ async def upload_assigned_pull_attachment(
|
||||||
if not isinstance(attachment, dict):
|
if not isinstance(attachment, dict):
|
||||||
raise ValueError("Gitea attachment response was not an object")
|
raise ValueError("Gitea attachment response was not an object")
|
||||||
name = attachment.get("name")
|
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")
|
size = attachment.get("size")
|
||||||
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
if not isinstance(name, str) or not name or not url or not isinstance(size, int):
|
||||||
raise ValueError("Gitea did not confirm the attachment")
|
raise ValueError("Gitea did not confirm the attachment")
|
||||||
|
|
@ -1586,7 +1579,7 @@ async def create_issue(
|
||||||
"updated_at": issue.get("updated_at", "")
|
"updated_at": issue.get("updated_at", "")
|
||||||
if isinstance(issue.get("updated_at"), str)
|
if isinstance(issue.get("updated_at"), str)
|
||||||
else "",
|
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,
|
"assignees": logins,
|
||||||
"updated_at": confirmed.get("updated_at", "")
|
"updated_at": confirmed.get("updated_at", "")
|
||||||
if isinstance(confirmed.get("updated_at"), str) else "",
|
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,
|
"assignees": logins,
|
||||||
"updated_at": confirmed.get("updated_at", "")
|
"updated_at": confirmed.get("updated_at", "")
|
||||||
if isinstance(confirmed.get("updated_at"), str) else "",
|
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)
|
and isinstance(issue["milestone"].get("title"), str)
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"url": _safe_web_url(issue.get("html_url")),
|
"url": _safe_gitea_web_url(issue.get("html_url")),
|
||||||
"labels": [
|
"labels": [
|
||||||
label["name"] for label in labels
|
label["name"] for label in labels
|
||||||
if isinstance(label, dict) and isinstance(label.get("name"), str)
|
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,
|
"number": dependency_number,
|
||||||
"title": item.get("title", "") if isinstance(item.get("title"), str) else "",
|
"title": item.get("title", "") if isinstance(item.get("title"), str) else "",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": _safe_web_url(item.get("html_url")),
|
"url": _safe_gitea_web_url(item.get("html_url")),
|
||||||
})
|
})
|
||||||
return dependencies
|
return dependencies
|
||||||
|
|
||||||
|
|
@ -2425,7 +2418,7 @@ async def _update_issue_content(
|
||||||
"body": body,
|
"body": body,
|
||||||
"state": issue.get("state", "open"),
|
"state": issue.get("state", "open"),
|
||||||
"updated_at": issue.get("updated_at", ""),
|
"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")
|
raise IssueEditConflictError("issue changed upstream")
|
||||||
|
|
||||||
|
|
@ -2450,7 +2443,7 @@ async def _update_issue_content(
|
||||||
"body": body,
|
"body": body,
|
||||||
"state": confirmed.get("state", "open"),
|
"state": confirmed.get("state", "open"),
|
||||||
"updated_at": confirmed.get("updated_at", ""),
|
"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,
|
"number": number,
|
||||||
"title": target.get("title", "") if isinstance(target.get("title"), str) else "",
|
"title": target.get("title", "") if isinstance(target.get("title"), str) else "",
|
||||||
"state": state,
|
"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_review": True, "work_reasons": ["review_requested"]} if kind == "review" else {}),
|
||||||
**({
|
**({
|
||||||
"is_filed": True,
|
"is_filed": True,
|
||||||
|
|
@ -2623,7 +2616,7 @@ async def pull_completion_detail(repository: str, number: int) -> dict:
|
||||||
"number": number,
|
"number": number,
|
||||||
"title": pull.get("title") if isinstance(pull.get("title"), str) else "",
|
"title": pull.get("title") if isinstance(pull.get("title"), str) else "",
|
||||||
"body": pull.get("body") if isinstance(pull.get("body"), 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 "",
|
"author": user.get("login") if isinstance(user.get("login"), str) else "",
|
||||||
"head_sha": sha,
|
"head_sha": sha,
|
||||||
"state": pull.get("state") if isinstance(pull.get("state"), str) else "",
|
"state": pull.get("state") if isinstance(pull.get("state"), str) else "",
|
||||||
|
|
@ -2865,7 +2858,7 @@ async def submit_pull_review(
|
||||||
return {
|
return {
|
||||||
"id": review.get("id"),
|
"id": review.get("id"),
|
||||||
"state": review.get("state") or "COMMENT",
|
"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": {
|
"subject": {
|
||||||
"title": "Retry failed deploy",
|
"title": "Retry failed deploy",
|
||||||
"type": "Issue",
|
"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",
|
"title": "Retry failed deploy",
|
||||||
"type": "Issue",
|
"type": "Issue",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"html_url": "https://forge.example/stackchain/api/issues/7",
|
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||||
"latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
"latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{"id": 43, "repository": None, "subject": None},
|
{"id": 43, "repository": None, "subject": None},
|
||||||
|
|
@ -193,8 +193,8 @@ async def test_unread_notifications_are_normalized_for_mobile_handoff():
|
||||||
"title": "Retry failed deploy",
|
"title": "Retry failed deploy",
|
||||||
"subject_type": "Issue",
|
"subject_type": "Issue",
|
||||||
"state": "open",
|
"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_url": "https://forge.example/stackchain/api/issues/7",
|
"subject_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 43,
|
"id": 43,
|
||||||
|
|
@ -245,8 +245,8 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7",
|
"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",
|
"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",
|
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/7",
|
||||||
"latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9",
|
"latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if request.url.path.endswith("/issues/7"):
|
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.",
|
"body": "Logs point to the worker timeout.",
|
||||||
"created_at": "2026-08-06T12:30:00Z",
|
"created_at": "2026-08-06T12:30:00Z",
|
||||||
"user": {"login": "alexander"},
|
"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"):
|
if request.url.path.endswith("/issues/7/comments"):
|
||||||
return httpx.Response(200, json=[{
|
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.",
|
"body": "Logs point to the worker timeout.",
|
||||||
"created_at": "2026-08-06T12:30:00Z",
|
"created_at": "2026-08-06T12:30:00Z",
|
||||||
"user": {"login": "alexander"},
|
"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"})
|
}], headers={"X-Total-Count": "1"})
|
||||||
return httpx.Response(404)
|
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",
|
"title": "Retry failed deploy",
|
||||||
"subject_type": "Issue",
|
"subject_type": "Issue",
|
||||||
"state": "open",
|
"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.",
|
"subject_body": "Deploy fails after **three** retries.",
|
||||||
"latest_comment": {
|
"latest_comment": {
|
||||||
"author": "alexander",
|
"author": "alexander",
|
||||||
"body": "Logs point to the worker timeout.",
|
"body": "Logs point to the worker timeout.",
|
||||||
"created_at": "2026-08-06T12:30:00Z",
|
"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},
|
"issue": {"number": 7, "assignees": [], "claimable": True},
|
||||||
"acknowledge_supported": True,
|
"acknowledge_supported": True,
|
||||||
|
|
@ -362,7 +362,7 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin
|
||||||
"user": {"login": "timmy"},
|
"user": {"login": "timmy"},
|
||||||
"body": "Please retry the worker.",
|
"body": "Please retry the worker.",
|
||||||
"created_at": "2026-08-07T19:00:00Z",
|
"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))
|
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",
|
"author": "timmy",
|
||||||
"body": "Please retry the worker.",
|
"body": "Please retry the worker.",
|
||||||
"created_at": "2026-08-07T19:00:00Z",
|
"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",
|
"title": "Older review",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/api"},
|
"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",
|
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||||
"updated_at": "2026-08-15T12:00:00Z",
|
"updated_at": "2026-08-15T12:00:00Z",
|
||||||
"repository": {"full_name": "stackchain/dashboard"},
|
"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))
|
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",
|
"id": 878, "number": 878, "title": "Completed delegation", "state": "closed",
|
||||||
"updated_at": "2026-08-15T12:00:00Z",
|
"updated_at": "2026-08-15T12:00:00Z",
|
||||||
"repository": {"full_name": "stackchain/dashboard"},
|
"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"],
|
"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
|
del page, limit
|
||||||
common = {
|
common = {
|
||||||
"id": 7, "number": 7, "title": "My own filing", "state": "open",
|
"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":
|
if stream == "issue":
|
||||||
return {"items": [{**common, "assignees": [{"login": "timmy"}]}], "page": 1, "total": 1, "has_more": False}
|
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"},
|
"repository": {"full_name": "stackchain/mobile"},
|
||||||
"updated_at": "2026-08-06T12:00:00Z",
|
"updated_at": "2026-08-06T12:00:00Z",
|
||||||
"due_date": "2026-08-09T23:59:59Z",
|
"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():
|
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"],
|
"work_reasons": ["assigned_to_me", "review_requested"],
|
||||||
"repository": {"full_name": "stackchain/api"},
|
"repository": {"full_name": "stackchain/api"},
|
||||||
"updated_at": "2026-08-06T11:00:00Z",
|
"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)
|
monkeypatch.setattr(main, "current_user", user)
|
||||||
|
|
@ -864,7 +864,7 @@ async def test_pull_review_detail_combines_pr_files_status_and_reviews(monkeypat
|
||||||
return {
|
return {
|
||||||
"title": "Review API",
|
"title": "Review API",
|
||||||
"body": "Please check the retry flow.",
|
"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"},
|
"user": {"login": "alex"},
|
||||||
"head": {"sha": "abc123"},
|
"head": {"sha": "abc123"},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ async def test_global_search_specific_type_uses_one_full_width_scoped_stream():
|
||||||
"title": "Review mobile search",
|
"title": "Review mobile search",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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))
|
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,
|
"number": 42,
|
||||||
"title": "Repair mobile queue",
|
"title": "Repair mobile queue",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": "https://forge.example/stackchain/api/issues/42",
|
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||||
}],
|
}],
|
||||||
"partial": False,
|
"partial": False,
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +180,7 @@ async def test_global_search_endpoint_returns_bounded_normalized_results(monkeyp
|
||||||
"number": 42,
|
"number": 42,
|
||||||
"title": "Repair mobile queue",
|
"title": "Repair mobile queue",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": "https://forge.example/stackchain/api/issues/42",
|
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||||
}], "partial": False}
|
}], "partial": False}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,7 +200,7 @@ async def test_global_search_preview_returns_normalized_action_context(monkeypat
|
||||||
"author": "alex",
|
"author": "alex",
|
||||||
"labels": ["P1"],
|
"labels": ["P1"],
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
"url": "https://forge.example/stackchain/api/issues/42",
|
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||||
"claimable": True,
|
"claimable": True,
|
||||||
"assigned_to_me": False,
|
"assigned_to_me": False,
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +233,7 @@ async def test_global_search_preview_conversation_returns_a_bounded_authorized_p
|
||||||
"author": "alexander",
|
"author": "alexander",
|
||||||
"body": "Current **decision**",
|
"body": "Current **decision**",
|
||||||
"created_at": "2026-08-14T12:00:00Z",
|
"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,
|
"page": 3,
|
||||||
"older_page": 2,
|
"older_page": 2,
|
||||||
|
|
@ -278,7 +278,7 @@ async def test_global_search_preview_reply_is_idempotent_for_an_authorized_pull(
|
||||||
"author": "timmy",
|
"author": "timmy",
|
||||||
"body": body,
|
"body": body,
|
||||||
"created_at": "2026-08-14T12:30:00Z",
|
"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)
|
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):
|
async def upload(repository, number, filename, content_type, content):
|
||||||
uploads.append((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, "work_preview", preview)
|
||||||
monkeypatch.setattr(main.gitea_proxy, "upload_preview_attachment", upload)
|
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 first.status_code == replay.status_code == 201
|
||||||
assert replay.json() == first.json()
|
assert replay.json() == first.json()
|
||||||
assert first.json()["markdown"] == ""
|
assert first.json()["markdown"] == ""
|
||||||
assert previews == [("stackchain/api", "pull", 42)]
|
assert previews == [("stackchain/api", "pull", 42)]
|
||||||
assert uploads == [("stackchain/api", 42, "photo.png", "image/png", image)]
|
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=[{
|
return httpx.Response(200, json=[{
|
||||||
"id": 4, "number": 42, "title": "Repair queue", "state": "open",
|
"id": 4, "number": 42, "title": "Repair queue", "state": "open",
|
||||||
"repository": {"full_name": "stackchain/api"},
|
"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",
|
"id": 4, "number": 42, "title": "Repair queue", "state": "open",
|
||||||
"repository": {"full_name": "stackchain/api"},
|
"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",
|
"id": 5, "number": 43, "title": "Unsafe", "state": "open",
|
||||||
"repository": {"full_name": "stackchain/api"},
|
"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=[{
|
return httpx.Response(200, json=[{
|
||||||
"id": 8, "number": 9, "title": "Improve search", "state": "closed",
|
"id": 8, "number": 9, "title": "Improve search", "state": "closed",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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))
|
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": [{
|
"items": [{
|
||||||
"kind": "issue", "repository": "stackchain/api", "number": 42,
|
"kind": "issue", "repository": "stackchain/api", "number": 42,
|
||||||
"title": "Repair queue", "state": "open",
|
"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,
|
"kind": "pull", "repository": "stackchain/web", "number": 9,
|
||||||
"title": "Improve search", "state": "closed",
|
"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,
|
"partial": False,
|
||||||
"has_more": 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
|
@pytest.mark.anyio
|
||||||
async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
|
|
@ -464,7 +496,7 @@ async def test_global_search_returns_healthy_stream_when_other_stream_fails():
|
||||||
"title": "Improve search",
|
"title": "Improve search",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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))
|
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,
|
"number": 9,
|
||||||
"title": "Improve search",
|
"title": "Improve search",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": "https://forge.example/stackchain/web/pulls/9",
|
"url": "http://127.0.0.1:3000/stackchain/web/pulls/9",
|
||||||
}],
|
}],
|
||||||
"partial": True,
|
"partial": True,
|
||||||
"has_more": 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}',
|
"title": f'{params["type"]} {number}',
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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))
|
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}',
|
"title": f'{params["type"]} {number}',
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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)])
|
} for number in range(start, start + stream_limit)])
|
||||||
|
|
||||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
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}",
|
"title": f"{kind} {number}",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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)]
|
} for number in (1, 2)]
|
||||||
return httpx.Response(200, json=items)
|
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}",
|
"title": f"{kind} page {page} result {number}",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"repository": {"full_name": "stackchain/web"},
|
"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)]
|
} for number in (1, 2, 3, 4)]
|
||||||
return httpx.Response(200, json=items)
|
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",
|
"title": "Repair queue",
|
||||||
"body": "Keep mobile operators moving.",
|
"body": "Keep mobile operators moving.",
|
||||||
"state": "open",
|
"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"},
|
"user": {"login": "alex"},
|
||||||
"labels": [{"name": "P1"}, None],
|
"labels": [{"name": "P1"}, None],
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
|
|
@ -672,7 +704,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i
|
||||||
"author": "alex",
|
"author": "alex",
|
||||||
"labels": ["P1"],
|
"labels": ["P1"],
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
"url": "https://forge.example/stackchain/api/issues/42",
|
"url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||||
"claimable": True,
|
"claimable": True,
|
||||||
"reopenable": False,
|
"reopenable": False,
|
||||||
"assigned_to_me": False,
|
"assigned_to_me": False,
|
||||||
|
|
@ -690,7 +722,7 @@ async def test_work_preview_derives_pull_kind_and_never_offers_issue_claim():
|
||||||
"number": 9,
|
"number": 9,
|
||||||
"title": "Improve search",
|
"title": "Improve search",
|
||||||
"state": "open",
|
"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},
|
"pull_request": {"merged": False},
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
})
|
})
|
||||||
|
|
@ -722,7 +754,7 @@ async def test_work_preview_marks_authoritative_requested_pull_review_actionable
|
||||||
"number": 9,
|
"number": 9,
|
||||||
"title": "Improve search",
|
"title": "Improve search",
|
||||||
"state": "open",
|
"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},
|
"pull_request": {"merged": False},
|
||||||
"assignees": [],
|
"assignees": [],
|
||||||
})
|
})
|
||||||
|
|
@ -746,7 +778,7 @@ async def test_work_preview_offers_reopen_only_for_closed_issues():
|
||||||
"number": 42,
|
"number": 42,
|
||||||
"title": "Resume work",
|
"title": "Resume work",
|
||||||
"state": "closed",
|
"state": "closed",
|
||||||
"html_url": "https://forge.example/stackchain/api/issues/42",
|
"html_url": "http://127.0.0.1:3000/stackchain/api/issues/42",
|
||||||
"assignees": [],
|
"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",
|
"updated_at": "2026-08-07T10:02:00Z",
|
||||||
"assignees": [{"login": "timmy"}],
|
"assignees": [{"login": "timmy"}],
|
||||||
"pull_request": None,
|
"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))
|
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 == {
|
assert result == {
|
||||||
"repository": "stackchain/api", "number": 17, "title": "My draft",
|
"repository": "stackchain/api", "number": 17, "title": "My draft",
|
||||||
"body": "Draft body", "state": "open", "updated_at": "2026-08-07T10:02:00Z",
|
"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={
|
return httpx.Response(200, json={
|
||||||
"number": 17, "title": "Clarified", "body": "New body", "state": "open",
|
"number": 17, "title": "Clarified", "body": "New body", "state": "open",
|
||||||
"updated_at": "2026-08-07T10:01:00Z",
|
"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))
|
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 == {
|
assert result == {
|
||||||
"repository": "stackchain/api", "number": 17, "title": "Clarified",
|
"repository": "stackchain/api", "number": 17, "title": "Clarified",
|
||||||
"body": "New body", "state": "open", "updated_at": "2026-08-07T10:01:00Z",
|
"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"],
|
"labels": ["P0"],
|
||||||
"assignees": [assignee],
|
"assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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)
|
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,
|
"id": 151,
|
||||||
"name": "mobile",
|
"name": "mobile",
|
||||||
"full_name": "stackchain/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",
|
"description": "secret context that the picker does not need",
|
||||||
"private": True,
|
"private": True,
|
||||||
}]
|
}]
|
||||||
|
|
@ -1042,7 +1042,7 @@ async def test_repository_page_endpoint_and_issue_creation_support_later_reposit
|
||||||
return {
|
return {
|
||||||
"items": [{
|
"items": [{
|
||||||
"id": 51, "name": "later", "full_name": "stackchain/later",
|
"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",
|
"updated_at": "2026-08-09T12:00:00Z",
|
||||||
}],
|
}],
|
||||||
"page": 2, "total": 51, "has_more": False,
|
"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",
|
"id": 403, "number": 403, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-09T12:00:00Z",
|
"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)
|
monkeypatch.setattr(main.gitea_proxy, "repo_page", page)
|
||||||
|
|
@ -1087,7 +1087,7 @@ def test_context_exposes_truthful_repository_pagination():
|
||||||
repositories = gitea_proxy.RepositoryItems(
|
repositories = gitea_proxy.RepositoryItems(
|
||||||
[{
|
[{
|
||||||
"id": 1, "name": "api", "full_name": "stackchain/api",
|
"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",
|
"updated_at": "2026-08-09T12:00:00Z",
|
||||||
}],
|
}],
|
||||||
{"page": 1, "total": 51, "has_more": True},
|
{"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"},
|
"milestone": {"id": 9, "title": "August RC"},
|
||||||
"due_date": "2026-08-31T23:59:59Z",
|
"due_date": "2026-08-31T23:59:59Z",
|
||||||
"updated_at": "2026-08-07T20:00:00Z",
|
"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)
|
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",
|
"id": 81, "number": 17, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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)
|
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",
|
"id": 201, "number": 201, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T15:00:00Z",
|
"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"
|
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",
|
"id": 81, "number": 17, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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)
|
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",
|
"id": 81, "number": 17, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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)
|
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",
|
"id": 81, "number": 17, "title": title, "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": [assignee],
|
"repository": repository, "labels": [], "assignees": [assignee],
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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(
|
ledger = IdempotencyLedger(
|
||||||
|
|
@ -1407,7 +1407,7 @@ async def test_gitea_create_issue_posts_self_assignment_and_normalizes_confirmat
|
||||||
return httpx.Response(201, json={
|
return httpx.Response(201, json={
|
||||||
"id": 81, "number": 17, "title": "Capture mobile work", "state": "open",
|
"id": 81, "number": 17, "title": "Capture mobile work", "state": "open",
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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"}],
|
"assignees": [{"login": "timmy"}],
|
||||||
"labels": [{"id": 3, "name": "P0"}],
|
"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={
|
return httpx.Response(201, json={
|
||||||
"id": 83, "number": 19, "title": "Backlog capture", "state": "open",
|
"id": 83, "number": 19, "title": "Backlog capture", "state": "open",
|
||||||
"updated_at": "2026-08-07T03:00:00Z",
|
"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": [],
|
"assignees": [], "labels": [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1634,7 +1634,7 @@ async def test_gitea_create_issue_requires_confirmed_self_assignment():
|
||||||
async def handler(_request):
|
async def handler(_request):
|
||||||
return httpx.Response(201, json={
|
return httpx.Response(201, json={
|
||||||
"id": 81, "number": 17, "title": "Capture mobile work", "state": "open",
|
"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))
|
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={
|
return httpx.Response(200, json={
|
||||||
"id": 81, "number": 17, "title": "Available", "state": "open",
|
"id": 81, "number": 17, "title": "Available", "state": "open",
|
||||||
"assignees": None, "pull_request": None, "labels": [],
|
"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":
|
if request.method == "GET" and request.url.path == "/api/v1/user":
|
||||||
return httpx.Response(200, json={"login": "timmy"})
|
return httpx.Response(200, json={"login": "timmy"})
|
||||||
return httpx.Response(200, json={
|
return httpx.Response(200, json={
|
||||||
"id": 81, "number": 17, "title": "Available", "state": "open",
|
"id": 81, "number": 17, "title": "Available", "state": "open",
|
||||||
"assignees": [{"login": "timmy"}], "labels": [],
|
"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))
|
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={
|
return httpx.Response(200, json={
|
||||||
"id": 81, "number": 17, "title": "Resume work", "state": "closed",
|
"id": 81, "number": 17, "title": "Resume work", "state": "closed",
|
||||||
"assignees": [], "pull_request": None, "labels": [{"name": "P1"}],
|
"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":
|
if request.method == "GET" and request.url.path == "/api/v1/user":
|
||||||
return httpx.Response(200, json={"login": "timmy"})
|
return httpx.Response(200, json={"login": "timmy"})
|
||||||
return httpx.Response(200, json={
|
return httpx.Response(200, json={
|
||||||
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||||
"assignees": [{"login": "timmy"}], "labels": [{"name": "P1"}],
|
"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))
|
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",
|
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||||
"repository": "stackchain/api", "labels": ["P1"],
|
"repository": "stackchain/api", "labels": ["P1"],
|
||||||
"assignees": ["timmy"], "updated_at": "",
|
"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={
|
return httpx.Response(200, json={
|
||||||
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
"id": 81, "number": 17, "title": "Resume work", "state": "open",
|
||||||
"assignees": [{"login": "timmy"}], "pull_request": None, "labels": [],
|
"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))
|
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 {
|
return {
|
||||||
"id": 81, "number": number, "title": "Available", "state": "open",
|
"id": 81, "number": number, "title": "Available", "state": "open",
|
||||||
"repository": repository, "labels": [], "assignees": ["timmy"],
|
"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)
|
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 {
|
return {
|
||||||
"id": 81, "number": number, "title": "Resume work", "state": "open",
|
"id": 81, "number": number, "title": "Resume work", "state": "open",
|
||||||
"repository": repository, "labels": ["P1"], "assignees": ["timmy"],
|
"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)
|
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,
|
"id": 82,
|
||||||
"body": "Ready to ship",
|
"body": "Ready to ship",
|
||||||
"created_at": "2026-08-07T10:10:00Z",
|
"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"},
|
"user": {"login": "timmy"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -2584,7 +2584,7 @@ async def test_gitea_issue_comment_posts_body_and_returns_safe_identity():
|
||||||
"author": "timmy",
|
"author": "timmy",
|
||||||
"body": "Ready to ship",
|
"body": "Ready to ship",
|
||||||
"created_at": "2026-08-07T10:10:00Z",
|
"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,
|
"number": 3,
|
||||||
"title": "Restore signing service",
|
"title": "Restore signing service",
|
||||||
"state": "open",
|
"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"},
|
"repository": {"full_name": "stackchain/platform"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -2621,7 +2621,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||||
"id": 81,
|
"id": 81,
|
||||||
"body": "Latest update",
|
"body": "Latest update",
|
||||||
"created_at": "2026-08-07T10:00:00Z",
|
"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"},
|
"user": {"login": "sam"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2635,7 +2635,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||||
"body": "Full issue context",
|
"body": "Full issue context",
|
||||||
"updated_at": "2026-08-07T09:59:00Z",
|
"updated_at": "2026-08-07T09:59:00Z",
|
||||||
"milestone": {"id": 9, "title": "August RC", "description": "not exposed"},
|
"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"}],
|
"labels": [{"name": "P1"}],
|
||||||
"assignees": [{"login": "timmy"}],
|
"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",
|
"updated_at": "2026-08-07T09:59:00Z",
|
||||||
"due_date": None,
|
"due_date": None,
|
||||||
"milestone": {"id": 9, "title": "August RC"},
|
"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"],
|
"labels": ["P1"],
|
||||||
"assignees": ["timmy"],
|
"assignees": ["timmy"],
|
||||||
"dependencies_available": True,
|
"dependencies_available": True,
|
||||||
|
|
@ -2671,7 +2671,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||||
"number": 3,
|
"number": 3,
|
||||||
"title": "Restore signing service",
|
"title": "Restore signing service",
|
||||||
"state": "open",
|
"state": "open",
|
||||||
"url": "https://forge.example/stackchain/platform/issues/3",
|
"url": "http://127.0.0.1:3000/stackchain/platform/issues/3",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"comments": [
|
"comments": [
|
||||||
|
|
@ -2680,7 +2680,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||||
"author": "sam",
|
"author": "sam",
|
||||||
"body": "Latest update",
|
"body": "Latest update",
|
||||||
"created_at": "2026-08-07T10:00:00Z",
|
"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": {
|
"conversation": {
|
||||||
|
|
@ -2690,7 +2690,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments
|
||||||
"author": "sam",
|
"author": "sam",
|
||||||
"body": "Latest update",
|
"body": "Latest update",
|
||||||
"created_at": "2026-08-07T10:00:00Z",
|
"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,
|
"page": 1,
|
||||||
|
|
@ -2984,7 +2984,7 @@ async def test_gitea_add_dependency_validates_assignment_candidate_and_confirmat
|
||||||
return httpx.Response(200, json=[{
|
return httpx.Response(200, json=[{
|
||||||
"number": 9, "state": "open", "title": "Restore API",
|
"number": 9, "state": "open", "title": "Restore API",
|
||||||
"repository": {"full_name": "stackchain/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 mutated else [])
|
||||||
if request.method == "POST" and path.endswith("/dependencies"):
|
if request.method == "POST" and path.endswith("/dependencies"):
|
||||||
return httpx.Response(201, json={})
|
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 json.loads(mutation.content) == {"owner": "stackchain", "repo": "api", "index": 9}
|
||||||
assert result["dependencies"] == [{
|
assert result["dependencies"] == [{
|
||||||
"repository": "stackchain/api", "number": 9, "title": "Restore API",
|
"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 = {
|
dependency = {
|
||||||
"number": 9, "state": "open", "title": "Restore API",
|
"number": 9, "state": "open", "title": "Restore API",
|
||||||
"repository": {"full_name": "stackchain/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):
|
async def handler(request):
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ async def test_gitea_attachment_revalidates_assignment_and_sends_multipart():
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"name": "checkout.png",
|
"name": "checkout.png",
|
||||||
"size": len(PNG_BYTES),
|
"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))
|
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 PNG_BYTES in upload.content
|
||||||
assert result == {
|
assert result == {
|
||||||
"name": "checkout.png",
|
"name": "checkout.png",
|
||||||
"url": "https://forge.example/attachments/checkout.png",
|
"url": "http://127.0.0.1:3000/attachments/checkout.png",
|
||||||
"size": len(PNG_BYTES),
|
"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={
|
return httpx.Response(201, json={
|
||||||
"name": "pull.png", "size": len(PNG_BYTES),
|
"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))
|
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"),
|
("POST", "/api/v1/repos/stackchain/web/issues/31/assets"),
|
||||||
]
|
]
|
||||||
assert PNG_BYTES in requests[-1].content
|
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={
|
json={
|
||||||
"id": 91,
|
"id": 91,
|
||||||
"state": "APPROVED",
|
"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 == {
|
assert result == {
|
||||||
"id": 91,
|
"id": 91,
|
||||||
"state": "APPROVED",
|
"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):
|
async def upload(thread_id, filename, content_type, content):
|
||||||
calls.append((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)
|
monkeypatch.setattr(main.gitea_proxy, "upload_notification_attachment", upload, raising=False)
|
||||||
transport = httpx.ASGITransport(app=main.app)
|
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.status_code == 201
|
||||||
assert response.json()["markdown"] == ""
|
assert response.json()["markdown"] == ""
|
||||||
assert calls == [(527, "proof.png", "image/png", PNG)]
|
assert calls == [(527, "proof.png", "image/png", PNG)]
|
||||||
assert main.request_body_limit("POST", "/api/v1/notifications/527/attachments") == 2 * 1024 * 1024 + 64 * 1024
|
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={
|
return httpx.Response(201, json={
|
||||||
"name": "proof.png", "size": len(PNG),
|
"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))
|
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"),
|
("GET", "/api/v1/notifications/threads/527"),
|
||||||
("POST", "/api/v1/repos/stackchain/web/issues/31/assets"),
|
("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
|
@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}};
|
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'}};
|
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}});
|
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;}}
|
if(url.endsWith('/reply') && replyAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}}
|
||||||
return url.endsWith('/reply')?{{id:8}}:{{status:'read'}};
|
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]] == [
|
assert [call["key"] for call in output["calls"][:3]] == [
|
||||||
"reply-image:attachment", "reply-image:reply", "reply-image:reply"
|
"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"}]
|
assert output["result"]["confirmed"] == [{"status": "read"}]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user