diff --git a/__tests__/issueCommentTest/meow.test.ts b/__tests__/issueCommentTest/meow.test.ts index eec62e7f..ef758e40 100644 --- a/__tests__/issueCommentTest/meow.test.ts +++ b/__tests__/issueCommentTest/meow.test.ts @@ -146,6 +146,22 @@ describe('/meow', () => { ) }) + it('names a status-less response as a redirect', async () => { + // Response.error() is the only status 0 fetch can resolve with + vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.error()) + const warning = vi.spyOn(core, 'warning').mockImplementation(() => {}) + + await handleIssueComment(contextFor('/meow')) + + expect(warning).toHaveBeenCalledWith(expect.stringContaining('cat api responded with a redirect')) + expect(createComment).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + 1, + 'The cat API is unavailable right now.', + ) + }) + it('degrades to a note on a network error', async () => { server.use(http.get(catApi, () => HttpResponse.error())) vi.spyOn(core, 'warning').mockImplementation(() => {}) diff --git a/__tests__/issueCommentTest/trigger.test.ts b/__tests__/issueCommentTest/trigger.test.ts index f09f771b..938c8ee7 100644 --- a/__tests__/issueCommentTest/trigger.test.ts +++ b/__tests__/issueCommentTest/trigger.test.ts @@ -6,7 +6,8 @@ import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { handleIssueComment } from '../../src/issueComment/handleIssueComment' -import { okToTestOnPullRequest, retest } from '../../src/issueComment/trigger' +import { approvePendingRuns, okToTestOnPullRequest, retest } from '../../src/issueComment/trigger' +import { newOctokit } from '../../src/utils/octokit' import issueCommentEvent from '../fixtures/issues/issueCommentEvent.json' import prOpenedEvent from '../fixtures/pullReq/pullReqOpenedEvent.json' import * as utils from '../testUtils' @@ -807,6 +808,16 @@ describe('/ok-to-test', () => { expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('could not approve run 8 (CI)')) }) + it('a non-Error approve rejection carries no status and fails with its string form', async () => { + server.use(serveRuns(pendingRuns)) + const octokit = newOctokit('some-token') + vi.spyOn(octokit.actions, 'approveWorkflowRun').mockRejectedValue('nope') + + await expect(approvePendingRuns(octokit, new utils.MockContext(prCommentEvent('/ok-to-test')), 1, headSha)) + .rejects + .toThrow('could not approve run 8 (CI): nope') + }) + it('on an issue comments that it only applies to pull requests', async () => { const reply = new utils.ObserveRequest() const event = structuredClone(issueCommentEvent) diff --git a/__tests__/plugins/tide.test.ts b/__tests__/plugins/tide.test.ts index fc31db6e..402e45eb 100644 --- a/__tests__/plugins/tide.test.ts +++ b/__tests__/plugins/tide.test.ts @@ -4,9 +4,10 @@ import { http } from 'msw' import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { fetchMergeability, tideOnCheckSuite, tideOnComment, tideOnPullRequest, tideOnReview, tryMergePullRequest, unknownRetryDelaysMs } from '../../src/plugins/tide' +import { fetchMergeability, loadTide, mergeOnce, tideOnCheckSuite, tideOnComment, tideOnPullRequest, tideOnReview, tryMergePullRequest, unknownRetryDelaysMs } from '../../src/plugins/tide' import { resolveTide } from '../../src/utils/config' import { newOctokit } from '../../src/utils/octokit' +import { resetOwnersCaches } from '../../src/utils/owners' import * as sleepModule from '../../src/utils/sleep' import labelFileContents from '../fixtures/labels/labelFileContentsResp.json' import checkSuiteCompletedEvent from '../fixtures/pullReq/checkSuiteCompletedEvent.json' @@ -114,6 +115,16 @@ describe('fetchMergeability', () => { expect(info).toHaveBeenCalledWith('mergeability of pr #1 is still unknown after 3 retries') }) + it('a state computed on the read after the last wait is returned without the give-up line', async () => { + const gets = servePull(pull([], unknown), pull([], unknown), pull([], unknown), pull([], { mergeable_state: 'behind' })) + const info = vi.spyOn(core, 'info') + + await expect(fetchMergeability(octokit, context, 1)).resolves.toMatchObject({ state: 'behind', mergeable: true }) + expect(gets).toHaveLength(unknownRetryDelaysMs.length + 1) + expect(sleep.mock.calls.map(call => call[0])).toEqual(unknownRetryDelaysMs) + expect(info).not.toHaveBeenCalled() + }) + it('treats a null mergeable as unknown even when the state says otherwise', async () => { const gets = servePull(pull([], { mergeable: null, mergeable_state: 'clean' }), pull([])) @@ -128,6 +139,37 @@ describe('fetchMergeability', () => { expect(gets).toHaveLength(1) expect(sleep).not.toHaveBeenCalled() }) + + it('reads a pull request without a draft field as not draft', async () => { + const { draft, ...noDraft } = pull([]) + expect(draft).toBe(false) + servePull(noDraft) + + await expect(fetchMergeability(octokit, context, 1)).resolves.toMatchObject({ draft: false }) + }) +}) + +describe('mergeOnce', () => { + it('reports a non-Error rejection by its string form, without a status', async () => { + vi.spyOn(octokit.pulls, 'merge').mockRejectedValue('nope') + + await expect(mergeOnce(octokit, context, 1, tide, 'headsha')).resolves.toEqual({ result: 'failed', message: 'nope', status: undefined }) + }) +}) + +describe('loadTide', () => { + beforeEach(() => { + resetOwnersCaches() + server.use(...utils.noOrgOrRepoConfigExcept()) + }) + + it('without a base in scope, reads the OWNERS files of the default branch', async () => { + const tree = new utils.ObserveRequest() + server.use(utils.defaultBranchTree(['OWNERS'], tree)) + + await expect(loadTide(octokit, context)).resolves.toMatchObject({ labels: ['lgtm', 'approved'] }) + await expect(tree.called()).resolves.toBe('called') + }) }) describe('tryMergePullRequest', () => { @@ -437,6 +479,19 @@ describe('tryMergePullRequest', () => { expect(error).toHaveBeenCalledWith(expect.stringContaining('Resource not accessible by integration')) }) + it('a compare response without a files list is read as no workflow files behind', async () => { + servePull(forkPull()) + observeMerge(403, forbidden) + const diff = serveDiff([], ['src/a.go']) + server.use(http.get(`${repo}/compare/headsha...master`, utils.mockResponse(200, { status: 'identical' }, diff.compare))) + const error = vi.spyOn(core, 'error').mockImplementation(() => {}) + + await expect(tryMergePullRequest(octokit, context, 1, tide)).resolves.toBe('failed') + await expect(diff.compare.called()).resolves.toBe('called') + await expect(diff.comment.notCalled()).resolves.toBe('not called') + expect(error).toHaveBeenCalledWith(expect.stringContaining('Resource not accessible by integration')) + }) + it('a fork 403 with no workflow file anywhere is still a failure', async () => { servePull(forkPull()) observeMerge(403, forbidden)