Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions __tests__/bundle/blunderbussDelayBareListing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { FakeGithub } from './fakeGithub'
import { Buffer } from 'node:buffer'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'

import labelFileContents from '../fixtures/labels/labelFileContentsResp.json'
import pullReqListPulls from '../fixtures/pullReq/pullReqListPulls.json'
import { blobSha } from '../utils/ownersData'
import { start } from './fakeGithub'
import { helpersFor, repo, token } from './helpers'
import { runBundle } from './runBundle'

vi.setConfig({ testTimeout: 30_000 })

// The delayed-reviewers loop of the sweep (src/cronJobs/sweep.ts,
// `requestReviewersIfDue`) reads `requested_reviewers ?? []` and
// `requested_teams ?? []` off the pull list entry: a due pull request listed
// without either key counts as having no requested reviewers and gets them.
describe('dist/index.js delayed blunderbuss on a pull request listed without reviewer keys', () => {
let gh: FakeGithub
const { routeOwners } = helpersFor(() => gh)
const owners = { OWNERS: 'reviewers:\n- bob\n' }

beforeAll(async () => {
gh = await start()
})
afterEach(() => gh.reset())
afterAll(() => gh.close())

function ago(minutes: number) {
return new Date(Date.now() - minutes * 60_000).toISOString()
}

it('requests reviewers on a due pull request whose listing omits requested_reviewers and requested_teams', async () => {
const file = structuredClone(labelFileContents)
file.content = Buffer.from('blunderbuss:\n delay: 1h\n').toString('base64')
gh.route('GET', '/repos/Codertocat/.project/contents/prow.yaml', { status: 200, body: file })
const { requested_reviewers: _reviewers, requested_teams: _teams, ...bare } = {
...structuredClone(pullReqListPulls[0]),
number: 1,
created_at: ago(70),
updated_at: ago(70),
labels: [],
assignees: [],
draft: false,
user: { login: 'Codertocat' },
base: { ref: 'master', sha: 'basesha' },
head: { sha: 'sha1', repo: { full_name: 'Codertocat/Hello-World' } },
}
expect(bare).not.toHaveProperty('requested_reviewers')
expect(bare).not.toHaveProperty('requested_teams')
gh.route('GET', new RegExp(`^${repo}/pulls\\?`), { status: 200, body: [bare] })
gh.route('GET', `${repo}/git/trees/master`, {
status: 200,
body: { sha: 'master', truncated: false, tree: [{ path: 'OWNERS', type: 'blob', sha: blobSha('OWNERS') }] },
})
routeOwners(owners, ['x.go'], bare)
gh.route('GET', `${repo}/issues/1/timeline`, { status: 200, body: [] })
gh.route('GET', `${repo}/pulls/1/reviews`, { status: 200, body: [] })
gh.route('POST', `${repo}/pulls/1/requested_reviewers`, { status: 201, body: {} })

const result = await runBundle({ eventName: 'schedule', payload: {}, inputs: { ...token, jobs: 'sweep' }, apiUrl: gh.url })

expect(result.status, result.stdout).toBe(0)
expect(result.errors).toEqual([])
expect(result.stdout).toContain('sweep: 0 candidates updated since')
expect(result.stdout).toContain('sweep: 1 more checked for delayed reviewers')
expect(result.stdout).not.toContain('already has requested reviewers')
expect(gh.requestsMatching('GET', /\/timeline\?per_page=100$/)).toHaveLength(1)
expect(gh.requestsMatching('GET', /\/reviews\?per_page=1$/)).toHaveLength(1)
expect(gh.requestsMatching('POST', /\/requested_reviewers$/).map(r => r.body)).toEqual([{ reviewers: ['bob'] }])
expect(gh.requests.filter(r => r.method !== 'GET').map(r => r.path)).toEqual([`${repo}/pulls/1/requested_reviewers`])
})
})
89 changes: 89 additions & 0 deletions __tests__/cronJobTest/sweepDelayBareListing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Buffer } from 'node:buffer'
import { http } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

import { sweep } from '../../src/cronJobs/sweep'
import { resetOwnersCaches } from '../../src/utils/owners'
import labelFileContents from '../fixtures/labels/labelFileContentsResp.json'
import pullReqListPulls from '../fixtures/pullReq/pullReqListPulls.json'
import pullReqOpenedEvent from '../fixtures/pullReq/pullReqOpenedEvent.json'
import * as utils from '../testUtils'
import { blobSha } from '../utils/ownersFixtures'

const server = setupServer()
beforeAll(() => server.listen(utils.failOnUnhandledRequest))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

const repo = `${utils.api}/repos/Codertocat/Hello-World`
const now = new Date('2026-09-16T12:00:00Z')

function ago(minutes: number): string {
return new Date(now.getTime() - minutes * 60_000).toISOString()
}

function prowYaml(text: string) {
const file = structuredClone(labelFileContents)
file.content = Buffer.from(text).toString('base64')
return http.get(utils.contentsUrl('.github/prow.yaml'), utils.mockResponse(200, file))
}

// Covers the `requested_reviewers ?? []` and `requested_teams ?? []` fallbacks
// of `requestReviewersIfDue` (src/cronJobs/sweep.ts): a delayed-only pull
// request whose list entry carries neither key is treated as having no
// requested reviewers, so a due one still gets blunderbuss' reviewers.
describe('sweep with blunderbuss.delay on a pull request listed without reviewer keys', () => {
const owners = { OWNERS: 'approvers:\n- alice\nreviewers:\n- carol\n' }
let context: utils.MockContext

beforeEach(() => {
utils.setupJobsEnv('sweep')
vi.useFakeTimers({ now, toFake: ['Date'] })
resetOwnersCaches()
context = new utils.MockContext(pullReqOpenedEvent)
})

afterEach(() => {
vi.useRealTimers()
})

it('requests reviewers on a due pull request whose listing omits requested_reviewers and requested_teams', async () => {
const tree = Object.keys(owners).map(path => ({ path, type: 'blob', sha: blobSha(path) }))
const timeline = new utils.ObserveRequest()
const reviews = new utils.ObserveRequest()
const requested = new utils.ObserveRequest()
const { requested_reviewers: _reviewers, requested_teams: _teams, ...bare } = {
...structuredClone(pullReqListPulls[0]),
number: 1,
draft: false,
labels: [],
created_at: ago(70),
updated_at: ago(70),
assignees: [],
user: { login: 'some-author' },
head: { sha: 'sha1' },
base: { ref: 'master', sha: 'basesha' },
}
expect(bare).not.toHaveProperty('requested_reviewers')
expect(bare).not.toHaveProperty('requested_teams')
server.use(
prowYaml('blunderbuss:\n delay: 1h\n'),
...utils.noOrgOrRepoConfigExcept(),
utils.defaultBranchTree(Object.keys(owners)),
http.get(`${repo}/git/trees/basesha`, utils.mockResponse(200, { sha: 'basesha', truncated: false, tree })),
...Object.entries(owners).map(([path, contents]) => http.get(`${repo}/git/blobs/${blobSha(path)}`, utils.mockResponse(200, { encoding: 'base64', content: Buffer.from(contents).toString('base64') }))),
http.get(`${repo}/pulls`, utils.mockResponse(200, [bare])),
http.get(`${repo}/pulls/1`, utils.mockResponse(200, bare)),
http.get(`${repo}/pulls/1/files`, utils.mockResponse(200, [{ filename: 'x.go', status: 'modified' }])),
http.get(`${repo}/issues/1/timeline`, utils.mockResponse(200, [], timeline)),
http.get(`${repo}/pulls/1/reviews`, utils.mockResponse(200, [], reviews)),
http.post(`${repo}/pulls/1/requested_reviewers`, utils.mockResponse(201, {}, requested)),
)

await expect(sweep(context)).resolves.toEqual({ candidates: [], merged: [], enqueued: [], failures: [] })
await expect(timeline.called()).resolves.toBe('called')
await expect(reviews.called()).resolves.toBe('called')
await expect(requested.called()).resolves.toBe('called')
})
})
Loading