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
68 changes: 68 additions & 0 deletions __tests__/bundle/blunderbussDelayWithoutOwners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 { start } from './fakeGithub'
import { repo, token } from './helpers'
import { runBundle } from './runBundle'

vi.setConfig({ testTimeout: 30_000 })

// The delayed-reviewers loop of the sweep (src/cronJobs/sweep.ts) probes the
// base branch for OWNERS files before `requestReviewersIfDue`; without any, a
// due pull request is left alone and none of blunderbuss' reads happen.
describe('dist/index.js delayed blunderbuss on a base branch without OWNERS files', () => {
let gh: FakeGithub

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('probes the base branch once and requests no reviewers for a due pull request', 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 })
gh.route('GET', new RegExp(`^${repo}/pulls\\?`), {
status: 200,
body: [{
...structuredClone(pullReqListPulls[0]),
number: 1,
created_at: ago(70),
updated_at: ago(70),
labels: [],
requested_reviewers: [],
requested_teams: [],
assignees: [],
draft: false,
user: { login: 'Codertocat' },
base: { ref: 'master', sha: 'basesha' },
head: { sha: 'sha1', repo: { full_name: 'Codertocat/Hello-World' } },
}],
})
gh.route('GET', `${repo}/git/trees/master`, { status: 200, body: { sha: 'master', truncated: false, tree: [] } })

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(gh.requestsMatching('GET', /\/git\/trees\/master\?recursive=true$/)).toHaveLength(1)
expect(gh.requestsMatching('GET', /\/timeline\?/)).toEqual([])
expect(gh.requestsMatching('GET', /\/reviews\?/)).toEqual([])
expect(gh.requests.filter(r => r.method !== 'GET')).toEqual([])
// nothing but the configuration probes, the pull list and the one tree read
expect(gh.requests.map(r => r.path).filter(path => !/\/contents\//.test(path))).toEqual([
`${repo}/pulls?state=open&sort=updated&direction=desc&per_page=100&page=1`,
`${repo}/git/trees/master?recursive=true`,
])
})
})
84 changes: 84 additions & 0 deletions __tests__/cronJobTest/sweepDelayWithoutOwners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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'

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 delayed-reviewers loop of `sweep` (src/cronJobs/sweep.ts, the
// `branchHasOwners` guard ahead of `requestReviewersIfDue`) on a base branch
// without OWNERS files: the only pull requests in scope are the delayed ones,
// so the skip is observable as the absence of every blunderbuss read and write.
describe('sweep with blunderbuss.delay on a base branch without OWNERS files', () => {
let context: utils.MockContext

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

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

it('probes the base branch once and requests no reviewers for a due pull request', async () => {
const tree = new utils.ObserveRequest()
const timeline = new utils.ObserveRequest()
const reviews = new utils.ObserveRequest()
const requested = new utils.ObserveRequest()
const due = {
...structuredClone(pullReqListPulls[0]),
number: 1,
draft: false,
labels: [],
created_at: ago(70),
updated_at: ago(70),
requested_reviewers: [],
requested_teams: [],
assignees: [],
user: { login: 'some-author' },
head: { sha: 'sha1' },
base: { ref: 'master', sha: 'basesha' },
}
server.use(
prowYaml('blunderbuss:\n delay: 1h\n'),
...utils.noOrgOrRepoConfigExcept(),
utils.defaultBranchTree([], tree),
http.get(`${repo}/pulls`, utils.mockResponse(200, [due])),
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(tree.called()).resolves.toBe('called')
await expect(timeline.notCalled()).resolves.toBe('not called')
await expect(reviews.notCalled()).resolves.toBe('not called')
await expect(requested.notCalled()).resolves.toBe('not called')
})
})
Loading