ProvenPin Install

Octokit REST client with MSW: pagination, early stop and errors: working versions (tested 2026-10-15)

Test an @octokit/rest client offline with MSW handlers for api.github.com: paginate through Link headers, stop paging early with done(), create issues with auth, and map 404 RequestErrors.

passed4 passed · 0 failed2026-10-15 20:09 UTC
Status
passed
Last passed
2026-10-15 20:09 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
@octokit/rest@22.0.1@types/node@26.6.5tsx@4.23.15msw@3.0.3
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38799498972

The code

Exactly the files that passed. Your agent can pull them with get_recipe("octokit-rest-msw-pagination") (install), or see them on GitHub.

package.json
{
  "name": "octokit-rest-msw-pagination",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "@octokit/rest": "22.0.1"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15",
    "msw": "3.0.3"
  }
}
src/github.ts
import { Octokit } from "@octokit/rest";

export function makeClient(token: string, baseUrl = "https://api.github.com") {
  return new Octokit({ auth: token, baseUrl, userAgent: "provenpin-recipe/1.0", request: { retries: 0 } });
}

/** All open issues (not PRs) across every page, following the Link header. */
export async function listOpenIssues(gh: Octokit, owner: string, repo: string) {
  const items = await gh.paginate(gh.rest.issues.listForRepo, { owner, repo, state: "open", per_page: 2 });
  return items.filter((i) => !i.pull_request).map((i) => ({ number: i.number, title: i.title }));
}

/** Stop paging early once we find a label, using paginate's done() callback. */
export async function firstWithLabel(gh: Octokit, owner: string, repo: string, label: string) {
  let hit: number | null = null;
  await gh.paginate(gh.rest.issues.listForRepo, { owner, repo, per_page: 2 }, (res, done) => {
    const f = res.data.find((i) => i.labels.some((l) => (typeof l === "string" ? l : l.name) === label));
    if (f) { hit = f.number; done(); }
    return [];
  });
  return hit;
}

export async function openIssue(gh: Octokit, owner: string, repo: string, title: string, labels: string[]) {
  const { data, status } = await gh.rest.issues.create({ owner, repo, title, labels });
  return { status, number: data.number, url: data.html_url };
}

export async function repoExists(gh: Octokit, owner: string, repo: string) {
  try {
    await gh.rest.repos.get({ owner, repo });
    return true;
  } catch (e) {
    if ((e as { status?: number }).status === 404) return false;
    throw e;
  }
}
test/github.test.ts
import { test, before, after, afterEach } from "node:test";
import assert from "node:assert/strict";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { makeClient, listOpenIssues, firstWithLabel, openIssue, repoExists } from "../src/github.ts";

const API = "https://api.github.com";
const all = [
  { number: 1, title: "one", labels: [] },
  { number: 2, title: "two (pr)", labels: [], pull_request: { url: "x" } },
  { number: 3, title: "three", labels: [{ name: "bug" }] },
  { number: 4, title: "four", labels: [] },
  { number: 5, title: "five", labels: [{ name: "docs" }] },
];
const pagesServed: number[] = [];

const server = setupServer(
  http.get(`${API}/repos/acme/app/issues`, ({ request }) => {
    const u = new URL(request.url);
    const page = Number(u.searchParams.get("page") ?? 1), per = Number(u.searchParams.get("per_page"));
    pagesServed.push(page);
    const last = Math.ceil(all.length / per);
    const link = (p: number, rel: string) => `<${API}/repos/acme/app/issues?per_page=${per}&page=${p}>; rel="${rel}"`;
    const links = [page < last && link(page + 1, "next"), link(last, "last")].filter(Boolean).join(", ");
    return HttpResponse.json(all.slice((page - 1) * per, page * per), { headers: { link: links } });
  }),
  http.post(`${API}/repos/acme/app/issues`, async ({ request }) => {
    assert.equal(request.headers.get("authorization"), "token test-token");
    const body = (await request.json()) as { title: string; labels: string[] };
    return HttpResponse.json({ number: 42, title: body.title, labels: body.labels, html_url: "https://github.com/acme/app/issues/42" }, { status: 201 });
  }),
  http.get(`${API}/repos/acme/:repo`, ({ params }) =>
    params.repo === "app" ? HttpResponse.json({ id: 1, name: "app" }) : HttpResponse.json({ message: "Not Found" }, { status: 404 }),
  ),
);

before(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); pagesServed.length = 0; });
after(() => server.close());

test("paginate follows Link headers across all pages", async () => {
  const issues = await listOpenIssues(makeClient("test-token"), "acme", "app");
  assert.deepEqual(issues.map((i) => i.number), [1, 3, 4, 5]);
  assert.deepEqual(pagesServed, [1, 2, 3]);
});

test("done() stops paging early", async () => {
  assert.equal(await firstWithLabel(makeClient("t"), "acme", "app", "bug"), 3);
  assert.deepEqual(pagesServed, [1, 2]);
});

test("create sends auth and JSON body", async () => {
  assert.deepEqual(await openIssue(makeClient("test-token"), "acme", "app", "Broken link", ["bug"]), {
    status: 201, number: 42, url: "https://github.com/acme/app/issues/42",
  });
});

test("404 surfaces as RequestError status", async () => {
  const gh = makeClient("t");
  assert.equal(await repoExists(gh, "acme", "app"), true);
  assert.equal(await repoExists(gh, "acme", "gone"), false);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.