diff --git a/batch/github/fetcher.test.ts b/batch/github/fetcher.test.ts index 3a14dbf9..d092e7bb 100644 --- a/batch/github/fetcher.test.ts +++ b/batch/github/fetcher.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from 'vitest' import type { ShapedTimelineItem } from './model' // 純粋関数なので直接 import してテスト -const { buildRequestedAtMap, createFetcher, shapeTagNode } = +const { buildRequestedAtMap, createFetcher, paginateGraphQL, shapeTagNode } = await import('./fetcher') describe('buildRequestedAtMap', () => { @@ -263,3 +263,83 @@ describe('shapeTagNode', () => { expect(result).toBeNull() }) }) + +describe('paginateGraphQL shouldStop', () => { + type Node = { number: number; updatedAt: string } + const makeGraphqlFn = (pages: Node[][]) => { + let callIndex = 0 + return (_vars: Record) => { + const nodes = pages[callIndex++] ?? [] + return Promise.resolve({ + pullRequests: { + nodes, + pageInfo: { + hasNextPage: callIndex < pages.length, + endCursor: `cursor-${callIndex}`, + }, + }, + }) + } + } + type Result = Awaited>> + + const extractConnection = (r: Result) => r.pullRequests + const processNode = (n: Node) => n + + test('excludes node with updatedAt equal to stopBefore', async () => { + const stopBefore = '2026-04-01T00:00:00Z' + const pages: Node[][] = [ + [ + { number: 3, updatedAt: '2026-04-02T00:00:00Z' }, + { number: 2, updatedAt: '2026-04-01T00:00:00Z' }, // == stopBefore → 除外 + { number: 1, updatedAt: '2026-03-31T00:00:00Z' }, + ], + ] + const result = await paginateGraphQL( + makeGraphqlFn(pages), + extractConnection, + processNode, + { shouldStop: (node) => node.updatedAt <= stopBefore }, + ) + expect(result).toEqual([{ number: 3, updatedAt: '2026-04-02T00:00:00Z' }]) + }) + + test('includes nodes newer than stopBefore', async () => { + const stopBefore = '2026-04-01T00:00:00Z' + const pages: Node[][] = [ + [ + { number: 5, updatedAt: '2026-04-03T00:00:00Z' }, + { number: 4, updatedAt: '2026-04-02T00:00:00Z' }, + { number: 3, updatedAt: '2026-03-31T00:00:00Z' }, // older → stop + ], + ] + const result = await paginateGraphQL( + makeGraphqlFn(pages), + extractConnection, + processNode, + { shouldStop: (node) => node.updatedAt <= stopBefore }, + ) + expect(result).toEqual([ + { number: 5, updatedAt: '2026-04-03T00:00:00Z' }, + { number: 4, updatedAt: '2026-04-02T00:00:00Z' }, + ]) + }) + + test('returns all nodes when shouldStop is not provided', async () => { + const pages: Node[][] = [ + [ + { number: 1, updatedAt: '2026-04-01T00:00:00Z' }, + { number: 2, updatedAt: '2026-03-01T00:00:00Z' }, + ], + ] + const result = await paginateGraphQL( + makeGraphqlFn(pages), + extractConnection, + processNode, + ) + expect(result).toEqual([ + { number: 1, updatedAt: '2026-04-01T00:00:00Z' }, + { number: 2, updatedAt: '2026-03-01T00:00:00Z' }, + ]) + }) +}) diff --git a/batch/github/fetcher.ts b/batch/github/fetcher.ts index b860b1d4..86c5f8d3 100644 --- a/batch/github/fetcher.ts +++ b/batch/github/fetcher.ts @@ -761,7 +761,7 @@ interface PaginateOptions { * extractConnection でレスポンスからノード配列と pageInfo を取り出し、 * processNode でノードをアイテムに変換する。 */ -async function paginateGraphQL( +export async function paginateGraphQL( graphqlFn: (variables: Record) => Promise, extractConnection: ( result: TResult, @@ -1157,7 +1157,7 @@ export const createFetcher = ({ owner, repo, octokit }: createFetcherProps) => { label: 'pullrequestList()', // ISO 8601 UTC 文字列同士なので lexicographic 比較 = 時系列比較 shouldStop: stopBefore - ? (node) => node.updatedAt < stopBefore + ? (node) => node.updatedAt <= stopBefore : undefined, }, )