258 lines · 9.1 KB · TypeScript
RawDownload
import * as Schema from "effect/Schema";import { describe, expect, it } from "vite-plus/test"; import {  PullRequestActionInput,  PullRequestCapabilities,  PullRequestListInput,  PullRequestListResult,  PullRequestReviewerRequestInput,  resolvePullRequestAuthorFilter,} from "./pullRequest.ts"; const decodeListResult = Schema.decodeUnknownSync(PullRequestListResult);const decodeListInput = Schema.decodeUnknownSync(PullRequestListInput);const decodeReviewerRequest = Schema.decodeUnknownSync(PullRequestReviewerRequestInput);const decodeAction = Schema.decodeUnknownSync(PullRequestActionInput); const LIST_RESULT: PullRequestListResult = {  viewers: { "github.com": "bilal", "gitlab.com": "bilal.hassan" },  providers: [    {      host: "github.com",      kind: "github",      searchesOnHost: true,      projectCount: 1,      configured: true,      detail: null,    },    {      host: "gitlab.com",      kind: "gitlab",      searchesOnHost: true,      projectCount: 1,      configured: false,      detail: "glab is not installed.",    },  ],  entries: [    {      provider: "github",      host: "github.com",      projectId: "project-1" as PullRequestListResult["entries"][number]["projectId"],      projectTitle: "t3code",      repository: "pingdotgg/t3code",      number: 1,      title: "Add a pull requests page",      url: "https://github.com/pingdotgg/t3code/pull/1",      author: { login: "octocat", name: null, avatarUrl: null },      headBranch: "feat/page",      baseBranch: "main",      state: "open",      isDraft: false,      mergeability: "mergeable",      additions: 1,      deletions: 0,      createdAt: "2026-07-01T00:00:00Z",      updatedAt: "2026-07-02T00:00:00Z",      viewerReviewRequested: false,      labels: [{ name: "backend", color: null }],    },  ],  errors: [],  truncated: false,  nextCursors: { "github.com pingdotgg/t3code": "2026-07-02T00:00:00Z|1|1" },}; describe("PullRequestListResult", () => {  /**   * The RPC builds this codec at call time, so a shape it cannot lower — an open-keyed record   * with an optional value, for one — fails as an interrupted request rather than as a schema   * error. Building it here turns that into a test failure instead.   */  it("round-trips through the JSON codec the RPC serializes with", () => {    const codec = Schema.toCodecJson(PullRequestListResult);     const decoded = Schema.decodeUnknownSync(codec)(Schema.encodeUnknownSync(codec)(LIST_RESULT));     expect(decoded).toStrictEqual(LIST_RESULT);  });   it("keys a viewer by host, so two hosts of one kind stay separate accounts", () => {    const decoded = decodeListResult({      ...LIST_RESULT,      viewers: { "github.com": "bilal", "github.acme.dev": "b.hassan" },    });     expect(decoded.viewers["github.com"]).toBe("bilal");    expect(decoded.viewers["github.acme.dev"]).toBe("b.hassan");  });}); describe("PullRequestListInput", () => {  it("trims a search, so what is sent is what was typed", () => {    expect(decodeListInput({ state: "open", query: "  page  " }).query).toBe("page");  });   it("has no search when none was asked for", () => {    expect(decodeListInput({ state: "open" }).query).toBeUndefined();  });   it("bounds a search, because it travels into a command and a query string", () => {    expect(decodeListInput({ state: "open", query: "p".repeat(200) }).query).toHaveLength(200);    expect(() => decodeListInput({ state: "open", query: "p".repeat(201) })).toThrow();  });   it("takes back the continuation a result handed out, keyed the way it arrived", () => {    const cursors = { "github.com pingdotgg/t3code": "2026-07-02T00:00:00Z|99|1,2" };     expect(decodeListInput({ state: "open", cursors }).cursors).toStrictEqual(cursors);  });   it("has no continuation when the listing is being read from the top", () => {    expect(decodeListInput({ state: "open" }).cursors).toBeUndefined();  });   it("bounds a continuation, because it comes back from the page and goes into a filter", () => {    const long = (length: number) => ({ "github.com acme/web": "c".repeat(length) });    expect(decodeListInput({ state: "open", cursors: long(4096) })).toBeDefined();    expect(() => decodeListInput({ state: "open", cursors: long(4097) })).toThrow();  });}); describe("PullRequestReviewerRequestInput", () => {  const ref = { projectId: "p1", repository: "acme/web", number: 1 };  const reviewer = { id: "octocat", kind: "user" };   it("carries the same shape whichever direction the request goes", () => {    // One operation, turned around: `requested` is the whole difference between asking somebody    // for a review and taking the request back.    for (const requested of [true, false]) {      expect(decodeReviewerRequest({ ...ref, reviewers: [reviewer], requested }).requested).toBe(        requested,      );    }  });   it("refuses a request that names nobody, which no host would do anything with", () => {    expect(() => decodeReviewerRequest({ ...ref, reviewers: [], requested: true })).toThrow();  });   it("bounds the reviewers, because they travel into a body the page composed", () => {    const many = (count: number) =>      Array.from({ length: count }, (_, index) => ({ id: `user${index}`, kind: "user" }));    expect(      decodeReviewerRequest({ ...ref, reviewers: many(25), requested: true }).reviewers,    ).toHaveLength(25);    expect(() => decodeReviewerRequest({ ...ref, reviewers: many(26), requested: true })).toThrow();  });   it("keeps a team apart from a person, which is a different thing to ask", () => {    expect(      decodeReviewerRequest({        ...ref,        reviewers: [reviewer, { id: "web-platform", kind: "team" }],        requested: true,      }).reviewers.map((entry) => entry.kind),    ).toEqual(["user", "team"]);  });}); describe("updating a branch that has fallen behind its base", () => {  const ref = { projectId: "project-1", repository: "acme/web", number: 7 };   it("carries the way the branch should be brought up to date", () => {    expect(decodeAction({ ...ref, action: "update-branch", updateMethod: "rebase" })).toMatchObject(      {        action: "update-branch",        updateMethod: "rebase",      },    );  });   it("takes the action without a method, which is the host's own default", () => {    expect(decodeAction({ ...ref, action: "update-branch" }).updateMethod).toBeUndefined();  });   it("refuses a way no host offers", () => {    expect(() =>      decodeAction({ ...ref, action: "update-branch", updateMethod: "squash" }),    ).toThrow();  });}); describe("leaving a merge for the host to make once it is ready", () => {  const ref = { projectId: "project-1", repository: "acme/web", number: 7 };   it("carries the strategy the deferred merge should use, as merging now does", () => {    expect(      decodeAction({ ...ref, action: "enable-auto-merge", mergeMethod: "squash" }),    ).toMatchObject({ action: "enable-auto-merge", mergeMethod: "squash" });  });   it("takes the arming back without a strategy, because there is nothing to choose", () => {    expect(decodeAction({ ...ref, action: "disable-auto-merge" }).mergeMethod).toBeUndefined();  });}); describe("reverting a merged pull request", () => {  it("carries the revert action without merge options", () => {    const action = decodeAction({      projectId: "project-1",      repository: "acme/web",      number: 7,      action: "revert",    });     expect(action.action).toBe("revert");    expect(action.mergeMethod).toBeUndefined();  });}); describe("approving fork workflows", () => {  it("carries workflow approval as its own action", () => {    const action = decodeAction({      projectId: "project-1",      repository: "acme/web",      number: 7,      action: "approve-workflows",    });     expect(action.action).toBe("approve-workflows");  });}); describe("PullRequestCapabilities", () => {  const decodeCapabilities = Schema.decodeUnknownSync(PullRequestCapabilities);  const base = {    diff: true,    comment: true,    actions: [],    mergeMethods: [],    search: true,    review: { inlineComment: true, reply: true, resolve: true, verdicts: [] },    reviewers: { request: true, listCandidates: true },  };   it("decodes a server that says nothing about reactions as a server with none", () => {    expect(decodeCapabilities(base).reactions).toBeUndefined();  });}); describe("naming the reader as the author to narrow by", () => {  it("reads me as whoever is signed in, however it is written", () => {    expect(resolvePullRequestAuthorFilter("me", "octocat")).toBe("octocat");    expect(resolvePullRequestAuthorFilter("@me", "octocat")).toBe("octocat");    expect(resolvePullRequestAuthorFilter("  ME  ", "octocat")).toBe("octocat");  });   it("leaves any other name as it was typed", () => {    expect(resolvePullRequestAuthorFilter(" mercedes ", "octocat")).toBe("mercedes");    expect(resolvePullRequestAuthorFilter("me-too", "octocat")).toBe("me-too");  });   it("stands as typed where the host has not said who the reader is", () => {    expect(resolvePullRequestAuthorFilter("me", null)).toBe("me");    expect(resolvePullRequestAuthorFilter("me", "  ")).toBe("me");  });});