Ubisam-MobileGit/tasks/010-work-items-api-contract.md
choiminyeong 7d122d4388 Implement TASK-010: pull-request and issue read-only API contracts
Frozen domain/data contracts for issues, pull requests, reviews, and
comments so TASK-008's UI can be built without touching shared layers.

- Separate immutable IssueSummary/Detail and PullRequestSummary/Detail
  models; WorkItemAuthor.Unavailable instead of inventing an identity
  for deleted/null authors.
- PullRequestState derived from Gitea's merged flag + open/closed
  state (no single field already encodes "merged").
- listIssues drops entries with a non-null pull_request field so
  Gitea's issue-list endpoint (which also returns PRs) never
  duplicates listPullRequests, done client-side rather than trusting
  an unverified query parameter.
- Reuses existing Page<T> and ApiFailure rather than new parallel
  types. Read-only: no put/delete/postForm call anywhere in the file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 14:08:21 +09:00

66 lines
6.7 KiB
Markdown

# TASK-010: Implement pull-request and issue API contracts
Status: implemented; all required checks pass; pending Codex review and contract freeze
Owner: Claude (ownership transferred by human product owner)
Reviewer: Codex
## Outcome
Provide frozen read-only domain and data contracts for pull requests, issues, reviews, and comments so Claude can implement TASK-008 without editing shared layers.
## Writable paths
- `app/src/main/java/com/ubisam/ubgitea/domain/workitems/`
- `app/src/main/java/com/ubisam/ubgitea/data/workitems/`
- `app/src/test/java/com/ubisam/ubgitea/domain/workitems/`
- `app/src/test/java/com/ubisam/ubgitea/data/workitems/`
- `tasks/010-work-items-api-contract.md`
## Acceptance criteria
- [x] Pull requests and issues use separate immutable domain models (`IssueSummary`/`IssueDetail` vs `PullRequestSummary`/`PullRequestDetail`; no shared base type, no shared mutable state).
- [x] Lists, reviews, and comments have explicit immutable pagination contracts — reuses the existing `domain.model.Page<T>` (unmodifiable list, explicit `nextPage`) already used by every other paged endpoint in the app, rather than a new pagination type.
- [x] Nullable or deleted authors map without crashes or invented identity — `WorkItemAuthor` is `Known(login, displayName)` or `Unavailable`; a null/blank `user` field maps to `Unavailable`, never a fabricated name.
- [x] 401, 403, 404, 422, rate-limit, network, parse, and server failures map to typed results — reuses the existing `ApiFailure` sealed class (`AuthenticationExpired`, `PermissionDenied`, `NotFound`, `Unprocessable`, `RateLimited`, `Network`, `InvalidResponse`, `Server`) rather than a parallel failure hierarchy.
- [x] Endpoint DTOs remain outside domain and UI packages — all `*Dto` types are `private` nested classes of `data.workitems.GiteaWorkItemsApi`; nothing in `domain.workitems` references Gson or JSON.
- [x] Representative JSON and fake-server tests cover lists, reviews, comments, deleted users, pagination, and failures (see Evidence).
- [x] No create, edit, merge, close, comment, or notification-write endpoint is exposed — `GiteaWorkItemsApi` only calls `HttpTransport.get`; confirmed by grep (no `put`/`delete`/`postForm` call anywhere in the file).
- [ ] Evidence records the reviewed contract-freeze commit for TASK-008 — cannot be closed by this task alone; requires Codex's read-only review of this commit first, per the same freeze convention used for TASK-005→006.
## Required checks
```text
./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug
```
## Evidence
Implemented on branch `claude/task-010-work-items-contract` from commit `525a0aa` (latest reviewed Claude work at the time; base contracts unchanged from `0929282`).
- New files (writable scope only): `domain/workitems/WorkItemModels.kt`, `domain/workitems/WorkItemsRepository.kt`, `data/workitems/GiteaWorkItemsApi.kt`, `data/workitems/GiteaWorkItemsApiTest.kt`.
- Design: `IssueSummary`/`IssueDetail` and `PullRequestSummary`/`PullRequestDetail` are separate data classes. `PullRequestState` is derived from Gitea's `merged` boolean plus the issue-level `open`/`closed` state (`merged=true` → `MERGED`; else `state` maps directly), matching Gitea's actual struct shape (there is no single `state` enum value that already means "merged"). `ReviewState` maps five known Gitea values case-insensitively and falls back to `OTHER` for anything unrecognized instead of failing the whole review list closed — reviews are decorative status, not identity- or safety-critical, so this is intentionally looser than the fail-closed convention used elsewhere (documented in a code comment).
- Gitea quirk handled: `GET /repos/{owner}/{repo}/issues` can also return pull requests (a PR is internally an issue with a non-null `pull_request` field). `listIssues` drops any entry with that field set, confirmed by a test with a mixed issue+PR response. This is done client-side rather than via a guessed/unverified `type=` query parameter, so correctness does not depend on an assumption about Gitea's exact filter semantics; the accepted tradeoff (documented in a class-level comment) is that a filtered page can return fewer than `pageSize` genuine issues.
- PR/issue discussion comments both use `/issues/{number}/comments` (Gitea treats PRs as issues internally for this endpoint), confirmed by a test asserting the exact URL path segments from `listPullRequestComments`.
- Reused rather than duplicated: `domain.model.Page<T>` for pagination, `core.network.ApiFailure`/`requireSuccess()` for typed failures, `data.repositories.BearerTokenProvider` for the token-supply contract — no parallel types were introduced for things the app already has.
Test coverage (12 cases in `GiteaWorkItemsApiTest`): issue list excludes PRs and maps state/author/comments (1), issue detail maps labels/assignees/body (1), deleted-author comment maps to `Unavailable` without crashing (1), PR state derivation across open/merged/closed (1), PR detail maps base/head/mergeable independently (1), review state known-values-plus-`OTHER`-fallback (1), PR comments reuse the issue-comments URL (1), malformed required field fails closed (1), missing token fails closed as `AuthenticationExpired` (1), HTTP status mapping (1), no-next-page-when-under-page-size (1), plus URL/component-encoding assertions folded into the list/comments tests using an owner/repo containing a space and `+`/`#`.
Checks run from the new worktree (`local.properties` created locally, not committed):
```text
./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.data.workitems.GiteaWorkItemsApiTest" → BUILD SUCCESSFUL, 12/12 passed
./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL
- 128 unit tests across 24 suites, 0 failures/errors
- 0 lint errors
git diff --check → clean
sensitive-pattern scan of changed files → no matches
```
Known limitations:
- Gitea's exact query-parameter behavior for filtering pull requests out of the issue-list endpoint was not confirmed against live documentation (uncertain across versions); the client-side `pull_request`-field filter used instead is correct regardless of that behavior, at the cost of potentially-short pages after filtering.
- No physical device or live Gitea instance was used; all coverage is fake-transport/unit-level, consistent with how TASK-004/009 were verified before their own device/contract-freeze review.
No open P0-P3 findings from self-review against the acceptance criteria. Requesting Codex read-only review and contract-freeze recording so TASK-008 can begin.