diff --git a/__tests__/plugins/tide.test.ts b/__tests__/plugins/tide.test.ts index 316c5ef4..8d640399 100644 --- a/__tests__/plugins/tide.test.ts +++ b/__tests__/plugins/tide.test.ts @@ -146,6 +146,15 @@ describe('fetchMergeability', () => { await expect(getPull(octokit, context, 1)).resolves.toMatchObject({ draft: false }) }) + + it('reads a head whose repository is gone (head.repo null) as not a fork', async () => { + servePull(pull([], { + head: { sha: 'headsha', repo: null }, + base: { ref: 'master', sha: 'basesha', repo: { full_name: 'Codertocat/Hello-World' } }, + })) + + await expect(getPull(octokit, context, 1)).resolves.toMatchObject({ fork: false }) + }) }) describe('mergeOnce', () => { @@ -214,6 +223,19 @@ describe('tryMergePullRequest', () => { expect(sleep).not.toHaveBeenCalled() }) + // the draft flag is read before mergeable_state: a draft is refused without waiting for GitHub to compute the state + it('does not wait for an unknown state on a draft pr', async () => { + const gets = servePull(pull(['lgtm'], { ...unknown, draft: true })) + const merge = observeMerge() + const info = vi.spyOn(core, 'info') + + await expect(tryMergePullRequest(octokit, context, 1, tide)).resolves.toBe('skipped') + await expect(merge.notCalled()).resolves.toBe('not called') + expect(gets).toHaveLength(1) + expect(sleep).not.toHaveBeenCalled() + expect(info).toHaveBeenCalledWith('skipping pr #1: not mergeable (draft)') + }) + it('waits for an unknown state when the gate passes, then merges', async () => { const gets = servePull(pull(['lgtm'], unknown), pull(['lgtm'])) const merge = observeMerge() @@ -466,6 +488,31 @@ describe('tryMergePullRequest', () => { expect(body).toContain('workflow files (`.github/workflows/a.yml`, `.github/workflows/b.yml`, `.github/workflows/c.yml`, `.github/workflows/d.yml`, `.github/workflows/e.yml` and 2 more) that it changes.') }) + it('exactly five workflow files are all listed, with no "and 0 more"', async () => { + servePull(forkPull()) + observeMerge(403, forbidden) + const own = ['a', 'b', 'c', 'd', 'e'].map(name => `.github/workflows/${name}.yml`) + const diff = serveDiff([], own) + vi.spyOn(core, 'warning').mockImplementation(() => {}) + + await expect(tryMergePullRequest(octokit, context, 1, tide)).resolves.toBe('skipped') + const body = (await diff.comment.body()).body as string + expect(body).toContain('workflow files (`.github/workflows/a.yml`, `.github/workflows/b.yml`, `.github/workflows/c.yml`, `.github/workflows/d.yml`, `.github/workflows/e.yml`) that it changes.') + expect(body).not.toContain('more') + }) + + it('only paths under the top-level .github/workflows/ count: a nested copy is not a workflow file', async () => { + servePull(forkPull()) + observeMerge(403, forbidden) + const diff = serveDiff(['docs/.github/workflows/example.yml'], ['templates/.github/workflows/prow.yml']) + 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 same-repository pull request keeps the raw failure and never compares', async () => { servePull(pull(['lgtm'], { head: { sha: 'headsha', repo: { full_name: 'Codertocat/Hello-World' } }, base: { ref: 'master', sha: 'basesha', repo: { full_name: 'Codertocat/Hello-World' } } })) observeMerge(403, forbidden) diff --git a/__tests__/plugins/tideMergeQueue.test.ts b/__tests__/plugins/tideMergeQueue.test.ts index 1e6b5d89..b1d1870a 100644 --- a/__tests__/plugins/tideMergeQueue.test.ts +++ b/__tests__/plugins/tideMergeQueue.test.ts @@ -146,6 +146,19 @@ describe('evaluateMerge on a branch that requires a merge queue', () => { expect(mutations(calls, 'enqueuePullRequest')).toHaveLength(0) }) + // without a draft field the gate cannot see the draft; mergeable_state: draft is still ours to refuse, not the queue's + it('a pr without a draft field whose mergeable_state is draft is refused, not enqueued', async () => { + const { draft, ...noDraft } = pull(['lgtm'], { mergeable_state: 'draft' }) + expect(draft).toBe(false) + servePull(noDraft) + const merge = observeMerge() + const calls = serveGraphql({ enabled: true }) + + await expect(evaluateMerge(octokit, context, 1, tide)).resolves.toEqual({ result: 'skipped', reason: 'not mergeable (draft)' }) + await expect(merge.notCalled()).resolves.toBe('not called') + expect(mutations(calls, 'enqueuePullRequest')).toHaveLength(0) + }) + it('an unknown state is retried, then enqueued', async () => { const gets = servePull(pull(['lgtm'], { mergeable: null, mergeable_state: 'unknown' }), pull(['lgtm'])) const calls = serveGraphql() @@ -282,7 +295,10 @@ describe('evaluateMerge on a branch that requires a merge queue', () => { servePull(pull(['lgtm'])) const merge = observeMerge(405, { message: 'Changes must be made through the merge queue.' }) vi.spyOn(core, 'error').mockImplementation(() => {}) - // no /graphql handler: an unhandled request would fail the test + // queueState swallows a failed read (one warning, then a direct merge), so an unhandled + // request would not fail the test: count the calls instead + const calls = serveGraphql({ enabled: true }) + const warning = vi.spyOn(core, 'warning') await expect(evaluateMerge(octokit, context, 1, resolveTide({ merge_queue: 'off' }))).resolves.toEqual({ result: 'failed', @@ -290,6 +306,8 @@ describe('evaluateMerge on a branch that requires a merge queue', () => { status: 405, }) await expect(merge.called()).resolves.toBe('called') + expect(calls).toHaveLength(0) + expect(warning).not.toHaveBeenCalled() }) it('debugs once that the queue\'s merge method wins over tide.merge_method', async () => { @@ -374,8 +392,10 @@ describe('dequeue when the gate breaks', () => { it('merge_queue: off: no GraphQL call when the gate fails', async () => { servePull(pull([])) + const calls = serveGraphql({ enabled: true }) await expect(evaluateMerge(octokit, context, 1, resolveTide({ merge_queue: 'off' }))).resolves.toEqual({ result: 'skipped', reason: 'missing lgtm' }) + expect(calls).toHaveLength(0) }) it('a stale lgtm on an in-queue bot-enqueued pr is stripped and dequeued', async () => {