From 37a39ec030837603147da5c02c8947bf938373f5 Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 14:40:09 -0400 Subject: [PATCH 1/3] test(bundle): drive parseProwConfig's empty-document arm (config.ts:373) through dist/index.js A blank or bare-'---' .github/prow.yaml is a found configuration source with no sections: /lifecycle stale falls back to the built-in defaults and /kind cleanup fails naming the missing key rather than reporting no configuration. Neither path had end-to-end coverage. A comment-only file still throws through the bundle (issue #393), so that row is left out until parseProwConfig handles zero-document input. Closes #394 Signed-off-by: quality --- __tests__/bundle/emptyConfigDocument.test.ts | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 __tests__/bundle/emptyConfigDocument.test.ts diff --git a/__tests__/bundle/emptyConfigDocument.test.ts b/__tests__/bundle/emptyConfigDocument.test.ts new file mode 100644 index 00000000..68ec2ea6 --- /dev/null +++ b/__tests__/bundle/emptyConfigDocument.test.ts @@ -0,0 +1,74 @@ +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 { start } from './fakeGithub' +import { comment, configReads, helpersFor, repo, repoLabels, token } from './helpers' +import { runBundle } from './runBundle' + +vi.setConfig({ testTimeout: 30_000 }) + +const issueRead = `GET ${repo}/issues/1` +const labelsRead = `GET ${repo}/labels?per_page=100` +const labelWrite = `POST ${repo}/issues/1/labels` + +function yamlFile(text: string) { + const file = structuredClone(labelFileContents) + file.content = Buffer.from(text).toString('base64') + return file +} + +// the empty-document arm of parseProwConfig (src/utils/config.ts: `loaded === undefined || loaded === null` -> +// `{}`), driven through dist/index.js: a .github/prow.yaml that is blank, comment-only or a bare document +// marker is a found configuration source with no sections, which is not the same as having no file at all +describe('dist/index.js with an empty .github/prow.yaml', () => { + let gh: FakeGithub + const { expectRequests } = helpersFor(() => gh) + + beforeAll(async () => { + gh = await start() + }) + afterEach(() => gh.reset()) + afterAll(() => gh.close()) + + function run(yaml: string, body: string, command: string) { + gh.route('GET', `${repo}/contents/.github%2Fprow.yaml`, { status: 200, body: yamlFile(yaml) }) + gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [] } }) + gh.route('GET', `${repo}/labels`, repoLabels('lifecycle/stale', 'kind/cleanup')) + gh.route('POST', `${repo}/issues/1/labels`, { status: 200, body: [] }) + return runBundle({ + eventName: 'issue_comment', + payload: comment(body), + inputs: { ...token, 'prow-commands': command }, + apiUrl: gh.url, + }) + } + + // a comment-only file belongs here too, but js-yaml 5 throws on zero-document input and the loader's + // `trim()` guard does not catch it (issue #393); add that row once parseProwConfig handles it + const emptyDocuments: [string, string][] = [ + ['a blank file', ''], + ['a bare document marker', '---\n'], + ] + + it.each(emptyDocuments)('%s stops the repository tier search and serves /lifecycle from the built-in defaults', async (_name, yaml) => { + const result = await run(yaml, '/lifecycle stale', '/lifecycle') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(result.stdout).toContain('lifecycle: using built-in labels frozen,stale,rotten') + expect(gh.requestsMatching('POST', /\/issues\/1\/labels$/).map(r => r.body)).toEqual([{ labels: ['lifecycle/stale'] }]) + // the empty file is a found source: the loader reads no further repository path and never reports "no prow configuration found" + expectRequests(configReads({ repo: '.github/prow.yaml' }), [issueRead, labelsRead, labelWrite]) + }) + + it.each(emptyDocuments)('%s is a found source without a kind section: /kind fails naming the missing key, not a missing configuration', async (_name, yaml) => { + const result = await run(yaml, '/kind cleanup', '/kind') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes(`kind: yaml malformed, expected 'kind' top level key`)), result.stdout).toBe(true) + expect(result.errors.some(e => e.includes('no prow configuration found'))).toBe(false) + expect(gh.requestsMatching('POST', /\/issues\/1\/labels$/)).toEqual([]) + }) +}) From 138529c141a90646075e702d56ea1df262e7f430 Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 23:01:05 -0400 Subject: [PATCH 2/3] test(bundle): keep only the blank-file /lifecycle case of the empty-document arm Signed-off-by: quality --- __tests__/bundle/emptyConfigDocument.test.ts | 26 +++++--------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/__tests__/bundle/emptyConfigDocument.test.ts b/__tests__/bundle/emptyConfigDocument.test.ts index 68ec2ea6..17ef84e1 100644 --- a/__tests__/bundle/emptyConfigDocument.test.ts +++ b/__tests__/bundle/emptyConfigDocument.test.ts @@ -20,8 +20,8 @@ function yamlFile(text: string) { } // the empty-document arm of parseProwConfig (src/utils/config.ts: `loaded === undefined || loaded === null` -> -// `{}`), driven through dist/index.js: a .github/prow.yaml that is blank, comment-only or a bare document -// marker is a found configuration source with no sections, which is not the same as having no file at all +// `{}`), driven through dist/index.js: a blank .github/prow.yaml is a found configuration source with no +// sections, which is not the same as having no file at all describe('dist/index.js with an empty .github/prow.yaml', () => { let gh: FakeGithub const { expectRequests } = helpersFor(() => gh) @@ -35,7 +35,7 @@ describe('dist/index.js with an empty .github/prow.yaml', () => { function run(yaml: string, body: string, command: string) { gh.route('GET', `${repo}/contents/.github%2Fprow.yaml`, { status: 200, body: yamlFile(yaml) }) gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [] } }) - gh.route('GET', `${repo}/labels`, repoLabels('lifecycle/stale', 'kind/cleanup')) + gh.route('GET', `${repo}/labels`, repoLabels('lifecycle/stale')) gh.route('POST', `${repo}/issues/1/labels`, { status: 200, body: [] }) return runBundle({ eventName: 'issue_comment', @@ -46,14 +46,9 @@ describe('dist/index.js with an empty .github/prow.yaml', () => { } // a comment-only file belongs here too, but js-yaml 5 throws on zero-document input and the loader's - // `trim()` guard does not catch it (issue #393); add that row once parseProwConfig handles it - const emptyDocuments: [string, string][] = [ - ['a blank file', ''], - ['a bare document marker', '---\n'], - ] - - it.each(emptyDocuments)('%s stops the repository tier search and serves /lifecycle from the built-in defaults', async (_name, yaml) => { - const result = await run(yaml, '/lifecycle stale', '/lifecycle') + // `trim()` guard does not catch it (issue #393); add that case once parseProwConfig handles it + it('a blank file stops the repository tier search and serves /lifecycle from the built-in defaults', async () => { + const result = await run('', '/lifecycle stale', '/lifecycle') expect(result.status, result.stdout).toBe(0) expect(result.errors).toEqual([]) @@ -62,13 +57,4 @@ describe('dist/index.js with an empty .github/prow.yaml', () => { // the empty file is a found source: the loader reads no further repository path and never reports "no prow configuration found" expectRequests(configReads({ repo: '.github/prow.yaml' }), [issueRead, labelsRead, labelWrite]) }) - - it.each(emptyDocuments)('%s is a found source without a kind section: /kind fails naming the missing key, not a missing configuration', async (_name, yaml) => { - const result = await run(yaml, '/kind cleanup', '/kind') - - expect(result.status, result.stdout).toBe(1) - expect(result.errors.some(e => e.includes(`kind: yaml malformed, expected 'kind' top level key`)), result.stdout).toBe(true) - expect(result.errors.some(e => e.includes('no prow configuration found'))).toBe(false) - expect(gh.requestsMatching('POST', /\/issues\/1\/labels$/)).toEqual([]) - }) }) From f9695ea7bbf1612de8a97129639f757555457763 Mon Sep 17 00:00:00 2001 From: Bob Killen Date: Fri, 9 Oct 2026 10:56:43 -0500 Subject: [PATCH 3/3] test(bundle): address review on empty config document test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bob Killen --- __tests__/bundle/emptyConfigDocument.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/__tests__/bundle/emptyConfigDocument.test.ts b/__tests__/bundle/emptyConfigDocument.test.ts index 17ef84e1..1c228256 100644 --- a/__tests__/bundle/emptyConfigDocument.test.ts +++ b/__tests__/bundle/emptyConfigDocument.test.ts @@ -21,7 +21,7 @@ function yamlFile(text: string) { // the empty-document arm of parseProwConfig (src/utils/config.ts: `loaded === undefined || loaded === null` -> // `{}`), driven through dist/index.js: a blank .github/prow.yaml is a found configuration source with no -// sections, which is not the same as having no file at all +// sections, which is not the same as having no file at all; a comment-only file is the same case (issue #393) describe('dist/index.js with an empty .github/prow.yaml', () => { let gh: FakeGithub const { expectRequests } = helpersFor(() => gh) @@ -45,10 +45,11 @@ describe('dist/index.js with an empty .github/prow.yaml', () => { }) } - // a comment-only file belongs here too, but js-yaml 5 throws on zero-document input and the loader's - // `trim()` guard does not catch it (issue #393); add that case once parseProwConfig handles it - it('a blank file stops the repository tier search and serves /lifecycle from the built-in defaults', async () => { - const result = await run('', '/lifecycle stale', '/lifecycle') + it.each([ + ['a blank file', ''], + ['a comment-only file', '# prow configuration\n# nothing enabled yet\n'], + ])('%s stops the repository tier search and serves /lifecycle from the built-in defaults', async (_name, yaml) => { + const result = await run(yaml, '/lifecycle stale', '/lifecycle') expect(result.status, result.stdout).toBe(0) expect(result.errors).toEqual([])