feat: add pr view and checks (task 0009)
All checks were successful
CI / test (pull_request) Successful in 36s
CI / test (push) Successful in 37s

Add `pr view <n>` and `pr checks <n>`, built on the truncation machinery
and review fetches from earlier slices.

`pr view` uses the three-call fetch pattern (PR + reviews in parallel, then
the head commit's combined status) so `checks`, `comment_count`, and
`review_count` are in the default output; `--comments`, `--reviews`, and
`--full` behave as on `issue view`, with `--reviews` exposing Gitea's
`official`/`stale` fields plus per-review inline comments.

`pr checks <n>` renders the checks summary line and the `{ name, conclusion }`
rows, or the scalar no-CI message when no statuses exist.

The state→conclusion mapping and summary live in a new `src/checks.ts`;
`commentRows` is extracted into `src/comment.ts` and shared with `issue view`.
This commit was merged in pull request #9.
This commit is contained in:
2026-07-12 20:22:17 -04:00
parent 6333058b72
commit 16d2f29f19
10 changed files with 991 additions and 35 deletions

126
test/pr-checks.test.ts Normal file
View File

@@ -0,0 +1,126 @@
import { afterEach, describe, expect, it } from "vitest";
import { startFixtureServer, type FixtureServer } from "./fixture-server.js";
import { runCliTest, testModeEnv } from "./harness.js";
const PULL_PATH = "/api/v1/repos/testowner/testrepo/pulls/5";
const STATUS_PATH = "/api/v1/repos/testowner/testrepo/commits/abc123/status";
let server: FixtureServer;
afterEach(async () => {
await server.close();
});
describe("pr checks", () => {
it("renders a summary line and a checks list mapping each commit-status state", async () => {
server = await startFixtureServer([
{
method: "GET",
path: PULL_PATH,
body: { number: 5, head: { sha: "abc123", ref: "feature" } },
},
{
method: "GET",
path: STATUS_PATH,
body: {
sha: "abc123",
total_count: 2,
statuses: [
{ context: "build", status: "success" },
{ context: "test", status: "failure" },
],
},
},
]);
const { stdout, exitCode } = await runCliTest(["pr", "checks", "5"], {
env: testModeEnv(server.url),
});
expect(exitCode).toBe(0);
expect(stdout).toContain("summary: 1 passed, 1 failed, 2 total");
expect(stdout).toContain("checks[2]{name,conclusion}:");
expect(stdout).toContain(" build,pass");
expect(stdout).toContain(" test,fail");
expect(stdout).toMatch(/^help\[\d+\]:/m);
});
it("renders a scalar checks line, not a list block, when no statuses are configured", async () => {
server = await startFixtureServer([
{
method: "GET",
path: PULL_PATH,
body: { number: 5, head: { sha: "abc123", ref: "feature" } },
},
{
method: "GET",
path: STATUS_PATH,
body: { sha: "abc123", total_count: 0, statuses: [] },
},
]);
const { stdout, exitCode } = await runCliTest(["pr", "checks", "5"], {
env: testModeEnv(server.url),
});
expect(exitCode).toBe(0);
expect(stdout).toContain(
"checks: 0 passed, 0 failed — this PR has no CI checks configured",
);
expect(stdout).not.toContain("summary:");
expect(stdout).not.toMatch(/checks\[\d+\]\{/);
expect(stdout).toMatch(/^help\[\d+\]:/m);
});
it("maps every commit-status state and folds skipped/pending into the summary when non-zero", async () => {
server = await startFixtureServer([
{
method: "GET",
path: PULL_PATH,
body: { number: 5, head: { sha: "abc123", ref: "feature" } },
},
{
method: "GET",
path: STATUS_PATH,
body: {
sha: "abc123",
total_count: 5,
statuses: [
{ context: "lint", status: "success" },
{ context: "build", status: "failure" },
{ context: "deploy", status: "warning" },
{ context: "e2e", status: "skipped" },
{ context: "docs", status: "pending" },
],
},
},
]);
const { stdout, exitCode } = await runCliTest(["pr", "checks", "5"], {
env: testModeEnv(server.url),
});
expect(exitCode).toBe(0);
expect(stdout).toContain("summary: 1 passed, 2 failed, 1 skipped, 1 pending, 5 total");
expect(stdout).toContain("checks[5]{name,conclusion}:");
expect(stdout).toContain(" lint,pass");
expect(stdout).toContain(" build,fail");
expect(stdout).toContain(" deploy,fail");
expect(stdout).toContain(" e2e,skip");
expect(stdout).toContain(" docs,pending");
});
it("reports a nonexistent PR as PR_NOT_FOUND with exit 1", async () => {
server = await startFixtureServer([
{
method: "GET",
path: "/api/v1/repos/testowner/testrepo/pulls/999",
status: 404,
body: { message: "Not Found" },
},
]);
const { stdout, exitCode } = await runCliTest(["pr", "checks", "999"], {
env: testModeEnv(server.url),
});
expect(exitCode).toBe(1);
expect(stdout).toContain("code: PR_NOT_FOUND");
});
});