github-issue-tower-defence-management 1.123.3 → 1.125.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -11
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +21 -36
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/githubGraphqlClient.js +99 -0
- package/bin/adapter/repositories/githubGraphqlClient.js.map +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +278 -304
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +79 -142
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/projectConfig.ts +6 -11
- package/src/adapter/repositories/GraphqlProjectRepository.ts +81 -77
- package/src/adapter/repositories/githubGraphqlClient.test.ts +251 -0
- package/src/adapter/repositories/githubGraphqlClient.ts +122 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +663 -328
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +465 -449
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +62 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +208 -237
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/githubGraphqlClient.d.ts +21 -0
- package/types/adapter/repositories/githubGraphqlClient.d.ts.map +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +2 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { mock } from 'jest-mock-extended';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ApiV3CheerioRestIssueRepository,
|
|
4
|
+
REQUIRED_CHECKS_CACHE_TTL_MS,
|
|
5
|
+
} from './ApiV3CheerioRestIssueRepository';
|
|
3
6
|
import { ApiV3IssueRepository } from './ApiV3IssueRepository';
|
|
4
7
|
import { RestIssueRepository } from './RestIssueRepository';
|
|
5
8
|
import {
|
|
@@ -1965,74 +1968,202 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
1965
1968
|
});
|
|
1966
1969
|
});
|
|
1967
1970
|
|
|
1971
|
+
const jsonResponse = (body: object): Response =>
|
|
1972
|
+
new Response(JSON.stringify(body), {
|
|
1973
|
+
status: 200,
|
|
1974
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
const toResponse = (value: Response | object): Response =>
|
|
1978
|
+
value instanceof Response ? value : jsonResponse(value);
|
|
1979
|
+
|
|
1980
|
+
type FetchRoutes = {
|
|
1981
|
+
timeline?: () => Response | object;
|
|
1982
|
+
mergeability?: () => Response | object;
|
|
1983
|
+
slimPullRequest?: (variables: {
|
|
1984
|
+
prNumber: number;
|
|
1985
|
+
reviewThreadsAfter: string | null;
|
|
1986
|
+
}) => Response | object;
|
|
1987
|
+
branchRules?: (url: string) => Response | object;
|
|
1988
|
+
branchDetail?: (url: string) => Response | object;
|
|
1989
|
+
checkRuns?: (url: string) => Response | object;
|
|
1990
|
+
combinedStatus?: (url: string) => Response | object;
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
const requestUrlOf = (input: RequestInfo | URL): string =>
|
|
1994
|
+
typeof input === 'string'
|
|
1995
|
+
? input
|
|
1996
|
+
: input instanceof URL
|
|
1997
|
+
? input.toString()
|
|
1998
|
+
: input.url;
|
|
1999
|
+
|
|
2000
|
+
const requestBodyOf = (init?: RequestInit): string =>
|
|
2001
|
+
typeof init?.body === 'string' ? init.body : '';
|
|
2002
|
+
|
|
2003
|
+
type GraphqlRequestBody = {
|
|
2004
|
+
query: string;
|
|
2005
|
+
variables: {
|
|
2006
|
+
prNumber: number;
|
|
2007
|
+
reviewThreadsAfter: string | null;
|
|
2008
|
+
};
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
const parseGraphqlRequestBody = (init?: RequestInit): GraphqlRequestBody => {
|
|
2012
|
+
const parsed: unknown = JSON.parse(requestBodyOf(init));
|
|
2013
|
+
if (
|
|
2014
|
+
typeof parsed !== 'object' ||
|
|
2015
|
+
parsed === null ||
|
|
2016
|
+
!('query' in parsed) ||
|
|
2017
|
+
typeof parsed.query !== 'string'
|
|
2018
|
+
) {
|
|
2019
|
+
throw new Error('Unexpected GraphQL request body in test');
|
|
2020
|
+
}
|
|
2021
|
+
const rawVariables: unknown =
|
|
2022
|
+
'variables' in parsed ? parsed.variables : null;
|
|
2023
|
+
const variables =
|
|
2024
|
+
typeof rawVariables === 'object' && rawVariables !== null
|
|
2025
|
+
? rawVariables
|
|
2026
|
+
: {};
|
|
2027
|
+
const prNumber =
|
|
2028
|
+
'prNumber' in variables && typeof variables.prNumber === 'number'
|
|
2029
|
+
? variables.prNumber
|
|
2030
|
+
: 0;
|
|
2031
|
+
const reviewThreadsAfter =
|
|
2032
|
+
'reviewThreadsAfter' in variables &&
|
|
2033
|
+
typeof variables.reviewThreadsAfter === 'string'
|
|
2034
|
+
? variables.reviewThreadsAfter
|
|
2035
|
+
: null;
|
|
2036
|
+
return { query: parsed.query, variables: { prNumber, reviewThreadsAfter } };
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
const mockFetchRoutes = (routes: FetchRoutes) =>
|
|
2040
|
+
jest
|
|
2041
|
+
.spyOn(global, 'fetch')
|
|
2042
|
+
.mockImplementation(
|
|
2043
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
2044
|
+
const url = requestUrlOf(input);
|
|
2045
|
+
if (url === 'https://api.github.com/graphql') {
|
|
2046
|
+
const body = parseGraphqlRequestBody(init);
|
|
2047
|
+
if (body.query.includes('timelineItems') && routes.timeline) {
|
|
2048
|
+
return toResponse(routes.timeline());
|
|
2049
|
+
}
|
|
2050
|
+
if (
|
|
2051
|
+
body.query.includes('mergeStateStatus') &&
|
|
2052
|
+
routes.mergeability
|
|
2053
|
+
) {
|
|
2054
|
+
return toResponse(routes.mergeability());
|
|
2055
|
+
}
|
|
2056
|
+
if (body.query.includes('headRefOid') && routes.slimPullRequest) {
|
|
2057
|
+
return toResponse(routes.slimPullRequest(body.variables));
|
|
2058
|
+
}
|
|
2059
|
+
throw new Error(`Unexpected GraphQL query in test: ${body.query}`);
|
|
2060
|
+
}
|
|
2061
|
+
if (url.includes('/rules/branches/')) {
|
|
2062
|
+
return toResponse(
|
|
2063
|
+
routes.branchRules ? routes.branchRules(url) : [],
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (/\/branches\/[^/?]+$/.test(url)) {
|
|
2067
|
+
return toResponse(
|
|
2068
|
+
routes.branchDetail ? routes.branchDetail(url) : {},
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
if (url.includes('/check-runs')) {
|
|
2072
|
+
return toResponse(
|
|
2073
|
+
routes.checkRuns
|
|
2074
|
+
? routes.checkRuns(url)
|
|
2075
|
+
: { total_count: 0, check_runs: [] },
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
if (url.includes('/status?')) {
|
|
2079
|
+
return toResponse(
|
|
2080
|
+
routes.combinedStatus
|
|
2081
|
+
? routes.combinedStatus(url)
|
|
2082
|
+
: { statuses: [] },
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
throw new Error(`Unexpected fetch URL in test: ${url}`);
|
|
2086
|
+
},
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
type FetchSpy = ReturnType<typeof mockFetchRoutes>;
|
|
2090
|
+
|
|
2091
|
+
const countCallsMatching = (
|
|
2092
|
+
fetchSpy: FetchSpy,
|
|
2093
|
+
predicate: (url: string, body: string) => boolean,
|
|
2094
|
+
): number =>
|
|
2095
|
+
fetchSpy.mock.calls.filter(([input, init]) =>
|
|
2096
|
+
predicate(requestUrlOf(input), requestBodyOf(init)),
|
|
2097
|
+
).length;
|
|
2098
|
+
|
|
2099
|
+
const countMergeabilityQueries = (fetchSpy: FetchSpy): number =>
|
|
2100
|
+
countCallsMatching(
|
|
2101
|
+
fetchSpy,
|
|
2102
|
+
(url, body) =>
|
|
2103
|
+
url === 'https://api.github.com/graphql' &&
|
|
2104
|
+
body.includes('mergeStateStatus'),
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
const buildSlimPullRequestResponse = (
|
|
2108
|
+
overrides: {
|
|
2109
|
+
url?: string;
|
|
2110
|
+
state?: string;
|
|
2111
|
+
isDraft?: boolean;
|
|
2112
|
+
headRefName?: string;
|
|
2113
|
+
baseRefName?: string;
|
|
2114
|
+
mergeable?: string;
|
|
2115
|
+
headRefOid?: string;
|
|
2116
|
+
reviewThreads?: {
|
|
2117
|
+
pageInfo: { endCursor: string | null; hasNextPage: boolean };
|
|
2118
|
+
nodes: Array<{ isResolved: boolean }>;
|
|
2119
|
+
};
|
|
2120
|
+
} = {},
|
|
2121
|
+
) => ({
|
|
2122
|
+
data: {
|
|
2123
|
+
repository: {
|
|
2124
|
+
pullRequest: {
|
|
2125
|
+
url:
|
|
2126
|
+
overrides.url ??
|
|
2127
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2128
|
+
state: overrides.state ?? 'OPEN',
|
|
2129
|
+
isDraft: overrides.isDraft ?? false,
|
|
2130
|
+
headRefName: overrides.headRefName ?? 'feature-branch',
|
|
2131
|
+
baseRefName: overrides.baseRefName ?? 'main',
|
|
2132
|
+
mergeable: overrides.mergeable ?? 'MERGEABLE',
|
|
2133
|
+
headRefOid: overrides.headRefOid ?? 'headsha123',
|
|
2134
|
+
reviewThreads: overrides.reviewThreads ?? {
|
|
2135
|
+
pageInfo: { endCursor: null, hasNextPage: false },
|
|
2136
|
+
nodes: [],
|
|
2137
|
+
},
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
});
|
|
2142
|
+
|
|
1968
2143
|
describe('getOpenPullRequest CI state computation', () => {
|
|
1969
2144
|
afterEach(() => {
|
|
1970
2145
|
jest.restoreAllMocks();
|
|
1971
2146
|
});
|
|
1972
2147
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
pullRequest: {
|
|
1984
|
-
url: 'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
1985
|
-
state: 'OPEN',
|
|
1986
|
-
isDraft: false,
|
|
1987
|
-
headRefName: 'dependabot/npm_and_yarn/some-package-2.0.0',
|
|
1988
|
-
baseRefName: 'main',
|
|
1989
|
-
mergeable: 'MERGEABLE',
|
|
1990
|
-
baseRepository: {
|
|
1991
|
-
branchProtectionRules: { nodes: [] },
|
|
1992
|
-
defaultBranchRef: { name: 'main' },
|
|
1993
|
-
rulesets: { nodes: [] },
|
|
2148
|
+
it('returns isCiStateSuccess true when the latest check run per name is success even though an older run has failure', async () => {
|
|
2149
|
+
mockFetchRoutes({
|
|
2150
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2151
|
+
checkRuns: () => ({
|
|
2152
|
+
total_count: 2,
|
|
2153
|
+
check_runs: [
|
|
2154
|
+
{
|
|
2155
|
+
id: 100,
|
|
2156
|
+
name: 'check_pull_requests_to_link_issues',
|
|
2157
|
+
conclusion: 'failure',
|
|
1994
2158
|
},
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
statusCheckRollup: {
|
|
2000
|
-
contexts: {
|
|
2001
|
-
nodes: checkRunNodes,
|
|
2002
|
-
},
|
|
2003
|
-
},
|
|
2004
|
-
},
|
|
2005
|
-
},
|
|
2006
|
-
],
|
|
2159
|
+
{
|
|
2160
|
+
id: 200,
|
|
2161
|
+
name: 'check_pull_requests_to_link_issues',
|
|
2162
|
+
conclusion: 'success',
|
|
2007
2163
|
},
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
},
|
|
2012
|
-
});
|
|
2013
|
-
|
|
2014
|
-
it('returns isCiStateSuccess true when latest CheckRun per name is SUCCESS even though an older run has FAILURE', async () => {
|
|
2015
|
-
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
2016
|
-
new Response(
|
|
2017
|
-
JSON.stringify(
|
|
2018
|
-
buildGraphqlPrResponse([
|
|
2019
|
-
{
|
|
2020
|
-
__typename: 'CheckRun',
|
|
2021
|
-
databaseId: 100,
|
|
2022
|
-
name: 'check_pull_requests_to_link_issues',
|
|
2023
|
-
conclusion: 'FAILURE',
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
__typename: 'CheckRun',
|
|
2027
|
-
databaseId: 200,
|
|
2028
|
-
name: 'check_pull_requests_to_link_issues',
|
|
2029
|
-
conclusion: 'SUCCESS',
|
|
2030
|
-
},
|
|
2031
|
-
]),
|
|
2032
|
-
),
|
|
2033
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
2034
|
-
),
|
|
2035
|
-
);
|
|
2164
|
+
],
|
|
2165
|
+
}),
|
|
2166
|
+
});
|
|
2036
2167
|
|
|
2037
2168
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2038
2169
|
const result = await repository.getOpenPullRequest(
|
|
@@ -2044,29 +2175,40 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2044
2175
|
expect(result?.isPassedAllCiJob).toBe(true);
|
|
2045
2176
|
});
|
|
2046
2177
|
|
|
2047
|
-
it('returns isCiStateSuccess false when latest
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
conclusion: 'FAILURE',
|
|
2063
|
-
},
|
|
2064
|
-
]),
|
|
2065
|
-
),
|
|
2066
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
2067
|
-
),
|
|
2178
|
+
it('returns isCiStateSuccess false when the latest check run per name has failure conclusion', async () => {
|
|
2179
|
+
mockFetchRoutes({
|
|
2180
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2181
|
+
checkRuns: () => ({
|
|
2182
|
+
total_count: 2,
|
|
2183
|
+
check_runs: [
|
|
2184
|
+
{ id: 100, name: 'ci', conclusion: 'success' },
|
|
2185
|
+
{ id: 200, name: 'ci', conclusion: 'failure' },
|
|
2186
|
+
],
|
|
2187
|
+
}),
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2191
|
+
const result = await repository.getOpenPullRequest(
|
|
2192
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2068
2193
|
);
|
|
2069
2194
|
|
|
2195
|
+
expect(result).not.toBeNull();
|
|
2196
|
+
expect(result?.isCiStateSuccess).toBe(false);
|
|
2197
|
+
expect(result?.isPassedAllCiJob).toBe(false);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
it('returns isCiStateSuccess false when the latest check run per name has null conclusion (still running)', async () => {
|
|
2201
|
+
mockFetchRoutes({
|
|
2202
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2203
|
+
checkRuns: () => ({
|
|
2204
|
+
total_count: 2,
|
|
2205
|
+
check_runs: [
|
|
2206
|
+
{ id: 100, name: 'ci', conclusion: 'failure' },
|
|
2207
|
+
{ id: 200, name: 'ci', conclusion: null },
|
|
2208
|
+
],
|
|
2209
|
+
}),
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2070
2212
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2071
2213
|
const result = await repository.getOpenPullRequest(
|
|
2072
2214
|
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
@@ -2077,29 +2219,28 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2077
2219
|
expect(result?.isPassedAllCiJob).toBe(false);
|
|
2078
2220
|
});
|
|
2079
2221
|
|
|
2080
|
-
it('returns isCiStateSuccess false when
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
{
|
|
2092
|
-
__typename: 'CheckRun',
|
|
2093
|
-
databaseId: 200,
|
|
2094
|
-
name: 'ci',
|
|
2095
|
-
conclusion: null,
|
|
2096
|
-
},
|
|
2097
|
-
]),
|
|
2098
|
-
),
|
|
2099
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
2100
|
-
),
|
|
2222
|
+
it('returns isCiStateSuccess false when a commit status context has failure state', async () => {
|
|
2223
|
+
mockFetchRoutes({
|
|
2224
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2225
|
+
combinedStatus: () => ({
|
|
2226
|
+
statuses: [{ context: 'external-ci', state: 'failure' }],
|
|
2227
|
+
}),
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2231
|
+
const result = await repository.getOpenPullRequest(
|
|
2232
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2101
2233
|
);
|
|
2102
2234
|
|
|
2235
|
+
expect(result).not.toBeNull();
|
|
2236
|
+
expect(result?.isCiStateSuccess).toBe(false);
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
it('returns isCiStateSuccess false when the head commit has no check runs and no commit statuses', async () => {
|
|
2240
|
+
mockFetchRoutes({
|
|
2241
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2103
2244
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2104
2245
|
const result = await repository.getOpenPullRequest(
|
|
2105
2246
|
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
@@ -2110,90 +2251,214 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2110
2251
|
expect(result?.isPassedAllCiJob).toBe(false);
|
|
2111
2252
|
});
|
|
2112
2253
|
|
|
2113
|
-
it('
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
defaultBranchRef: { name: 'main' },
|
|
2129
|
-
rulesets: { nodes: [] },
|
|
2130
|
-
},
|
|
2131
|
-
commits: {
|
|
2132
|
-
nodes: [
|
|
2133
|
-
{
|
|
2134
|
-
commit: {
|
|
2135
|
-
statusCheckRollup: {
|
|
2136
|
-
contexts: {
|
|
2137
|
-
nodes: [
|
|
2138
|
-
{
|
|
2139
|
-
__typename: 'StatusContext',
|
|
2140
|
-
context: 'external-ci',
|
|
2141
|
-
state: 'FAILURE',
|
|
2142
|
-
},
|
|
2143
|
-
],
|
|
2144
|
-
},
|
|
2145
|
-
},
|
|
2146
|
-
},
|
|
2147
|
-
},
|
|
2148
|
-
],
|
|
2149
|
-
},
|
|
2150
|
-
reviewThreads: { nodes: [] },
|
|
2151
|
-
},
|
|
2152
|
-
},
|
|
2153
|
-
},
|
|
2154
|
-
}),
|
|
2155
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
2156
|
-
),
|
|
2254
|
+
it('combines REST check runs and commit statuses into one CI success evaluation', async () => {
|
|
2255
|
+
mockFetchRoutes({
|
|
2256
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2257
|
+
checkRuns: () => ({
|
|
2258
|
+
total_count: 1,
|
|
2259
|
+
check_runs: [{ id: 1, name: 'unit-test', conclusion: 'success' }],
|
|
2260
|
+
}),
|
|
2261
|
+
combinedStatus: () => ({
|
|
2262
|
+
statuses: [{ context: 'external-ci', state: 'success' }],
|
|
2263
|
+
}),
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2267
|
+
const result = await repository.getOpenPullRequest(
|
|
2268
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2157
2269
|
);
|
|
2158
2270
|
|
|
2271
|
+
expect(result).not.toBeNull();
|
|
2272
|
+
expect(result?.isCiStateSuccess).toBe(true);
|
|
2273
|
+
expect(result?.isPassedAllCiJob).toBe(true);
|
|
2274
|
+
});
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
describe('getOpenPullRequest required check resolution via REST', () => {
|
|
2278
|
+
afterEach(() => {
|
|
2279
|
+
jest.restoreAllMocks();
|
|
2280
|
+
});
|
|
2281
|
+
|
|
2282
|
+
it('reports missing required checks merged from branch rules and classic branch protection', async () => {
|
|
2283
|
+
mockFetchRoutes({
|
|
2284
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2285
|
+
branchRules: () => [
|
|
2286
|
+
{
|
|
2287
|
+
type: 'required_status_checks',
|
|
2288
|
+
parameters: {
|
|
2289
|
+
required_status_checks: [{ context: 'ruleset-check' }],
|
|
2290
|
+
},
|
|
2291
|
+
},
|
|
2292
|
+
{ type: 'deletion' },
|
|
2293
|
+
],
|
|
2294
|
+
branchDetail: () => ({
|
|
2295
|
+
protection: {
|
|
2296
|
+
required_status_checks: { contexts: ['classic-check'] },
|
|
2297
|
+
},
|
|
2298
|
+
}),
|
|
2299
|
+
checkRuns: () => ({
|
|
2300
|
+
total_count: 1,
|
|
2301
|
+
check_runs: [{ id: 1, name: 'ruleset-check', conclusion: 'success' }],
|
|
2302
|
+
}),
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2159
2305
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2160
2306
|
const result = await repository.getOpenPullRequest(
|
|
2161
2307
|
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2162
2308
|
);
|
|
2163
2309
|
|
|
2164
2310
|
expect(result).not.toBeNull();
|
|
2165
|
-
expect(result?.
|
|
2311
|
+
expect(result?.missingRequiredCheckNames).toEqual(['classic-check']);
|
|
2312
|
+
expect(result?.isCiStateSuccess).toBe(true);
|
|
2313
|
+
expect(result?.isPassedAllCiJob).toBe(false);
|
|
2166
2314
|
});
|
|
2167
2315
|
|
|
2168
|
-
it('
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
state: 'OPEN',
|
|
2177
|
-
isDraft: false,
|
|
2178
|
-
headRefName: 'feature-branch',
|
|
2179
|
-
baseRefName: 'main',
|
|
2180
|
-
mergeable: 'MERGEABLE',
|
|
2181
|
-
baseRepository: {
|
|
2182
|
-
branchProtectionRules: { nodes: [] },
|
|
2183
|
-
defaultBranchRef: { name: 'main' },
|
|
2184
|
-
rulesets: { nodes: [] },
|
|
2185
|
-
},
|
|
2186
|
-
commits: {
|
|
2187
|
-
nodes: [{ commit: { statusCheckRollup: null } }],
|
|
2188
|
-
},
|
|
2189
|
-
reviewThreads: { nodes: [] },
|
|
2190
|
-
},
|
|
2191
|
-
},
|
|
2316
|
+
it('passes all required checks when every required check name has reported on the head commit', async () => {
|
|
2317
|
+
mockFetchRoutes({
|
|
2318
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2319
|
+
branchRules: () => [
|
|
2320
|
+
{
|
|
2321
|
+
type: 'required_status_checks',
|
|
2322
|
+
parameters: {
|
|
2323
|
+
required_status_checks: [{ context: 'ruleset-check' }],
|
|
2192
2324
|
},
|
|
2325
|
+
},
|
|
2326
|
+
],
|
|
2327
|
+
branchDetail: () => ({
|
|
2328
|
+
protection: {
|
|
2329
|
+
required_status_checks: { contexts: ['classic-check'] },
|
|
2330
|
+
},
|
|
2331
|
+
}),
|
|
2332
|
+
checkRuns: () => ({
|
|
2333
|
+
total_count: 2,
|
|
2334
|
+
check_runs: [
|
|
2335
|
+
{ id: 1, name: 'ruleset-check', conclusion: 'success' },
|
|
2336
|
+
{ id: 2, name: 'classic-check', conclusion: 'success' },
|
|
2337
|
+
],
|
|
2338
|
+
}),
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2342
|
+
const result = await repository.getOpenPullRequest(
|
|
2343
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2344
|
+
);
|
|
2345
|
+
|
|
2346
|
+
expect(result).not.toBeNull();
|
|
2347
|
+
expect(result?.missingRequiredCheckNames).toEqual([]);
|
|
2348
|
+
expect(result?.isPassedAllCiJob).toBe(true);
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
describe('required check names TTL cache', () => {
|
|
2353
|
+
afterEach(() => {
|
|
2354
|
+
jest.restoreAllMocks();
|
|
2355
|
+
});
|
|
2356
|
+
|
|
2357
|
+
it('fetches branch rules only once for repeated calls on the same base branch within the TTL', async () => {
|
|
2358
|
+
const fetchSpy = mockFetchRoutes({
|
|
2359
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
const { repository, dateRepository } =
|
|
2363
|
+
createApiV3CheerioRestIssueRepository();
|
|
2364
|
+
dateRepository.now.mockResolvedValue(
|
|
2365
|
+
new Date('2026-01-01T00:00:00.000Z'),
|
|
2366
|
+
);
|
|
2367
|
+
await repository.getOpenPullRequest(
|
|
2368
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2369
|
+
);
|
|
2370
|
+
await repository.getOpenPullRequest(
|
|
2371
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2372
|
+
);
|
|
2373
|
+
|
|
2374
|
+
expect(
|
|
2375
|
+
countCallsMatching(fetchSpy, (url) => url.includes('/rules/branches/')),
|
|
2376
|
+
).toBe(1);
|
|
2377
|
+
expect(
|
|
2378
|
+
countCallsMatching(fetchSpy, (url) => /\/branches\/[^/?]+$/.test(url)),
|
|
2379
|
+
).toBe(1);
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
it('fetches branch rules again after the TTL has expired', async () => {
|
|
2383
|
+
const fetchSpy = mockFetchRoutes({
|
|
2384
|
+
slimPullRequest: () => buildSlimPullRequestResponse(),
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
const { repository, dateRepository } =
|
|
2388
|
+
createApiV3CheerioRestIssueRepository();
|
|
2389
|
+
const baseTimeMs = new Date('2026-01-01T00:00:00.000Z').getTime();
|
|
2390
|
+
dateRepository.now
|
|
2391
|
+
.mockResolvedValueOnce(new Date(baseTimeMs))
|
|
2392
|
+
.mockResolvedValueOnce(
|
|
2393
|
+
new Date(baseTimeMs + REQUIRED_CHECKS_CACHE_TTL_MS + 1),
|
|
2394
|
+
);
|
|
2395
|
+
await repository.getOpenPullRequest(
|
|
2396
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2397
|
+
);
|
|
2398
|
+
await repository.getOpenPullRequest(
|
|
2399
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
expect(
|
|
2403
|
+
countCallsMatching(fetchSpy, (url) => url.includes('/rules/branches/')),
|
|
2404
|
+
).toBe(2);
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
it('fetches branch rules separately for different base branches (cache miss)', async () => {
|
|
2408
|
+
const fetchSpy = mockFetchRoutes({
|
|
2409
|
+
slimPullRequest: (variables) =>
|
|
2410
|
+
buildSlimPullRequestResponse({
|
|
2411
|
+
url: `https://github.com/HiromiShikata/test-repository/pull/${variables.prNumber}`,
|
|
2412
|
+
baseRefName: variables.prNumber === 31 ? 'main' : 'develop',
|
|
2193
2413
|
}),
|
|
2194
|
-
|
|
2195
|
-
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
const { repository, dateRepository } =
|
|
2417
|
+
createApiV3CheerioRestIssueRepository();
|
|
2418
|
+
dateRepository.now.mockResolvedValue(
|
|
2419
|
+
new Date('2026-01-01T00:00:00.000Z'),
|
|
2196
2420
|
);
|
|
2421
|
+
await repository.getOpenPullRequest(
|
|
2422
|
+
'https://github.com/HiromiShikata/test-repository/pull/31',
|
|
2423
|
+
);
|
|
2424
|
+
await repository.getOpenPullRequest(
|
|
2425
|
+
'https://github.com/HiromiShikata/test-repository/pull/32',
|
|
2426
|
+
);
|
|
2427
|
+
|
|
2428
|
+
expect(
|
|
2429
|
+
countCallsMatching(fetchSpy, (url) =>
|
|
2430
|
+
url.includes('/rules/branches/main'),
|
|
2431
|
+
),
|
|
2432
|
+
).toBe(1);
|
|
2433
|
+
expect(
|
|
2434
|
+
countCallsMatching(fetchSpy, (url) =>
|
|
2435
|
+
url.includes('/rules/branches/develop'),
|
|
2436
|
+
),
|
|
2437
|
+
).toBe(1);
|
|
2438
|
+
});
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
describe('getOpenPullRequest review thread paging', () => {
|
|
2442
|
+
afterEach(() => {
|
|
2443
|
+
jest.restoreAllMocks();
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
it('fetches every review thread page and evaluates resolution over all pages', async () => {
|
|
2447
|
+
const fetchSpy = mockFetchRoutes({
|
|
2448
|
+
slimPullRequest: (variables) =>
|
|
2449
|
+
buildSlimPullRequestResponse({
|
|
2450
|
+
reviewThreads:
|
|
2451
|
+
variables.reviewThreadsAfter === null
|
|
2452
|
+
? {
|
|
2453
|
+
pageInfo: { endCursor: 'cursor-1', hasNextPage: true },
|
|
2454
|
+
nodes: [{ isResolved: true }],
|
|
2455
|
+
}
|
|
2456
|
+
: {
|
|
2457
|
+
pageInfo: { endCursor: null, hasNextPage: false },
|
|
2458
|
+
nodes: [{ isResolved: false }],
|
|
2459
|
+
},
|
|
2460
|
+
}),
|
|
2461
|
+
});
|
|
2197
2462
|
|
|
2198
2463
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2199
2464
|
const result = await repository.getOpenPullRequest(
|
|
@@ -2201,8 +2466,25 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2201
2466
|
);
|
|
2202
2467
|
|
|
2203
2468
|
expect(result).not.toBeNull();
|
|
2204
|
-
expect(result?.
|
|
2205
|
-
expect(
|
|
2469
|
+
expect(result?.isResolvedAllReviewComments).toBe(false);
|
|
2470
|
+
expect(
|
|
2471
|
+
countCallsMatching(
|
|
2472
|
+
fetchSpy,
|
|
2473
|
+
(url, body) =>
|
|
2474
|
+
url === 'https://api.github.com/graphql' &&
|
|
2475
|
+
body.includes('headRefOid'),
|
|
2476
|
+
),
|
|
2477
|
+
).toBe(2);
|
|
2478
|
+
const secondSlimCall = fetchSpy.mock.calls
|
|
2479
|
+
.map(([input, init]) =>
|
|
2480
|
+
requestUrlOf(input) === 'https://api.github.com/graphql'
|
|
2481
|
+
? parseGraphqlRequestBody(init)
|
|
2482
|
+
: null,
|
|
2483
|
+
)
|
|
2484
|
+
.filter(
|
|
2485
|
+
(body) => body !== null && body.query.includes('headRefOid'),
|
|
2486
|
+
)[1];
|
|
2487
|
+
expect(secondSlimCall?.variables.reviewThreadsAfter).toBe('cursor-1');
|
|
2206
2488
|
});
|
|
2207
2489
|
});
|
|
2208
2490
|
|
|
@@ -2211,37 +2493,33 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2211
2493
|
jest.restoreAllMocks();
|
|
2212
2494
|
});
|
|
2213
2495
|
|
|
2496
|
+
const buildPullRequestTimelineNode = (
|
|
2497
|
+
prNumber: number,
|
|
2498
|
+
mergeable: string,
|
|
2499
|
+
) => ({
|
|
2500
|
+
__typename: 'CrossReferencedEvent',
|
|
2501
|
+
willCloseTarget: true,
|
|
2502
|
+
source: {
|
|
2503
|
+
__typename: 'PullRequest',
|
|
2504
|
+
url: `https://github.com/HiromiShikata/test-repository/pull/${prNumber}`,
|
|
2505
|
+
number: prNumber,
|
|
2506
|
+
state: 'OPEN',
|
|
2507
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
2508
|
+
isDraft: false,
|
|
2509
|
+
mergeable,
|
|
2510
|
+
headRefName: 'feature-branch',
|
|
2511
|
+
baseRefName: 'main',
|
|
2512
|
+
baseRef: { name: 'main' },
|
|
2513
|
+
},
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2214
2516
|
const buildTimelineResponse = (mergeable: string) => ({
|
|
2215
2517
|
data: {
|
|
2216
2518
|
repository: {
|
|
2217
2519
|
issue: {
|
|
2218
2520
|
timelineItems: {
|
|
2219
2521
|
pageInfo: { endCursor: null, hasNextPage: false },
|
|
2220
|
-
nodes: [
|
|
2221
|
-
{
|
|
2222
|
-
__typename: 'CrossReferencedEvent',
|
|
2223
|
-
willCloseTarget: true,
|
|
2224
|
-
source: {
|
|
2225
|
-
__typename: 'PullRequest',
|
|
2226
|
-
url: 'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2227
|
-
number: 11148,
|
|
2228
|
-
state: 'OPEN',
|
|
2229
|
-
createdAt: '2024-01-01T00:00:00Z',
|
|
2230
|
-
isDraft: false,
|
|
2231
|
-
mergeable,
|
|
2232
|
-
headRefName: 'feature-branch',
|
|
2233
|
-
baseRefName: 'main',
|
|
2234
|
-
baseRepository: {
|
|
2235
|
-
branchProtectionRules: { nodes: [] },
|
|
2236
|
-
defaultBranchRef: { name: 'main' },
|
|
2237
|
-
rulesets: { nodes: [] },
|
|
2238
|
-
},
|
|
2239
|
-
commits: { nodes: [] },
|
|
2240
|
-
reviewThreads: { nodes: [] },
|
|
2241
|
-
baseRef: { name: 'main' },
|
|
2242
|
-
},
|
|
2243
|
-
},
|
|
2244
|
-
],
|
|
2522
|
+
nodes: [buildPullRequestTimelineNode(11148, mergeable)],
|
|
2245
2523
|
},
|
|
2246
2524
|
},
|
|
2247
2525
|
},
|
|
@@ -2262,56 +2540,35 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2262
2540
|
},
|
|
2263
2541
|
});
|
|
2264
2542
|
|
|
2543
|
+
const slimForPrNumber = (variables: { prNumber: number }) =>
|
|
2544
|
+
buildSlimPullRequestResponse({
|
|
2545
|
+
url: `https://github.com/HiromiShikata/test-repository/pull/${variables.prNumber}`,
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2265
2548
|
it('resolves isConflicted true via a direct query when the timeline node reports mergeable UNKNOWN but the direct query reports CONFLICTING', async () => {
|
|
2266
|
-
const fetchSpy =
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2272
|
-
}),
|
|
2273
|
-
)
|
|
2274
|
-
.mockResolvedValueOnce(
|
|
2275
|
-
new Response(
|
|
2276
|
-
JSON.stringify(buildMergeabilityResponse('CONFLICTING', 'DIRTY')),
|
|
2277
|
-
{
|
|
2278
|
-
status: 200,
|
|
2279
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2280
|
-
},
|
|
2281
|
-
),
|
|
2282
|
-
);
|
|
2549
|
+
const fetchSpy = mockFetchRoutes({
|
|
2550
|
+
timeline: () => buildTimelineResponse('UNKNOWN'),
|
|
2551
|
+
mergeability: () => buildMergeabilityResponse('CONFLICTING', 'DIRTY'),
|
|
2552
|
+
slimPullRequest: slimForPrNumber,
|
|
2553
|
+
});
|
|
2283
2554
|
|
|
2284
2555
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2285
2556
|
const result = await repository.findRelatedOpenPRs(
|
|
2286
2557
|
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2287
2558
|
);
|
|
2288
2559
|
|
|
2289
|
-
expect(fetchSpy).
|
|
2560
|
+
expect(countMergeabilityQueries(fetchSpy)).toBe(1);
|
|
2290
2561
|
expect(result).toHaveLength(1);
|
|
2291
2562
|
expect(result[0].isConflicted).toBe(true);
|
|
2292
2563
|
expect(result[0].mergeable).toBe('CONFLICTING');
|
|
2293
2564
|
});
|
|
2294
2565
|
|
|
2295
2566
|
it('resolves isConflicted true when the direct query returns mergeable UNKNOWN but mergeStateStatus DIRTY', async () => {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2302
|
-
}),
|
|
2303
|
-
)
|
|
2304
|
-
.mockImplementation(() =>
|
|
2305
|
-
Promise.resolve(
|
|
2306
|
-
new Response(
|
|
2307
|
-
JSON.stringify(buildMergeabilityResponse('UNKNOWN', 'DIRTY')),
|
|
2308
|
-
{
|
|
2309
|
-
status: 200,
|
|
2310
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2311
|
-
},
|
|
2312
|
-
),
|
|
2313
|
-
),
|
|
2314
|
-
);
|
|
2567
|
+
mockFetchRoutes({
|
|
2568
|
+
timeline: () => buildTimelineResponse('UNKNOWN'),
|
|
2569
|
+
mergeability: () => buildMergeabilityResponse('UNKNOWN', 'DIRTY'),
|
|
2570
|
+
slimPullRequest: slimForPrNumber,
|
|
2571
|
+
});
|
|
2315
2572
|
|
|
2316
2573
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2317
2574
|
const result = await repository.findRelatedOpenPRs(
|
|
@@ -2323,82 +2580,39 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2323
2580
|
});
|
|
2324
2581
|
|
|
2325
2582
|
it('keeps isConflicted false when mergeability stays UNKNOWN after the bounded retries', async () => {
|
|
2326
|
-
const fetchSpy =
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2332
|
-
}),
|
|
2333
|
-
)
|
|
2334
|
-
.mockImplementation(() =>
|
|
2335
|
-
Promise.resolve(
|
|
2336
|
-
new Response(
|
|
2337
|
-
JSON.stringify(buildMergeabilityResponse('UNKNOWN', 'UNKNOWN')),
|
|
2338
|
-
{
|
|
2339
|
-
status: 200,
|
|
2340
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2341
|
-
},
|
|
2342
|
-
),
|
|
2343
|
-
),
|
|
2344
|
-
);
|
|
2583
|
+
const fetchSpy = mockFetchRoutes({
|
|
2584
|
+
timeline: () => buildTimelineResponse('UNKNOWN'),
|
|
2585
|
+
mergeability: () => buildMergeabilityResponse('UNKNOWN', 'UNKNOWN'),
|
|
2586
|
+
slimPullRequest: slimForPrNumber,
|
|
2587
|
+
});
|
|
2345
2588
|
|
|
2346
2589
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2347
2590
|
const result = await repository.findRelatedOpenPRs(
|
|
2348
2591
|
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2349
2592
|
);
|
|
2350
2593
|
|
|
2351
|
-
expect(fetchSpy).
|
|
2594
|
+
expect(countMergeabilityQueries(fetchSpy)).toBe(3);
|
|
2352
2595
|
expect(result).toHaveLength(1);
|
|
2353
2596
|
expect(result[0].isConflicted).toBe(false);
|
|
2354
2597
|
});
|
|
2355
2598
|
|
|
2356
2599
|
it('does not issue a direct mergeability query when the timeline node already reports a definitive mergeable value', async () => {
|
|
2357
|
-
const fetchSpy =
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}),
|
|
2362
|
-
);
|
|
2600
|
+
const fetchSpy = mockFetchRoutes({
|
|
2601
|
+
timeline: () => buildTimelineResponse('MERGEABLE'),
|
|
2602
|
+
slimPullRequest: slimForPrNumber,
|
|
2603
|
+
});
|
|
2363
2604
|
|
|
2364
2605
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2365
2606
|
const result = await repository.findRelatedOpenPRs(
|
|
2366
2607
|
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2367
2608
|
);
|
|
2368
2609
|
|
|
2369
|
-
expect(fetchSpy).
|
|
2610
|
+
expect(countMergeabilityQueries(fetchSpy)).toBe(0);
|
|
2370
2611
|
expect(result).toHaveLength(1);
|
|
2371
2612
|
expect(result[0].isConflicted).toBe(false);
|
|
2372
2613
|
expect(result[0].mergeable).toBe('MERGEABLE');
|
|
2373
2614
|
});
|
|
2374
2615
|
|
|
2375
|
-
const buildPullRequestTimelineNode = (
|
|
2376
|
-
prNumber: number,
|
|
2377
|
-
mergeable: string,
|
|
2378
|
-
) => ({
|
|
2379
|
-
__typename: 'CrossReferencedEvent',
|
|
2380
|
-
willCloseTarget: true,
|
|
2381
|
-
source: {
|
|
2382
|
-
__typename: 'PullRequest',
|
|
2383
|
-
url: `https://github.com/HiromiShikata/test-repository/pull/${prNumber}`,
|
|
2384
|
-
number: prNumber,
|
|
2385
|
-
state: 'OPEN',
|
|
2386
|
-
createdAt: '2024-01-01T00:00:00Z',
|
|
2387
|
-
isDraft: false,
|
|
2388
|
-
mergeable,
|
|
2389
|
-
headRefName: 'feature-branch',
|
|
2390
|
-
baseRefName: 'main',
|
|
2391
|
-
baseRepository: {
|
|
2392
|
-
branchProtectionRules: { nodes: [] },
|
|
2393
|
-
defaultBranchRef: { name: 'main' },
|
|
2394
|
-
rulesets: { nodes: [] },
|
|
2395
|
-
},
|
|
2396
|
-
commits: { nodes: [] },
|
|
2397
|
-
reviewThreads: { nodes: [] },
|
|
2398
|
-
baseRef: { name: 'main' },
|
|
2399
|
-
},
|
|
2400
|
-
});
|
|
2401
|
-
|
|
2402
2616
|
const buildTwoPrTimelineResponse = () => ({
|
|
2403
2617
|
data: {
|
|
2404
2618
|
repository: {
|
|
@@ -2419,33 +2633,21 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2419
2633
|
const consoleInfoSpy = jest
|
|
2420
2634
|
.spyOn(console, 'info')
|
|
2421
2635
|
.mockImplementation(() => undefined);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2428
|
-
}),
|
|
2429
|
-
)
|
|
2430
|
-
.mockResolvedValueOnce(
|
|
2431
|
-
new Response(
|
|
2432
|
-
JSON.stringify({
|
|
2433
|
-
data: { repository: { pullRequest: null } },
|
|
2434
|
-
errors: [
|
|
2435
|
-
{
|
|
2436
|
-
type: 'NOT_FOUND',
|
|
2437
|
-
path: ['repository', 'pullRequest'],
|
|
2438
|
-
message:
|
|
2439
|
-
'Could not resolve to a PullRequest with the number of 11148.',
|
|
2440
|
-
},
|
|
2441
|
-
],
|
|
2442
|
-
}),
|
|
2636
|
+
mockFetchRoutes({
|
|
2637
|
+
timeline: () => buildTwoPrTimelineResponse(),
|
|
2638
|
+
mergeability: () => ({
|
|
2639
|
+
data: { repository: { pullRequest: null } },
|
|
2640
|
+
errors: [
|
|
2443
2641
|
{
|
|
2444
|
-
|
|
2445
|
-
|
|
2642
|
+
type: 'NOT_FOUND',
|
|
2643
|
+
path: ['repository', 'pullRequest'],
|
|
2644
|
+
message:
|
|
2645
|
+
'Could not resolve to a PullRequest with the number of 11148.',
|
|
2446
2646
|
},
|
|
2447
|
-
|
|
2448
|
-
)
|
|
2647
|
+
],
|
|
2648
|
+
}),
|
|
2649
|
+
slimPullRequest: slimForPrNumber,
|
|
2650
|
+
});
|
|
2449
2651
|
|
|
2450
2652
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2451
2653
|
const result = await repository.findRelatedOpenPRs(
|
|
@@ -2468,17 +2670,12 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2468
2670
|
const consoleWarnSpy = jest
|
|
2469
2671
|
.spyOn(console, 'warn')
|
|
2470
2672
|
.mockImplementation(() => undefined);
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
new Response(JSON.stringify(buildTwoPrTimelineResponse()), {
|
|
2475
|
-
status: 200,
|
|
2476
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2477
|
-
}),
|
|
2478
|
-
)
|
|
2479
|
-
.mockResolvedValueOnce(
|
|
2673
|
+
mockFetchRoutes({
|
|
2674
|
+
timeline: () => buildTwoPrTimelineResponse(),
|
|
2675
|
+
mergeability: () =>
|
|
2480
2676
|
new Response('Internal Server Error', { status: 500 }),
|
|
2481
|
-
|
|
2677
|
+
slimPullRequest: slimForPrNumber,
|
|
2678
|
+
});
|
|
2482
2679
|
|
|
2483
2680
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2484
2681
|
const result = await repository.findRelatedOpenPRs(
|
|
@@ -2498,6 +2695,143 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2498
2695
|
});
|
|
2499
2696
|
});
|
|
2500
2697
|
|
|
2698
|
+
describe('findRelatedOpenPRs two-stage split', () => {
|
|
2699
|
+
afterEach(() => {
|
|
2700
|
+
jest.restoreAllMocks();
|
|
2701
|
+
});
|
|
2702
|
+
|
|
2703
|
+
const buildSlimTimelineResponse = () => ({
|
|
2704
|
+
data: {
|
|
2705
|
+
repository: {
|
|
2706
|
+
issue: {
|
|
2707
|
+
timelineItems: {
|
|
2708
|
+
pageInfo: { endCursor: null, hasNextPage: false },
|
|
2709
|
+
nodes: [
|
|
2710
|
+
{
|
|
2711
|
+
__typename: 'CrossReferencedEvent',
|
|
2712
|
+
willCloseTarget: true,
|
|
2713
|
+
source: {
|
|
2714
|
+
__typename: 'PullRequest',
|
|
2715
|
+
url: 'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2716
|
+
number: 11148,
|
|
2717
|
+
state: 'OPEN',
|
|
2718
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
2719
|
+
isDraft: false,
|
|
2720
|
+
mergeable: 'MERGEABLE',
|
|
2721
|
+
headRefName: 'feature-branch',
|
|
2722
|
+
baseRefName: 'main',
|
|
2723
|
+
baseRef: { name: 'main' },
|
|
2724
|
+
},
|
|
2725
|
+
},
|
|
2726
|
+
],
|
|
2727
|
+
},
|
|
2728
|
+
},
|
|
2729
|
+
},
|
|
2730
|
+
},
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it('produces the same evaluation inputs from the two-stage flow and sends no nested rules connections in any GraphQL query', async () => {
|
|
2734
|
+
const fetchSpy = mockFetchRoutes({
|
|
2735
|
+
timeline: () => buildSlimTimelineResponse(),
|
|
2736
|
+
slimPullRequest: () =>
|
|
2737
|
+
buildSlimPullRequestResponse({
|
|
2738
|
+
url: 'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2739
|
+
reviewThreads: {
|
|
2740
|
+
pageInfo: { endCursor: null, hasNextPage: false },
|
|
2741
|
+
nodes: [{ isResolved: true }],
|
|
2742
|
+
},
|
|
2743
|
+
}),
|
|
2744
|
+
branchRules: () => [
|
|
2745
|
+
{
|
|
2746
|
+
type: 'required_status_checks',
|
|
2747
|
+
parameters: { required_status_checks: [{ context: 'ci' }] },
|
|
2748
|
+
},
|
|
2749
|
+
],
|
|
2750
|
+
checkRuns: () => ({
|
|
2751
|
+
total_count: 1,
|
|
2752
|
+
check_runs: [{ id: 1, name: 'ci', conclusion: 'success' }],
|
|
2753
|
+
}),
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2756
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2757
|
+
const result = await repository.findRelatedOpenPRs(
|
|
2758
|
+
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2759
|
+
);
|
|
2760
|
+
|
|
2761
|
+
expect(result).toHaveLength(1);
|
|
2762
|
+
expect(result[0]).toEqual({
|
|
2763
|
+
url: 'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2764
|
+
branchName: 'feature-branch',
|
|
2765
|
+
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
2766
|
+
isDraft: false,
|
|
2767
|
+
isConflicted: false,
|
|
2768
|
+
mergeable: 'MERGEABLE',
|
|
2769
|
+
isPassedAllCiJob: true,
|
|
2770
|
+
isCiStateSuccess: true,
|
|
2771
|
+
isResolvedAllReviewComments: true,
|
|
2772
|
+
isBranchOutOfDate: false,
|
|
2773
|
+
missingRequiredCheckNames: [],
|
|
2774
|
+
});
|
|
2775
|
+
expect(
|
|
2776
|
+
countCallsMatching(
|
|
2777
|
+
fetchSpy,
|
|
2778
|
+
(url, body) =>
|
|
2779
|
+
url === 'https://api.github.com/graphql' &&
|
|
2780
|
+
(body.includes('branchProtectionRules') ||
|
|
2781
|
+
body.includes('rulesets') ||
|
|
2782
|
+
body.includes('statusCheckRollup')),
|
|
2783
|
+
),
|
|
2784
|
+
).toBe(0);
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2787
|
+
it('excludes a PR that is no longer open at the second stage', async () => {
|
|
2788
|
+
const consoleInfoSpy = jest
|
|
2789
|
+
.spyOn(console, 'info')
|
|
2790
|
+
.mockImplementation(() => undefined);
|
|
2791
|
+
mockFetchRoutes({
|
|
2792
|
+
timeline: () => buildSlimTimelineResponse(),
|
|
2793
|
+
slimPullRequest: () =>
|
|
2794
|
+
buildSlimPullRequestResponse({ state: 'MERGED' }),
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2798
|
+
const result = await repository.findRelatedOpenPRs(
|
|
2799
|
+
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2800
|
+
);
|
|
2801
|
+
|
|
2802
|
+
expect(result).toHaveLength(0);
|
|
2803
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
2804
|
+
expect.stringContaining(
|
|
2805
|
+
'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2806
|
+
),
|
|
2807
|
+
);
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
it('skips a PR whose second-stage status fetch fails and logs one warning', async () => {
|
|
2811
|
+
const consoleWarnSpy = jest
|
|
2812
|
+
.spyOn(console, 'warn')
|
|
2813
|
+
.mockImplementation(() => undefined);
|
|
2814
|
+
mockFetchRoutes({
|
|
2815
|
+
timeline: () => buildSlimTimelineResponse(),
|
|
2816
|
+
slimPullRequest: () =>
|
|
2817
|
+
new Response('Internal Server Error', { status: 500 }),
|
|
2818
|
+
});
|
|
2819
|
+
|
|
2820
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2821
|
+
const result = await repository.findRelatedOpenPRs(
|
|
2822
|
+
'https://github.com/HiromiShikata/test-repository/issues/11194',
|
|
2823
|
+
);
|
|
2824
|
+
|
|
2825
|
+
expect(result).toHaveLength(0);
|
|
2826
|
+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
|
2827
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
2828
|
+
expect.stringContaining(
|
|
2829
|
+
'https://github.com/HiromiShikata/test-repository/pull/11148',
|
|
2830
|
+
),
|
|
2831
|
+
);
|
|
2832
|
+
});
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2501
2835
|
const createApiV3CheerioRestIssueRepository = () => {
|
|
2502
2836
|
const apiV3IssueRepository = mock<ApiV3IssueRepository>();
|
|
2503
2837
|
const restIssueRepository = mock<RestIssueRepository>();
|
|
@@ -2519,6 +2853,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2519
2853
|
'dummy',
|
|
2520
2854
|
sleep,
|
|
2521
2855
|
);
|
|
2856
|
+
dateRepository.now.mockResolvedValue(new Date('2026-01-01T00:00:00.000Z'));
|
|
2522
2857
|
restIssueRepository.getIssue.mockResolvedValue({
|
|
2523
2858
|
labels: [],
|
|
2524
2859
|
assignees: [],
|