426 lines · 14.8 KB · TypeScript
RawDownload
import {  ProjectId,  type ThreadPullRequestLink,  type ThreadPullRequestSnapshot,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import {  legacyLinkedPullRequestOf,  legacyThreadPullRequestKey,  threadPullRequestSearchTerms,  resolveThreadCurrentPullRequest,  resolveThreadPullRequestChains,  resolveThreadPullRequestBadge,  threadPullRequestKeysEqual,} from "./threadPullRequests.ts"; function snapshot(input: Partial<ThreadPullRequestSnapshot> = {}): ThreadPullRequestSnapshot {  return {    state: "open",    title: "Change",    headBranch: "feature",    baseBranch: "main",    isDraft: false,    updatedAt: "2026-01-01T00:00:00.000Z",    syncedAt: "2026-01-01T00:00:00.000Z",    ...input,  };} function link(  number: number,  input: Partial<Omit<ThreadPullRequestLink, "number">> = {},): ThreadPullRequestLink {  return {    host: "github.com",    repository: "pingdotgg/t3code",    number,    url: `https://github.com/pingdotgg/t3code/pull/${number}`,    source: "manual",    linkedAt: `2026-01-01T00:00:${String(number).padStart(2, "0")}.000Z`,    snapshot: null,    stack: null,    ...input,  };} describe("threadPullRequestKeysEqual", () => {  it("ignores host and repository case", () => {    expect(      threadPullRequestKeysEqual(        { host: "GitHub.com", repository: "PingDotGG/t3code", number: 1 },        { host: "github.com", repository: "pingdotgg/t3code", number: 1 },      ),    ).toBe(true);    expect(      threadPullRequestKeysEqual(        { host: "github.com", repository: "pingdotgg/t3code", number: 1 },        { host: "gitlab.com", repository: "pingdotgg/t3code", number: 1 },      ),    ).toBe(false);  });}); describe("resolveThreadCurrentPullRequest", () => {  it("returns null with no visible links", () => {    expect(resolveThreadCurrentPullRequest([])).toBeNull();    expect(resolveThreadCurrentPullRequest([link(1, { source: "stack-dismissed" })])).toBeNull();  });   it("treats an unsynced link as open", () => {    expect(resolveThreadCurrentPullRequest([link(1)])).toMatchObject({      kind: "single",      link: { number: 1 },    });  });   it("prefers the single open link over terminal ones", () => {    const current = resolveThreadCurrentPullRequest([      link(1, { snapshot: snapshot({ state: "merged" }) }),      link(2, { snapshot: snapshot({ state: "open" }) }),      link(3, { snapshot: snapshot({ state: "closed" }) }),    ]);    expect(current).toMatchObject({ kind: "single", link: { number: 2 } });  });   it("reports a stack when several links are open and puts the highest layer on top", () => {    const stack = {      kind: "native" as const,      id: "s1",      number: 1,      url: "https://github.com/pingdotgg/t3code/stacks/1",      base: "main",      layers: [        { number: 10, headBranch: "a", state: "open" as const },        { number: 11, headBranch: "b", state: "open" as const },      ],    };    const current = resolveThreadCurrentPullRequest([      link(11, { snapshot: snapshot(), stack }),      link(10, { snapshot: snapshot(), stack }),    ]);    expect(current).toMatchObject({ kind: "stack", top: { number: 11 } });    if (current?.kind === "stack") {      expect(current.open.map((entry) => entry.number)).toEqual([11, 10]);    }  });   it("orders an open set without stack data by most recent link", () => {    const current = resolveThreadCurrentPullRequest([link(1), link(2)]);    expect(current).toMatchObject({ kind: "stack", top: { number: 2 } });  });   it("falls back to the most recently updated terminal link", () => {    const current = resolveThreadCurrentPullRequest([      link(1, { snapshot: snapshot({ state: "merged", updatedAt: "2026-01-03T00:00:00.000Z" }) }),      link(2, { snapshot: snapshot({ state: "closed", updatedAt: "2026-01-02T00:00:00.000Z" }) }),    ]);    expect(current).toMatchObject({ kind: "single", link: { number: 1 } });  });}); describe("legacyLinkedPullRequestOf", () => {  const identity = {    canonicalKey: "github.com/pingdotgg/t3code",    provider: "github",    displayName: "pingdotgg/t3code",    locator: {      source: "git-remote" as const,      remoteName: "origin",      remoteUrl: "https://github.com/pingdotgg/t3code.git",    },  };  it("projects only links the owning project can route without a host", () => {    expect(legacyLinkedPullRequestOf([link(7)], "project-1" as never, identity)).toEqual({      projectId: "project-1",      repository: "pingdotgg/t3code",      number: 7,      url: "https://github.com/pingdotgg/t3code/pull/7",    });    expect(legacyLinkedPullRequestOf([], "project-1" as never, identity)).toBeNull();  });  it.each([    {      host: "github.enterprise.test",      url: "https://github.enterprise.test/pingdotgg/t3code/pull/7",    },    { repository: "acme/other", url: "https://github.com/acme/other/pull/7" },  ])("omits an unsafe legacy route %j", (foreign) => {    expect(      legacyLinkedPullRequestOf([link(7, foreign)], "project-1" as never, identity),    ).toBeNull();    expect(      legacyLinkedPullRequestOf([link(7, foreign), link(8)], "project-1" as never, identity)        ?.number,    ).toBe(8);  });  it.each([    "dev.azure.com/org-a/project/_git/web",    "ssh.dev.azure.com/v3/org-a/project/web",    "org-a.visualstudio.com/DefaultCollection/project/_git/web",  ])("projects Azure links through the legacy selector for %s", (canonicalKey) => {    const azureIdentity = {      ...identity,      provider: "azure-devops",      canonicalKey,      displayName: canonicalKey.slice(canonicalKey.indexOf("/") + 1),      name: "web",    };    const own = link(7, {      host: "dev.azure.com",      repository: "org-a/project/_git/web",      url: "https://dev.azure.com/org-a/project/_git/web/pullrequest/7",    });    const foreign = link(7, {      host: "dev.azure.com",      repository: "web",      url: "https://dev.azure.com/org-b/project/_git/web/pullrequest/7",      linkedAt: "2026-01-02T00:00:00.000Z",    });    for (const repository of ["web", "org-a/project/_git/web"]) {      expect(        legacyLinkedPullRequestOf(          [{ ...own, repository }, foreign],          "project-1" as never,          azureIdentity,        ),      ).toEqual({ projectId: "project-1", repository: "web", number: 7, url: own.url });    }    expect(legacyLinkedPullRequestOf([foreign], "project-1" as never, azureIdentity)).toBeNull();    expect(legacyThreadPullRequestKey({ ...own, repository: "web" })).toEqual({      host: "dev.azure.com",      repository: "org-a/project/_git/web",      number: 7,    });    expect(      threadPullRequestKeysEqual(own, {        host: "org-a.visualstudio.com",        repository: "DefaultCollection/project/_git/web",        number: 7,      }),    ).toBe(true);  });  it("does not guess when the project identity is unavailable", () => {    expect(legacyLinkedPullRequestOf([link(7)], "project-1" as never, null)).toBeNull();  });}); describe("resolveThreadPullRequestChains", () => {  it("chains links by base → head within a repository, bottom to top", () => {    const chains = resolveThreadPullRequestChains([      link(3, { snapshot: snapshot({ headBranch: "c", baseBranch: "b" }) }),      link(1, { snapshot: snapshot({ headBranch: "a", baseBranch: "main" }) }),      link(2, { snapshot: snapshot({ headBranch: "b", baseBranch: "a" }) }),      link(9, { snapshot: snapshot({ headBranch: "solo", baseBranch: "main" }) }),    ]);    expect(chains.map((chain) => [chain.kind, chain.layers.map((layer) => layer.number)])).toEqual([      ["derived", [1, 2, 3]],      ["derived", [9]],    ]);  });   it("uses the native stack order when the host provides one", () => {    const stack = {      kind: "native" as const,      id: "s1",      number: 1,      url: "https://github.com/pingdotgg/t3code/stacks/1",      base: "main",      layers: [        { number: 5, headBranch: "a", state: "merged" as const },        { number: 6, headBranch: "b", state: "open" as const },      ],    };    const chains = resolveThreadPullRequestChains([      link(6, { snapshot: snapshot({ headBranch: "b", baseBranch: "main" }), stack }),      link(5, {        snapshot: snapshot({ state: "merged", headBranch: "a", baseBranch: "main" }),        stack,      }),      link(8),    ]);    expect(chains.map((chain) => [chain.kind, chain.layers.map((layer) => layer.number)])).toEqual([      ["native", [5, 6]],      ["derived", [8]],    ]);  });}); describe("chain selection and badge state", () => {  it.each([    ["open", false, "open", false, "open"],    ["closed", false, "closed", false, "closed"],    ["open", true, "open", true, "draft"],    ["open", false, "open", true, "open"],    ["closed", false, "open", true, "open"],    ["merged", false, "merged", false, "merged"],    ["merged", false, "closed", true, "closed"],  ] as const)(    "aggregates %s (draft %s) and %s (draft %s) as %s",    (firstState, firstDraft, secondState, secondDraft, state) => {      for (const stacked of [false, true]) {        const links = [          link(1, {            snapshot: snapshot({ state: firstState, isDraft: firstDraft, headBranch: "base" }),          }),          link(2, {            snapshot: snapshot({              state: secondState,              isDraft: secondDraft,              baseBranch: stacked ? "base" : "main",            }),          }),          link(3, { source: "stack-dismissed" }),        ];        expect(resolveThreadPullRequestBadge(links)).toEqual(          stacked            ? { kind: "stack", layers: 2, state }            : { kind: "pull-request", others: 1, state },        );      }    },  );   it.each(["open", "merged", "closed"] as const)(    "targets the top of a derived %s chain despite a later bottom update and link",    (state) => {      const bottom = link(2, {        snapshot: snapshot({ state, headBranch: "base", updatedAt: "2026-02-01T00:00:00.000Z" }),      });      const top = link(1, {        snapshot: snapshot({ state, headBranch: "top", baseBranch: "base" }),      });      expect(resolveThreadCurrentPullRequest([bottom, top])).toMatchObject(        state === "open"          ? { kind: "stack", top: { number: 1 }, open: [top, bottom] }          : { kind: "single", link: { number: 1 } },      );      expect(resolveThreadPullRequestBadge([bottom, top])).toEqual({        kind: "stack",        layers: 2,        state,      });    },  );   it("keeps the top native layer after completion and treats an unsynced layer as open", () => {    const stack = {      kind: "native" as const,      id: "native-1",      number: 1,      url: "https://github.com/pingdotgg/t3code/stacks/1",      base: "main",      layers: [        { number: 2, headBranch: "base", state: "merged" as const },        { number: 1, headBranch: "top", state: "merged" as const },      ],    };    const bottom = link(2, {      stack,      snapshot: snapshot({ state: "merged", updatedAt: "2026-02-01T00:00:00.000Z" }),    });    const top = link(1, { stack, snapshot: snapshot({ state: "merged" }) });    expect(resolveThreadCurrentPullRequest([bottom, top])).toMatchObject({      kind: "single",      link: { number: 1 },    });    expect(resolveThreadPullRequestBadge([bottom, { ...top, snapshot: null }])).toEqual({      kind: "stack",      layers: 2,      state: "open",    });  });   it("uses aggregate state and distinguishes unrelated work from a stack", () => {    const bottom = link(1, { snapshot: snapshot({ state: "merged", headBranch: "base" }) });    const top = link(2, { snapshot: snapshot({ state: "closed", baseBranch: "base" }) });    expect(resolveThreadPullRequestBadge([bottom, top])).toEqual({      kind: "stack",      layers: 2,      state: "closed",    });    expect(resolveThreadPullRequestBadge([bottom, top, link(3)])).toEqual({      kind: "pull-request",      others: 2,      state: "open",    });    expect(resolveThreadPullRequestBadge([link(3, { source: "stack-dismissed" })])).toBeNull();  });   it("keeps branch matching case-sensitive while ignoring repository case", () => {    const bottom = link(1, {      repository: "PingDotGG/T3code",      snapshot: snapshot({ headBranch: "Base" }),    });    const top = link(2, { snapshot: snapshot({ headBranch: "top", baseBranch: "base" }) });    expect(resolveThreadPullRequestChains([bottom, top])).toHaveLength(2);    expect(      resolveThreadPullRequestChains([        bottom,        { ...top, snapshot: snapshot({ headBranch: "top", baseBranch: "Base" }) },      ])[0]?.layers,    ).toEqual([bottom, { ...top, snapshot: snapshot({ headBranch: "top", baseBranch: "Base" }) }]);  });   it("preserves cyclic links without presenting a guessed stack order", () => {    const links = [      link(1, { snapshot: snapshot({ headBranch: "a", baseBranch: "b" }) }),      link(2, { snapshot: snapshot({ headBranch: "b", baseBranch: "a" }) }),    ];    expect(resolveThreadPullRequestChains(links).map((chain) => chain.layers)).toEqual(      links.map((entry) => [entry]),    );    expect(resolveThreadCurrentPullRequest(links)).toMatchObject({      kind: "stack",      top: { number: 2 },    });    expect(resolveThreadPullRequestBadge(links)).toEqual({      kind: "pull-request",      others: 1,      state: "open",    });  });   it("does not guess a parent when a head branch was reused", () => {    const links = [      link(1, { snapshot: snapshot({ state: "merged", headBranch: "reused" }) }),      link(2, { snapshot: snapshot({ headBranch: "reused" }) }),      link(3, { snapshot: snapshot({ headBranch: "top", baseBranch: "reused" }) }),    ];    expect(resolveThreadPullRequestChains(links).map((chain) => chain.layers)).toEqual(      links.map((entry) => [entry]),    );  });}); describe("threadPullRequestSearchTerms", () => {  it("includes completed and unsynced links but excludes dismissed links", () => {    const terms = threadPullRequestSearchTerms({      pullRequests: [        link(12, { snapshot: snapshot({ title: "Fix login", state: "merged" }) }),        link(34),        link(56, { source: "stack-dismissed" }),      ],    });    expect(terms).toContain("#12");    expect(terms).toContain("pingdotgg/t3code#12");    expect(terms).toContain("https://github.com/pingdotgg/t3code/pull/12");    expect(terms).toContain("Fix login");    expect(terms).toContain("#34");    expect(terms.join(" ")).not.toContain("56");  });}); it("searches the legacy projection when old environments decode to an empty links list", () => {  const linkedPullRequest = {    projectId: ProjectId.make("project"),    repository: "pingdotgg/t3code",    number: 12,    url: "https://github.com/pingdotgg/t3code/pull/12",  };  expect(threadPullRequestSearchTerms({ pullRequests: [], linkedPullRequest })).toContain("#12");  expect(    threadPullRequestSearchTerms({ pullRequests: [link(34)], linkedPullRequest }),  ).not.toContain("#12");});