| ... | @@ -251,7 +251,7 @@ describe("GitHubCli repository resolution", () => { | ... | @@ -251,7 +251,7 @@ describe("GitHubCli repository resolution", () => { |
| 251 | describe("GitHubCli.listPullRequestsByHead", () => { | 251 | describe("GitHubCli.listPullRequestsByHead", () => { |
| 252 | const remotes = remotesOutput(["origin", "git@github.com:acme/web.git"]); | 252 | const remotes = remotesOutput(["origin", "git@github.com:acme/web.git"]); |
| 253 | | 253 | |
| 254 | it.effect("reads heads on one repository in one GraphQL document", () => { | 254 | it.effect("reads a background sweep's staggered heads in one GraphQL document", () => { |
| 255 | const documents: Array<GitHubApi.GitHubGraphQlInput> = []; | 255 | const documents: Array<GitHubApi.GitHubGraphQlInput> = []; |
| 256 | const { layer } = harness({ | 256 | const { layer } = harness({ |
| 257 | remotes, | 257 | remotes, |
| ... | @@ -267,20 +267,23 @@ describe("GitHubCli.listPullRequestsByHead", () => { | ... | @@ -267,20 +267,23 @@ describe("GitHubCli.listPullRequestsByHead", () => { |
| 267 | }); | 267 | }); |
| 268 | return Effect.gen(function* () { | 268 | return Effect.gen(function* () { |
| 269 | const gh = yield* GitHubCli.GitHubCli; | 269 | const gh = yield* GitHubCli.GitHubCli; |
| 270 | const lookups = yield* Effect.all( | 270 | const lookup = (headSelector: string) => |
| 271 | ["feature/a", "feature/b"].map((headSelector) => | 271 | gh |
| 272 | gh.listPullRequestsByHead({ | 272 | .listPullRequestsByHead({ |
| 273 | cwd: "/repo", | 273 | cwd: "/repo", |
| 274 | headSelector, | 274 | headSelector, |
| 275 | state: "all", | 275 | state: "all", |
| 276 | limit: 100, | 276 | limit: 100, |
| 277 | rateLimitHost: "github.com", | 277 | rateLimitHost: "github.com", |
| 278 | }), | 278 | }) |
| 279 | ), | 279 | .pipe(Effect.forkChild); |
| 280 | { concurrency: "unbounded" }, | 280 | // Each branch's own git reads come first, so a sweep's lookups arrive spread out. |
| 281 | ).pipe(Effect.forkChild); | 281 | const firstLookup = yield* lookup("feature/a"); |
| 282 | yield* TestClock.adjust("50 millis"); | 282 | yield* TestClock.adjust("200 millis"); |
| 283 | const [first, second] = yield* Fiber.join(lookups); | 283 | const secondLookup = yield* lookup("feature/b"); |
| | 284 | yield* TestClock.adjust("300 millis"); |
| | 285 | const first = yield* Fiber.join(firstLookup); |
| | 286 | const second = yield* Fiber.join(secondLookup); |
| 284 | assert.deepStrictEqual( | 287 | assert.deepStrictEqual( |
| 285 | first?.map((pr) => pr.number), | 288 | first?.map((pr) => pr.number), |
| 286 | [7], | 289 | [7], |
| ... | @@ -298,6 +301,44 @@ describe("GitHubCli.listPullRequestsByHead", () => { | ... | @@ -298,6 +301,44 @@ describe("GitHubCli.listPullRequestsByHead", () => { |
| 298 | }).pipe(Effect.provide(layer)); | 301 | }).pipe(Effect.provide(layer)); |
| 299 | }); | 302 | }); |
| 300 | | 303 | |
| | 304 | it.effect("caps a background document at twenty-five heads", () => { |
| | 305 | const headCounts: Array<number> = []; |
| | 306 | const { layer } = harness({ |
| | 307 | remotes, |
| | 308 | api: { |
| | 309 | graphql: (input) => |
| | 310 | Effect.sync(() => { |
| | 311 | const heads = Object.keys(input.variables ?? {}).filter((key) => /^h\d+$/.test(key)); |
| | 312 | headCounts.push(heads.length); |
| | 313 | return encodeJson({ |
| | 314 | data: { repository: Object.fromEntries(heads.map((key) => [key, { nodes: [] }])) }, |
| | 315 | }); |
| | 316 | }), |
| | 317 | }, |
| | 318 | }); |
| | 319 | return Effect.gen(function* () { |
| | 320 | const gh = yield* GitHubCli.GitHubCli; |
| | 321 | const lookups = yield* Effect.all( |
| | 322 | Array.from({ length: 26 }, (_, index) => |
| | 323 | gh.listPullRequestsByHead({ |
| | 324 | cwd: "/repo", |
| | 325 | headSelector: `feature/${index}`, |
| | 326 | state: "all", |
| | 327 | limit: 100, |
| | 328 | rateLimitHost: "github.com", |
| | 329 | }), |
| | 330 | ), |
| | 331 | { concurrency: "unbounded" }, |
| | 332 | ).pipe(Effect.forkChild); |
| | 333 | yield* TestClock.adjust("500 millis"); |
| | 334 | yield* Fiber.join(lookups); |
| | 335 | assert.deepStrictEqual( |
| | 336 | headCounts.toSorted((a, b) => a - b), |
| | 337 | [1, 25], |
| | 338 | ); |
| | 339 | }).pipe(Effect.provide(layer)); |
| | 340 | }); |
| | 341 | |
| 301 | it.effect("matches an owner:branch selector on the head owner", () => { | 342 | it.effect("matches an owner:branch selector on the head owner", () => { |
| 302 | const { layer } = harness({ | 343 | const { layer } = harness({ |
| 303 | remotes, | 344 | remotes, |
| ... | @@ -355,10 +396,10 @@ describe("GitHubCli.listPullRequestsByHead", () => { | ... | @@ -355,10 +396,10 @@ describe("GitHubCli.listPullRequestsByHead", () => { |
| 355 | .listPullRequestsByHead({ cwd: "/repo", headSelector, state: "open", limit: 1 }) | 396 | .listPullRequestsByHead({ cwd: "/repo", headSelector, state: "open", limit: 1 }) |
| 356 | .pipe(Effect.flip, Effect.forkChild); | 397 | .pipe(Effect.flip, Effect.forkChild); |
| 357 | const missing = yield* read("missing"); | 398 | const missing = yield* read("missing"); |
| 358 | yield* TestClock.adjust("50 millis"); | 399 | yield* TestClock.adjust("500 millis"); |
| 359 | assert.strictEqual((yield* Fiber.join(missing))._tag, "GitHubCliUnavailableError"); | 400 | assert.strictEqual((yield* Fiber.join(missing))._tag, "GitHubCliUnavailableError"); |
| 360 | const limited = yield* read("limited"); | 401 | const limited = yield* read("limited"); |
| 361 | yield* TestClock.adjust("50 millis"); | 402 | yield* TestClock.adjust("500 millis"); |
| 362 | const error = yield* Fiber.join(limited); | 403 | const error = yield* Fiber.join(limited); |
| 363 | assert.strictEqual(error._tag, "GitHubCliRateLimitError"); | 404 | assert.strictEqual(error._tag, "GitHubCliRateLimitError"); |
| 364 | assert.propertyVal(error, "retryAt", 123); | 405 | assert.propertyVal(error, "retryAt", 123); |