From d2c729bb7cd8188cddbac3889d14d2990367712c Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 12:36:23 -0400 Subject: [PATCH] test(bundle): drive the lgtm job's no-lgtm-label arm and label-sync's verbatim labels.labels allowlist through dist/index.js onPrLgtm.ts:33 (a push to a pull request not carrying lgtm reads the labels and removes nothing) and labelCatalog.ts:80 (the /label allowlist is created with bare names, never labels/) had no end-to-end hits; only unit tests reached them. Closes #391 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- __tests__/bundle/labelSyncAllowlist.test.ts | 69 +++++++++++++++++++++ __tests__/bundle/lgtmJobNoLabel.test.ts | 66 ++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 __tests__/bundle/labelSyncAllowlist.test.ts create mode 100644 __tests__/bundle/lgtmJobNoLabel.test.ts diff --git a/__tests__/bundle/labelSyncAllowlist.test.ts b/__tests__/bundle/labelSyncAllowlist.test.ts new file mode 100644 index 00000000..93509e53 --- /dev/null +++ b/__tests__/bundle/labelSyncAllowlist.test.ts @@ -0,0 +1,69 @@ +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 { configReads, helpersFor, repo, token } from './helpers' +import { runBundle } from './runBundle' + +vi.setConfig({ testTimeout: 30_000 }) + +const labelsRead = `GET ${repo}/labels?per_page=100` + +function yamlFile(text: string) { + const file = structuredClone(labelFileContents) + file.content = Buffer.from(text).toString('base64') + return file +} + +// the catalog's verbatim arm (labelCatalog.ts `prefixed('', value)`), driven through dist/index.js: the `/label` +// allowlist is the `labels.labels` section and its values are created as-is, never as `labels/`. +// labelSyncCatalog.test.ts only composes prefixed sections (kind, a custom key) and needs-* labels +describe('dist/index.js label-sync creates the /label allowlist verbatim', () => { + let gh: FakeGithub + const { expectRequests } = helpersFor(() => gh) + + beforeAll(async () => { + gh = await start() + }) + afterEach(() => gh.reset()) + afterAll(() => gh.close()) + + function runLabelSync() { + return runBundle({ eventName: 'workflow_dispatch', payload: {}, inputs: { ...token, jobs: 'label-sync' }, apiUrl: gh.url }) + } + + it('a labels.labels section yields the bare names, with color and description carried over', async () => { + const config = yamlFile([ + 'labels:', + ' labels:', + ' - documentation', + ' - name: tide/merge-method-squash', + ' color: FEF2C0', + ' description: Squash when merging', + '', + ].join('\n')) + gh.route('GET', '/repos/Codertocat/.project/contents/prow.yaml', { status: 200, body: config }) + gh.route('GET', `${repo}/labels`, { status: 200, body: [] }) + gh.route('POST', `${repo}/labels`, { status: 201, body: {} }) + + const result = await runLabelSync() + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + const posts = gh.requestsMatching('POST', /\/labels$/) + const created = posts.map(p => (p.body as { name: string }).name) + expect(created).toContain('documentation') + expect(created).toContain('tide/merge-method-squash') + expect(created.filter(name => name.startsWith('labels/'))).toEqual([]) + expect(posts.find(p => (p.body as { name: string }).name === 'documentation')!.body).toEqual({ name: 'documentation' }) + expect(posts.find(p => (p.body as { name: string }).name === 'tide/merge-method-squash')!.body).toEqual({ + name: 'tide/merge-method-squash', + color: 'fef2c0', + description: 'Squash when merging', + }) + expect(result.stdout).toContain(`label-sync: created ${created.length} [${created.join(', ')}]`) + expectRequests([...configReads({ org: '.project' }), labelsRead], created.map(() => `POST ${repo}/labels`)) + }) +}) diff --git a/__tests__/bundle/lgtmJobNoLabel.test.ts b/__tests__/bundle/lgtmJobNoLabel.test.ts new file mode 100644 index 00000000..5300d251 --- /dev/null +++ b/__tests__/bundle/lgtmJobNoLabel.test.ts @@ -0,0 +1,66 @@ +import type { FakeGithub } from './fakeGithub' +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +import pullReqOpenedEvent from '../fixtures/pullReq/pullReqOpenedEvent.json' +import { start } from './fakeGithub' +import { helpersFor, ownersProbe, ownersReads, repo, token } from './helpers' +import { runBundle } from './runBundle' + +vi.setConfig({ testTimeout: 30_000 }) + +// the lgtm job's no-op arm (onPrLgtm.ts: `currentLabels.includes('lgtm')` false), driven through dist/index.js: +// a new push to a pull request that does not carry lgtm reads the labels and removes nothing. bundle.test.ts +// only drives the arm where the label is present and gets removed +describe('dist/index.js pull_request lgtm job on a pull request without the lgtm label', () => { + let gh: FakeGithub + const { expectRequests, routeOwners } = helpersFor(() => gh) + + beforeAll(async () => { + gh = await start() + }) + afterEach(() => gh.reset()) + afterAll(() => gh.close()) + + function synchronize(eventName: string) { + return runBundle({ + eventName, + payload: { ...pullReqOpenedEvent, action: 'synchronize' }, + inputs: { ...token, jobs: 'lgtm' }, + apiUrl: gh.url, + }) + } + + for (const eventName of ['pull_request', 'pull_request_target']) { + it(`${eventName}: reads the labels and issues no DELETE when lgtm is absent`, async () => { + routeOwners({}, ['src/file1.txt']) + gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [{ name: 'kind/bug' }, { name: 'approved' }] } }) + + const result = await synchronize(eventName) + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(result.stdout).toContain('remove-lgtm: found labels for issue kind/bug,approved') + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + // owners-label reads the (OWNERS-less) base tree, approve probes the default branch, then the lgtm job + // reads the labels and stops there + expectRequests([], [ + ...ownersReads, + ownersProbe, + `GET ${repo}/issues/1`, + ]) + }) + } + + it('pull_request: an unlabeled pull request is read and left alone', async () => { + routeOwners({}, ['src/file1.txt']) + gh.route('GET', `${repo}/issues/1`, { status: 200, body: { labels: [] } }) + + const result = await synchronize('pull_request') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(result.stdout).toContain('remove-lgtm: found labels for issue ') + expect(gh.requestsMatching('DELETE', /./)).toEqual([]) + expectRequests([], [...ownersReads, ownersProbe, `GET ${repo}/issues/1`]) + }) +})