| 1 | import { |
| 2 | ProjectId, |
| 3 | type ThreadPullRequestLink, |
| 4 | type ThreadPullRequestSnapshot, |
| 5 | } from "@t3tools/contracts"; |
| 6 | import { beforeEach, describe, expect, it } from "vite-plus/test"; |
| 7 | |
| 8 | import { |
| 9 | legacyLinkedPullRequestOf, |
| 10 | legacyThreadPullRequestKey, |
| 11 | threadPullRequestSearchTerms, |
| 12 | resolveThreadCurrentPullRequest, |
| 13 | resolveThreadPullRequestChains, |
| 14 | resolveThreadPullRequestBadge, |
| 15 | threadPullRequestKeysEqual, |
| 16 | } from "./threadPullRequests.ts"; |
| 17 | |
| 18 | // Match Hermes: these ES2023 array methods are absent on mobile, and this module runs in |
| 19 | // the home thread list on every launch. |
| 20 | beforeEach(() => { |
| 21 | const methods = ["toSorted", "toReversed", "toSpliced"] as const; |
| 22 | const descriptors = methods.map((method) => |
| 23 | Object.getOwnPropertyDescriptor(Array.prototype, method), |
| 24 | ); |
| 25 | for (const method of methods) Reflect.deleteProperty(Array.prototype, method); |
| 26 | return () => { |
| 27 | for (const [index, method] of methods.entries()) { |
| 28 | const descriptor = descriptors[index]; |
| 29 | if (descriptor) Reflect.defineProperty(Array.prototype, method, descriptor); |
| 30 | } |
| 31 | }; |
| 32 | }); |
| 33 | |
| 34 | function snapshot(input: Partial<ThreadPullRequestSnapshot> = {}): ThreadPullRequestSnapshot { |
| 35 | return { |
| 36 | state: "open", |
| 37 | title: "Change", |
| 38 | headBranch: "feature", |
| 39 | baseBranch: "main", |
| 40 | isDraft: false, |
| 41 | updatedAt: "2026-01-01T00:00:00.000Z", |
| 42 | syncedAt: "2026-01-01T00:00:00.000Z", |
| 43 | ...input, |
| 44 | }; |
| 45 | } |
| 46 | |
| 47 | function link( |
| 48 | number: number, |
| 49 | input: Partial<Omit<ThreadPullRequestLink, "number">> = {}, |
| 50 | ): ThreadPullRequestLink { |
| 51 | return { |
| 52 | host: "github.com", |
| 53 | repository: "pingdotgg/t3code", |
| 54 | number, |
| 55 | url: `https://github.com/pingdotgg/t3code/pull/${number}`, |
| 56 | source: "manual", |
| 57 | linkedAt: `2026-01-01T00:00:${String(number).padStart(2, "0")}.000Z`, |
| 58 | snapshot: null, |
| 59 | stack: null, |
| 60 | ...input, |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | describe("threadPullRequestKeysEqual", () => { |
| 65 | it("recovers Forgejo ports from old stored URLs and keeps separate servers distinct", () => { |
| 66 | const old = link(1, { |
| 67 | host: "forge.example", |
| 68 | repository: "team/repo", |
| 69 | url: "http://forge.example:3000/team/repo/pulls/1", |
| 70 | }); |
| 71 | expect(threadPullRequestKeysEqual(old, { ...old, host: "forge.example:3000" })).toBe(true); |
| 72 | expect( |
| 73 | threadPullRequestKeysEqual(old, { |
| 74 | host: "forge.example:3000", |
| 75 | repository: "team/repo", |
| 76 | number: 1, |
| 77 | }), |
| 78 | ).toBe(true); |
| 79 | expect( |
| 80 | threadPullRequestKeysEqual(old, { |
| 81 | ...old, |
| 82 | url: "http://forge.example:4000/team/repo/pulls/1", |
| 83 | }), |
| 84 | ).toBe(false); |
| 85 | }); |
| 86 | |
| 87 | it("ignores host and repository case", () => { |
| 88 | expect( |
| 89 | threadPullRequestKeysEqual( |
| 90 | { host: "GitHub.com", repository: "PingDotGG/t3code", number: 1 }, |
| 91 | { host: "github.com", repository: "pingdotgg/t3code", number: 1 }, |
| 92 | ), |
| 93 | ).toBe(true); |
| 94 | expect( |
| 95 | threadPullRequestKeysEqual( |
| 96 | { host: "github.com", repository: "pingdotgg/t3code", number: 1 }, |
| 97 | { host: "gitlab.com", repository: "pingdotgg/t3code", number: 1 }, |
| 98 | ), |
| 99 | ).toBe(false); |
| 100 | }); |
| 101 | }); |
| 102 | |
| 103 | describe("resolveThreadCurrentPullRequest", () => { |
| 104 | it("returns null with no visible links", () => { |
| 105 | expect(resolveThreadCurrentPullRequest([])).toBeNull(); |
| 106 | expect(resolveThreadCurrentPullRequest([link(1, { source: "stack-dismissed" })])).toBeNull(); |
| 107 | }); |
| 108 | |
| 109 | it("treats an unsynced link as open", () => { |
| 110 | expect(resolveThreadCurrentPullRequest([link(1)])).toMatchObject({ |
| 111 | kind: "single", |
| 112 | link: { number: 1 }, |
| 113 | }); |
| 114 | }); |
| 115 | |
| 116 | it("prefers the single open link over terminal ones", () => { |
| 117 | const current = resolveThreadCurrentPullRequest([ |
| 118 | link(1, { snapshot: snapshot({ state: "merged" }) }), |
| 119 | link(2, { snapshot: snapshot({ state: "open" }) }), |
| 120 | link(3, { snapshot: snapshot({ state: "closed" }) }), |
| 121 | ]); |
| 122 | expect(current).toMatchObject({ kind: "single", link: { number: 2 } }); |
| 123 | }); |
| 124 | |
| 125 | it("reports a stack when several links are open and puts the highest layer on top", () => { |
| 126 | const stack = { |
| 127 | kind: "native" as const, |
| 128 | id: "s1", |
| 129 | number: 1, |
| 130 | url: "https://github.com/pingdotgg/t3code/stacks/1", |
| 131 | base: "main", |
| 132 | layers: [ |
| 133 | { number: 10, headBranch: "a", state: "open" as const }, |
| 134 | { number: 11, headBranch: "b", state: "open" as const }, |
| 135 | ], |
| 136 | }; |
| 137 | const current = resolveThreadCurrentPullRequest([ |
| 138 | link(11, { snapshot: snapshot(), stack }), |
| 139 | link(10, { snapshot: snapshot(), stack }), |
| 140 | ]); |
| 141 | expect(current).toMatchObject({ kind: "stack", top: { number: 11 } }); |
| 142 | if (current?.kind === "stack") { |
| 143 | expect(current.open.map((entry) => entry.number)).toEqual([11, 10]); |
| 144 | } |
| 145 | }); |
| 146 | |
| 147 | it("orders an open set without stack data by most recent link", () => { |
| 148 | const current = resolveThreadCurrentPullRequest([link(1), link(2)]); |
| 149 | expect(current).toMatchObject({ kind: "stack", top: { number: 2 } }); |
| 150 | }); |
| 151 | |
| 152 | it("falls back to the most recently updated terminal link", () => { |
| 153 | const current = resolveThreadCurrentPullRequest([ |
| 154 | link(1, { snapshot: snapshot({ state: "merged", updatedAt: "2026-01-03T00:00:00.000Z" }) }), |
| 155 | link(2, { snapshot: snapshot({ state: "closed", updatedAt: "2026-01-02T00:00:00.000Z" }) }), |
| 156 | ]); |
| 157 | expect(current).toMatchObject({ kind: "single", link: { number: 1 } }); |
| 158 | }); |
| 159 | }); |
| 160 | |
| 161 | describe("legacyLinkedPullRequestOf", () => { |
| 162 | const identity = { |
| 163 | canonicalKey: "github.com/pingdotgg/t3code", |
| 164 | provider: "github", |
| 165 | displayName: "pingdotgg/t3code", |
| 166 | locator: { |
| 167 | source: "git-remote" as const, |
| 168 | remoteName: "origin", |
| 169 | remoteUrl: "https://github.com/pingdotgg/t3code.git", |
| 170 | }, |
| 171 | }; |
| 172 | it("projects only links the owning project can route without a host", () => { |
| 173 | expect(legacyLinkedPullRequestOf([link(7)], "project-1" as never, identity)).toEqual({ |
| 174 | projectId: "project-1", |
| 175 | repository: "pingdotgg/t3code", |
| 176 | number: 7, |
| 177 | url: "https://github.com/pingdotgg/t3code/pull/7", |
| 178 | }); |
| 179 | expect(legacyLinkedPullRequestOf([], "project-1" as never, identity)).toBeNull(); |
| 180 | }); |
| 181 | it.each([ |
| 182 | { |
| 183 | host: "github.enterprise.test", |
| 184 | url: "https://github.enterprise.test/pingdotgg/t3code/pull/7", |
| 185 | }, |
| 186 | { repository: "acme/other", url: "https://github.com/acme/other/pull/7" }, |
| 187 | ])("omits an unsafe legacy route %j", (foreign) => { |
| 188 | expect( |
| 189 | legacyLinkedPullRequestOf([link(7, foreign)], "project-1" as never, identity), |
| 190 | ).toBeNull(); |
| 191 | expect( |
| 192 | legacyLinkedPullRequestOf([link(7, foreign), link(8)], "project-1" as never, identity) |
| 193 | ?.number, |
| 194 | ).toBe(8); |
| 195 | }); |
| 196 | it.each([ |
| 197 | "dev.azure.com/org-a/project/_git/web", |
| 198 | "ssh.dev.azure.com/v3/org-a/project/web", |
| 199 | "org-a.visualstudio.com/DefaultCollection/project/_git/web", |
| 200 | ])("projects Azure links through the legacy selector for %s", (canonicalKey) => { |
| 201 | const azureIdentity = { |
| 202 | ...identity, |
| 203 | provider: "azure-devops", |
| 204 | canonicalKey, |
| 205 | displayName: canonicalKey.slice(canonicalKey.indexOf("/") + 1), |
| 206 | name: "web", |
| 207 | }; |
| 208 | const own = link(7, { |
| 209 | host: "dev.azure.com", |
| 210 | repository: "org-a/project/_git/web", |
| 211 | url: "https://dev.azure.com/org-a/project/_git/web/pullrequest/7", |
| 212 | }); |
| 213 | const foreign = link(7, { |
| 214 | host: "dev.azure.com", |
| 215 | repository: "web", |
| 216 | url: "https://dev.azure.com/org-b/project/_git/web/pullrequest/7", |
| 217 | linkedAt: "2026-01-02T00:00:00.000Z", |
| 218 | }); |
| 219 | for (const repository of ["web", "org-a/project/_git/web"]) { |
| 220 | expect( |
| 221 | legacyLinkedPullRequestOf( |
| 222 | [{ ...own, repository }, foreign], |
| 223 | "project-1" as never, |
| 224 | azureIdentity, |
| 225 | ), |
| 226 | ).toEqual({ projectId: "project-1", repository: "web", number: 7, url: own.url }); |
| 227 | } |
| 228 | expect(legacyLinkedPullRequestOf([foreign], "project-1" as never, azureIdentity)).toBeNull(); |
| 229 | expect(legacyThreadPullRequestKey({ ...own, repository: "web" })).toEqual({ |
| 230 | host: "dev.azure.com", |
| 231 | repository: "org-a/project/_git/web", |
| 232 | number: 7, |
| 233 | }); |
| 234 | expect( |
| 235 | threadPullRequestKeysEqual(own, { |
| 236 | host: "org-a.visualstudio.com", |
| 237 | repository: "DefaultCollection/project/_git/web", |
| 238 | number: 7, |
| 239 | }), |
| 240 | ).toBe(true); |
| 241 | }); |
| 242 | it("does not guess when the project identity is unavailable", () => { |
| 243 | expect(legacyLinkedPullRequestOf([link(7)], "project-1" as never, null)).toBeNull(); |
| 244 | }); |
| 245 | it("does not route links for a local-path remote with no host or provider", () => { |
| 246 | const localIdentity = { |
| 247 | canonicalKey: "/tmp/r/remote", |
| 248 | displayName: "remote", |
| 249 | locator: { |
| 250 | source: "git-remote" as const, |
| 251 | remoteName: "origin", |
| 252 | remoteUrl: "/tmp/r/remote.git", |
| 253 | }, |
| 254 | }; |
| 255 | expect(legacyLinkedPullRequestOf([link(7)], "project-1" as never, localIdentity)).toBeNull(); |
| 256 | }); |
| 257 | }); |
| 258 | |
| 259 | describe("resolveThreadPullRequestChains", () => { |
| 260 | it("chains links by base → head within a repository, bottom to top", () => { |
| 261 | const chains = resolveThreadPullRequestChains([ |
| 262 | link(3, { snapshot: snapshot({ headBranch: "c", baseBranch: "b" }) }), |
| 263 | link(1, { snapshot: snapshot({ headBranch: "a", baseBranch: "main" }) }), |
| 264 | link(2, { snapshot: snapshot({ headBranch: "b", baseBranch: "a" }) }), |
| 265 | link(9, { snapshot: snapshot({ headBranch: "solo", baseBranch: "main" }) }), |
| 266 | ]); |
| 267 | expect(chains.map((chain) => [chain.kind, chain.layers.map((layer) => layer.number)])).toEqual([ |
| 268 | ["derived", [1, 2, 3]], |
| 269 | ["derived", [9]], |
| 270 | ]); |
| 271 | }); |
| 272 | |
| 273 | it("uses the native stack order when the host provides one", () => { |
| 274 | const stack = { |
| 275 | kind: "native" as const, |
| 276 | id: "s1", |
| 277 | number: 1, |
| 278 | url: "https://github.com/pingdotgg/t3code/stacks/1", |
| 279 | base: "main", |
| 280 | layers: [ |
| 281 | { number: 5, headBranch: "a", state: "merged" as const }, |
| 282 | { number: 6, headBranch: "b", state: "open" as const }, |
| 283 | ], |
| 284 | }; |
| 285 | const chains = resolveThreadPullRequestChains([ |
| 286 | link(6, { snapshot: snapshot({ headBranch: "b", baseBranch: "main" }), stack }), |
| 287 | link(5, { |
| 288 | snapshot: snapshot({ state: "merged", headBranch: "a", baseBranch: "main" }), |
| 289 | stack, |
| 290 | }), |
| 291 | link(8), |
| 292 | ]); |
| 293 | expect(chains.map((chain) => [chain.kind, chain.layers.map((layer) => layer.number)])).toEqual([ |
| 294 | ["native", [5, 6]], |
| 295 | ["derived", [8]], |
| 296 | ]); |
| 297 | }); |
| 298 | }); |
| 299 | |
| 300 | describe("chain selection and badge state", () => { |
| 301 | it.each([ |
| 302 | ["open", false, "open", false, "open"], |
| 303 | ["closed", false, "closed", false, "closed"], |
| 304 | ["open", true, "open", true, "draft"], |
| 305 | ["open", false, "open", true, "open"], |
| 306 | ["closed", false, "open", true, "open"], |
| 307 | ["merged", false, "merged", false, "merged"], |
| 308 | ["merged", false, "closed", true, "closed"], |
| 309 | ] as const)( |
| 310 | "aggregates %s (draft %s) and %s (draft %s) as %s", |
| 311 | (firstState, firstDraft, secondState, secondDraft, state) => { |
| 312 | for (const stacked of [false, true]) { |
| 313 | const links = [ |
| 314 | link(1, { |
| 315 | snapshot: snapshot({ state: firstState, isDraft: firstDraft, headBranch: "base" }), |
| 316 | }), |
| 317 | link(2, { |
| 318 | snapshot: snapshot({ |
| 319 | state: secondState, |
| 320 | isDraft: secondDraft, |
| 321 | baseBranch: stacked ? "base" : "main", |
| 322 | }), |
| 323 | }), |
| 324 | link(3, { source: "stack-dismissed" }), |
| 325 | ]; |
| 326 | expect(resolveThreadPullRequestBadge(links)).toEqual( |
| 327 | stacked |
| 328 | ? { kind: "stack", layers: 2, state } |
| 329 | : { kind: "pull-request", others: 1, state }, |
| 330 | ); |
| 331 | } |
| 332 | }, |
| 333 | ); |
| 334 | |
| 335 | it.each(["open", "merged", "closed"] as const)( |
| 336 | "targets the top of a derived %s chain despite a later bottom update and link", |
| 337 | (state) => { |
| 338 | const bottom = link(2, { |
| 339 | snapshot: snapshot({ state, headBranch: "base", updatedAt: "2026-02-01T00:00:00.000Z" }), |
| 340 | }); |
| 341 | const top = link(1, { |
| 342 | snapshot: snapshot({ state, headBranch: "top", baseBranch: "base" }), |
| 343 | }); |
| 344 | expect(resolveThreadCurrentPullRequest([bottom, top])).toMatchObject( |
| 345 | state === "open" |
| 346 | ? { kind: "stack", top: { number: 1 }, open: [top, bottom] } |
| 347 | : { kind: "single", link: { number: 1 } }, |
| 348 | ); |
| 349 | expect(resolveThreadPullRequestBadge([bottom, top])).toEqual({ |
| 350 | kind: "stack", |
| 351 | layers: 2, |
| 352 | state, |
| 353 | }); |
| 354 | }, |
| 355 | ); |
| 356 | |
| 357 | it("keeps the top native layer after completion and treats an unsynced layer as open", () => { |
| 358 | const stack = { |
| 359 | kind: "native" as const, |
| 360 | id: "native-1", |
| 361 | number: 1, |
| 362 | url: "https://github.com/pingdotgg/t3code/stacks/1", |
| 363 | base: "main", |
| 364 | layers: [ |
| 365 | { number: 2, headBranch: "base", state: "merged" as const }, |
| 366 | { number: 1, headBranch: "top", state: "merged" as const }, |
| 367 | ], |
| 368 | }; |
| 369 | const bottom = link(2, { |
| 370 | stack, |
| 371 | snapshot: snapshot({ state: "merged", updatedAt: "2026-02-01T00:00:00.000Z" }), |
| 372 | }); |
| 373 | const top = link(1, { stack, snapshot: snapshot({ state: "merged" }) }); |
| 374 | expect(resolveThreadCurrentPullRequest([bottom, top])).toMatchObject({ |
| 375 | kind: "single", |
| 376 | link: { number: 1 }, |
| 377 | }); |
| 378 | expect(resolveThreadPullRequestBadge([bottom, { ...top, snapshot: null }])).toEqual({ |
| 379 | kind: "stack", |
| 380 | layers: 2, |
| 381 | state: "open", |
| 382 | }); |
| 383 | }); |
| 384 | |
| 385 | it("uses aggregate state and distinguishes unrelated work from a stack", () => { |
| 386 | const bottom = link(1, { snapshot: snapshot({ state: "merged", headBranch: "base" }) }); |
| 387 | const top = link(2, { snapshot: snapshot({ state: "closed", baseBranch: "base" }) }); |
| 388 | expect(resolveThreadPullRequestBadge([bottom, top])).toEqual({ |
| 389 | kind: "stack", |
| 390 | layers: 2, |
| 391 | state: "closed", |
| 392 | }); |
| 393 | expect(resolveThreadPullRequestBadge([bottom, top, link(3)])).toEqual({ |
| 394 | kind: "pull-request", |
| 395 | others: 2, |
| 396 | state: "open", |
| 397 | }); |
| 398 | expect(resolveThreadPullRequestBadge([link(3, { source: "stack-dismissed" })])).toBeNull(); |
| 399 | }); |
| 400 | |
| 401 | it("keeps branch matching case-sensitive while ignoring repository case", () => { |
| 402 | const bottom = link(1, { |
| 403 | repository: "PingDotGG/T3code", |
| 404 | snapshot: snapshot({ headBranch: "Base" }), |
| 405 | }); |
| 406 | const top = link(2, { snapshot: snapshot({ headBranch: "top", baseBranch: "base" }) }); |
| 407 | expect(resolveThreadPullRequestChains([bottom, top])).toHaveLength(2); |
| 408 | expect( |
| 409 | resolveThreadPullRequestChains([ |
| 410 | bottom, |
| 411 | { ...top, snapshot: snapshot({ headBranch: "top", baseBranch: "Base" }) }, |
| 412 | ])[0]?.layers, |
| 413 | ).toEqual([bottom, { ...top, snapshot: snapshot({ headBranch: "top", baseBranch: "Base" }) }]); |
| 414 | }); |
| 415 | |
| 416 | it("preserves cyclic links without presenting a guessed stack order", () => { |
| 417 | const links = [ |
| 418 | link(1, { snapshot: snapshot({ headBranch: "a", baseBranch: "b" }) }), |
| 419 | link(2, { snapshot: snapshot({ headBranch: "b", baseBranch: "a" }) }), |
| 420 | ]; |
| 421 | expect(resolveThreadPullRequestChains(links).map((chain) => chain.layers)).toEqual( |
| 422 | links.map((entry) => [entry]), |
| 423 | ); |
| 424 | expect(resolveThreadCurrentPullRequest(links)).toMatchObject({ |
| 425 | kind: "stack", |
| 426 | top: { number: 2 }, |
| 427 | }); |
| 428 | expect(resolveThreadPullRequestBadge(links)).toEqual({ |
| 429 | kind: "pull-request", |
| 430 | others: 1, |
| 431 | state: "open", |
| 432 | }); |
| 433 | }); |
| 434 | |
| 435 | it("does not guess a parent when a head branch was reused", () => { |
| 436 | const links = [ |
| 437 | link(1, { snapshot: snapshot({ state: "merged", headBranch: "reused" }) }), |
| 438 | link(2, { snapshot: snapshot({ headBranch: "reused" }) }), |
| 439 | link(3, { snapshot: snapshot({ headBranch: "top", baseBranch: "reused" }) }), |
| 440 | ]; |
| 441 | expect(resolveThreadPullRequestChains(links).map((chain) => chain.layers)).toEqual( |
| 442 | links.map((entry) => [entry]), |
| 443 | ); |
| 444 | }); |
| 445 | }); |
| 446 | |
| 447 | describe("threadPullRequestSearchTerms", () => { |
| 448 | it.each([undefined, [], [link(34)]])( |
| 449 | "searches the branch PR alongside existing links (%j)", |
| 450 | (pullRequests) => { |
| 451 | const branchPullRequest = { |
| 452 | projectId: ProjectId.make("project"), |
| 453 | repository: "pingdotgg/t3code", |
| 454 | number: 11809, |
| 455 | url: "https://github.com/pingdotgg/t3code/pull/11809", |
| 456 | }; |
| 457 | const terms = threadPullRequestSearchTerms({ |
| 458 | pullRequests, |
| 459 | branchPullRequest, |
| 460 | linkedPullRequest: { |
| 461 | ...branchPullRequest, |
| 462 | number: 12, |
| 463 | url: "https://github.com/pingdotgg/t3code/pull/12", |
| 464 | }, |
| 465 | }); |
| 466 | for (const query of ["11809", "#11809", "pingdotgg/t3code#11809", branchPullRequest.url]) { |
| 467 | expect(terms.some((term) => term.includes(query))).toBe(true); |
| 468 | } |
| 469 | expect(terms).toContain(pullRequests?.length ? "#34" : "#12"); |
| 470 | }, |
| 471 | ); |
| 472 | |
| 473 | it("includes completed and unsynced links but excludes dismissed links", () => { |
| 474 | const terms = threadPullRequestSearchTerms({ |
| 475 | pullRequests: [ |
| 476 | link(12, { snapshot: snapshot({ title: "Fix login", state: "merged" }) }), |
| 477 | link(34), |
| 478 | link(56, { source: "stack-dismissed" }), |
| 479 | ], |
| 480 | }); |
| 481 | expect(terms).toContain("#12"); |
| 482 | expect(terms).toContain("pingdotgg/t3code#12"); |
| 483 | expect(terms).toContain("https://github.com/pingdotgg/t3code/pull/12"); |
| 484 | expect(terms).toContain("Fix login"); |
| 485 | expect(terms).toContain("#34"); |
| 486 | expect(terms.join(" ")).not.toContain("56"); |
| 487 | }); |
| 488 | }); |
| 489 | |
| 490 | it("searches the legacy projection when old environments decode to an empty links list", () => { |
| 491 | const linkedPullRequest = { |
| 492 | projectId: ProjectId.make("project"), |
| 493 | repository: "pingdotgg/t3code", |
| 494 | number: 12, |
| 495 | url: "https://github.com/pingdotgg/t3code/pull/12", |
| 496 | }; |
| 497 | expect(threadPullRequestSearchTerms({ pullRequests: [], linkedPullRequest })).toContain("#12"); |
| 498 | expect( |
| 499 | threadPullRequestSearchTerms({ pullRequests: [link(34)], linkedPullRequest }), |
| 500 | ).not.toContain("#12"); |
| 501 | }); |