From 66adb653771bfcb0d069f8bbb0344f0cb4361014 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Fri, 14 Aug 2026 14:02:29 +0000 Subject: [PATCH] =?UTF-8?q?test(web):=20gitea=20issue=20creation=20?= =?UTF-8?q?=E2=80=94=20wire=20api=20coverage,=20module=20docs,=20section?= =?UTF-8?q?=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gitea create-issue path (server route, client, wire api, facade, UI button/dialog) landed with the forge user-lookup work; close the remaining verification/documentation gaps: - wire: gitea.test.ts covers issueCreate (POST /api/gitea/issues/create, full/optional body, error throw) - docs: DOCUMENTATION.md lists createIssue client method and the POST /repos/{owner}/{repo}/issues endpoint (labels as names) - ui: fix stale 'read-only by design' JSDoc in GiteaIssuesSection — creation is offered here and the detail view provides edit/close/reopen --- .../views/git/GiteaIssuesSection.tsx | 4 +- .../web/server/lib/gitea/DOCUMENTATION.md | 5 +- packages/web/src/api/gitea.test.ts | 77 +++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/views/git/GiteaIssuesSection.tsx b/packages/ui/src/components/views/git/GiteaIssuesSection.tsx index e7fc6335..890e78a8 100644 --- a/packages/ui/src/components/views/git/GiteaIssuesSection.tsx +++ b/packages/ui/src/components/views/git/GiteaIssuesSection.tsx @@ -18,7 +18,9 @@ const issueLabelBadgeClass = * Open Gitea issues for the context panel's PR view. The list is fetched * lazily (the parent only mounts this component while the Issues tab is * active); selecting a row mounts the shared `ForgeEntityDetailView` for the - * issue detail. Read-only by design — no create, update, or close actions. + * issue detail. Issue creation is supported here via the "new issue" button + * and `ForgeCreateIssueDialog`; the detail view also offers edit, close/reopen, + * comments, and metadata editing through the Gitea provider's write methods. */ export const GiteaIssuesSection: React.FC<{ directory: string }> = ({ directory }) => { const { t } = useI18n(); diff --git a/packages/web/server/lib/gitea/DOCUMENTATION.md b/packages/web/server/lib/gitea/DOCUMENTATION.md index 584df3ac..dbf04495 100644 --- a/packages/web/server/lib/gitea/DOCUMENTATION.md +++ b/packages/web/server/lib/gitea/DOCUMENTATION.md @@ -2,7 +2,7 @@ ## Purpose -- This module owns Gitea/Forgejo auth (Personal Access Token), raw REST v1 client access, remote-URL repo resolution, and Gitea issue / pull-request (PR) APIs for OpenChamber, including PR create/update/merge writes. +- This module owns Gitea/Forgejo auth (Personal Access Token), raw REST v1 client access, remote-URL repo resolution, and Gitea issue / pull-request (PR) APIs for OpenChamber, including issue create/update and PR create/update/merge writes. - From a user perspective, this is the layer that lets the app show Gitea issues and pull requests for a local project, including comments and per-file diffs, and create, edit, and merge pull requests. - Gitea and Forgejo share the same GitHub-style REST v1 API, so this module serves both. Gitea calls remote work **pull requests** (PR), not merge requests. Gitea repos are flat `owner/repo` — there are no multi-segment namespaces. - The module mirrors `packages/web/server/lib/gitlab/` (PAT auth + raw-fetch client) but uses a **Personal Access Token** against the `Authorization: token ` header and a **user-supplied base URL** (Gitea is self-hosted; there is no default instance). @@ -33,7 +33,7 @@ ### Client (`client.js`) -- `createGiteaClient({ token, baseUrl })`: raw-fetch REST v1 client with `request(path, { method, query, body, signal, raw })` plus convenience methods `user()`, `repo(owner, repo)`, `issues(owner, repo, params)`, `issue(owner, repo, number)`, `issueComments(owner, repo, number, params)`, `createIssueComment(owner, repo, number, body)`, `updateIssue(owner, repo, number, params)` (PATCH), `milestones(owner, repo, params)`, `repoLabels(owner, repo, params)`, `pullRequests(owner, repo, params)`, `pullRequest(owner, repo, number)`, `pullRequestDiff(owner, repo, number)` (raw `.diff` text via the `raw` option), `pullRequestFiles(owner, repo, number, params)`, `pullRequestCommits(owner, repo, number, params)`, `pullRequestReviews(owner, repo, number, params)`, `createPullReview(owner, repo, number, params)` (POST), `commitStatuses(owner, repo, sha, params)`, `createPullRequest(owner, repo, body)`, `updatePullRequest(owner, repo, number, body)` (PATCH), `mergePullRequest(owner, repo, number, body)` (POST), `branches(owner, repo, params)`. +- `createGiteaClient({ token, baseUrl })`: raw-fetch REST v1 client with `request(path, { method, query, body, signal, raw })` plus convenience methods `user()`, `repo(owner, repo)`, `issues(owner, repo, params)`, `issue(owner, repo, number)`, `issueComments(owner, repo, number, params)`, `createIssueComment(owner, repo, number, body)`, `createIssue(owner, repo, params)` (POST), `updateIssue(owner, repo, number, params)` (PATCH), `milestones(owner, repo, params)`, `repoLabels(owner, repo, params)`, `pullRequests(owner, repo, params)`, `pullRequest(owner, repo, number)`, `pullRequestDiff(owner, repo, number)` (raw `.diff` text via the `raw` option), `pullRequestFiles(owner, repo, number, params)`, `pullRequestCommits(owner, repo, number, params)`, `pullRequestReviews(owner, repo, number, params)`, `createPullReview(owner, repo, number, params)` (POST), `commitStatuses(owner, repo, sha, params)`, `createPullRequest(owner, repo, body)`, `updatePullRequest(owner, repo, number, body)` (PATCH), `mergePullRequest(owner, repo, number, body)` (POST), `branches(owner, repo, params)`. - `getGiteaClientOrNull()`: client for the current account, or `null`. - `isGiteaRateLimited()` / `noteGiteaRateLimit(error)`: own module-level rate-limit cooldown (not shared with the GitHub/GitLab modules). @@ -67,6 +67,7 @@ - User: `GET /user` -> `{ id, login, full_name, avatar_url, html_url, email, ... }`. - Issue list: `GET /repos/{owner}/{repo}/issues?type=issues&state=open&limit=50&page=N&q=` (`type=issues` excludes pull requests; entries carrying a `pull_request` field are skipped client-side as a backstop). - Issue detail: `GET /repos/{owner}/{repo}/issues/{number}`. +- Issue create: `POST /repos/{owner}/{repo}/issues` with `{ title, body?, labels? }` (labels are label **names**; `body` omitted when absent). - Issue/PR comments: `GET /repos/{owner}/{repo}/issues/{number}/comments`. - PR list: `GET /repos/{owner}/{repo}/pulls?state=open&limit=50&page=N&q=`. Gitea has no server-side source-branch filter, so when `sourceBranch` is requested the route scans `state=all` pages (cap 10 pages) and filters by `head.ref === sourceBranch` client-side, returning all matching states (open and merged). - PR detail: `GET /repos/{owner}/{repo}/pulls/{number}`. diff --git a/packages/web/src/api/gitea.test.ts b/packages/web/src/api/gitea.test.ts index ae5bb9bf..793dfdce 100644 --- a/packages/web/src/api/gitea.test.ts +++ b/packages/web/src/api/gitea.test.ts @@ -260,6 +260,83 @@ describe('createWebGiteaAPI', () => { await expect(api.prMerge({ directory: '/workspace', number: 12 })).rejects.toThrow('Bad Gateway'); }); + it('posts to /api/gitea/issues/create with the input body and returns the created issue', async () => { + const created = { + connected: true, + repo: null, + issue: { + number: 42, + title: 'Broken build', + url: 'https://gitea.example/owner/repo/issues/42', + state: 'open', + author: { username: 'octocat', id: 1 }, + labels: ['bug'], + }, + }; + runtimeFetchMock.mockResolvedValueOnce(Response.json(created)); + + const api = await createAPI(); + await expect(api.issueCreate!({ + directory: '/workspace', + title: 'Broken build', + body: 'The build is failing', + labels: ['bug'], + owner: 'group', + repo: 'repo', + })).resolves.toEqual(created); + + expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitea/issues/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + directory: '/workspace', + title: 'Broken build', + body: 'The build is failing', + labels: ['bug'], + owner: 'group', + repo: 'repo', + }), + }); + }); + + it('omits optional body/labels/owner/repo from the issueCreate body when absent', async () => { + const created = { + connected: true, + repo: null, + issue: { + number: 43, + title: 'Title only', + url: 'https://gitea.example/owner/repo/issues/43', + state: 'open', + author: { username: 'octocat', id: 1 }, + labels: [], + }, + }; + runtimeFetchMock.mockResolvedValueOnce(Response.json(created)); + + const api = await createAPI(); + await expect(api.issueCreate!({ directory: '/workspace', title: 'Title only' })).resolves.toEqual(created); + + expect(runtimeFetchMock).toHaveBeenCalledWith('/api/gitea/issues/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ directory: '/workspace', title: 'Title only' }), + }); + }); + + it('throws the server error when issueCreate fails', async () => { + runtimeFetchMock.mockResolvedValueOnce(Response.json( + { error: 'Your Gitea token needs write:repository scope to create issues' }, + { status: 400 }, + )); + + const api = await createAPI(); + await expect(api.issueCreate!({ + directory: '/workspace', + title: 'Broken build', + })).rejects.toThrow('Your Gitea token needs write:repository scope to create issues'); + }); + it('parses branches and the default branch from repoBranches', async () => { runtimeFetchMock.mockResolvedValueOnce(Response.json({ branches: ['main', 'feat/api'], defaultBranch: 'main' }));