From 9952fba0a77de7e86b622a2a3d28a0d7eca69efa Mon Sep 17 00:00:00 2001 From: quality Date: Tue, 6 Oct 2026 00:09:51 -0400 Subject: [PATCH] test(bundle): drive the prefixed label commands' and /remove's failure arms through dist/index.js Adds __tests__/bundle/prefixedRemoveArms.test.ts, running the committed bundle against the fake GitHub for the arms of src/labels/prefixed.ts and src/labels/remove.ts that no bundle suite reached: the missing-section and missing-args refusals of /kind, the no-op and failed-labels-read arms of /remove-kind, and the failed-labels-read and missing-args arms of /remove. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- __tests__/bundle/prefixedRemoveArms.test.ts | 101 ++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 __tests__/bundle/prefixedRemoveArms.test.ts diff --git a/__tests__/bundle/prefixedRemoveArms.test.ts b/__tests__/bundle/prefixedRemoveArms.test.ts new file mode 100644 index 0000000..a386291 --- /dev/null +++ b/__tests__/bundle/prefixedRemoveArms.test.ts @@ -0,0 +1,101 @@ +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, token } from './helpers' +import { runBundle } from './runBundle' + +vi.setConfig({ testTimeout: 30_000 }) + +function yamlFile(text: string) { + const file = structuredClone(labelFileContents) + file.content = Buffer.from(text).toString('base64') + return file +} + +// the arms of the prefixed label commands (src/labels/prefixed.ts) and of plain /remove (src/labels/remove.ts) +// that bundle.test.ts, labelCommands and the dispatcher suites leave out, driven through dist/index.js +describe('dist/index.js prefixed label and /remove arms', () => { + let gh: FakeGithub + const { expectCommandThenConfig, expectRequests } = helpersFor(() => gh) + + beforeAll(async () => { + gh = await start() + }) + afterEach(() => gh.reset()) + afterAll(() => gh.close()) + + function run(body: string, commands: string) { + return runBundle({ eventName: 'issue_comment', payload: comment(body), inputs: { ...token, 'prow-commands': commands }, apiUrl: gh.url }) + } + + it('/kind fails naming the missing section when the configuration has no kind key and the command has no built-in defaults', async () => { + gh.route('GET', `${repo}/contents/.prowlabels.yaml`, { status: 200, body: yamlFile('area:\n - bug\n') }) + + const result = await run('/kind cleanup', '/kind') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes(`could not get labels from yaml: Error: kind: yaml malformed, expected 'kind' top level key`))).toBe(true) + expectRequests(configReads({ repo: '.prowlabels.yaml' }), []) + }) + + it('/kind with only values outside the allowlist fails as missing args without reading or writing labels', async () => { + gh.route('GET', `${repo}/contents/.prowlabels.yaml`, { status: 200, body: labelFileContents }) + + const result = await run('/kind not-allowed', '/kind') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes('kind: command args missing from body'))).toBe(true) + expectRequests(configReads({ repo: '.prowlabels.yaml' }), []) + }) + + it('/remove-kind with none of its labels on the issue reads the labels and removes nothing', async () => { + gh.route('GET', `${repo}/contents/.prowlabels.yaml`, { status: 200, body: labelFileContents }) + gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [{ name: 'area/bug' }] } }) + + const result = await run('/remove-kind cleanup', '/kind') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + expectRequests(configReads({ repo: '.prowlabels.yaml' }), [`GET ${repo}/issues/1`]) + }) + + it('/remove-kind fails naming the labels read when the issue cannot be read', async () => { + gh.route('GET', `${repo}/contents/.prowlabels.yaml`, { status: 200, body: labelFileContents }) + gh.route('GET', `${repo}/issues/1`, { status: 500, body: { message: 'Internal Server Error' } }) + + const result = await run('/remove-kind cleanup', '/kind') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes('could not get labels from issue: Error: could not get issue'))).toBe(true) + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + expectRequests(configReads({ repo: '.prowlabels.yaml' }), [`GET ${repo}/issues/1`]) + }) + + it('/remove by a collaborator fails naming the labels read when the issue cannot be read', async () => { + gh.route('GET', `${repo}/collaborators/Codertocat`, { status: 204 }) + gh.route('GET', `${repo}/issues/1`, { status: 500, body: { message: 'Internal Server Error' } }) + + const result = await run('/remove foo', '/remove') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes('could not get labels from issue: Error: could not get issue'))).toBe(true) + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + expectCommandThenConfig([`GET ${repo}/collaborators/Codertocat`, `GET ${repo}/issues/1`]) + }) + + it('/remove by a collaborator naming only labels the issue does not carry fails as missing args', async () => { + gh.route('GET', `${repo}/collaborators/Codertocat`, { status: 204 }) + gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [{ name: 'bar' }] } }) + + const result = await run('/remove foo', '/remove') + + expect(result.status, result.stdout).toBe(1) + expect(result.errors.some(e => e.includes('remove: command args missing from body'))).toBe(true) + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + expectCommandThenConfig([`GET ${repo}/collaborators/Codertocat`, `GET ${repo}/issues/1`]) + }) +})