From 4a3553e1144337cf529c8e08eb21cac115983046 Mon Sep 17 00:00:00 2001 From: quality Date: Wed, 30 Sep 2026 23:19:39 -0400 Subject: [PATCH] test: assert docs/commands.md rows match the handleIssueComment dispatcher and the prow.yml prow-commands default Adds __tests__/commands.test.ts: every handler, alias and label command (with its /remove- form) has a row in docs/commands.md and vice versa; /override, /skip and /retest-required stay listed as unsupported with neither a row nor a handler; the reusable workflow's prow-commands default equals exactly the handlers minus /meow plus the label commands. Closes #253 Signed-off-by: quality --- __tests__/commands.test.ts | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 __tests__/commands.test.ts diff --git a/__tests__/commands.test.ts b/__tests__/commands.test.ts new file mode 100644 index 00000000..ac44198c --- /dev/null +++ b/__tests__/commands.test.ts @@ -0,0 +1,84 @@ +import { readFileSync } from 'node:fs' +import * as path from 'node:path' +import * as yaml from 'js-yaml' +import { describe, expect, it } from 'vitest' + +import { fixedLabelCommands } from '../src/labels/fixed' +import { prefixedLabelCommands, removeCommandFor } from '../src/labels/prefixed' + +const root = path.resolve(__dirname, '..') +const commandsDoc = 'docs/commands.md' +const dispatcher = 'src/issueComment/handleIssueComment.ts' +const reusableWorkflowPath = '.github/workflows/prow.yml' + +function read(file: string): string { + return readFileSync(path.join(root, file), 'utf8') +} + +const dispatcherSource = read(dispatcher) + +// the hand-written handlers: `'/assign': context => assign(context),` +const handlerCommands = [...dispatcherSource.matchAll(/^\s+'(\/[a-z-]+)': context =>/gm)].map(m => m[1]) + +// the explicit Prow-style aliases: `'/hold': ['/unhold', '/remove-hold'],` +const explicitAliases = [...dispatcherSource.matchAll(/^\s+'(\/[a-z-]+)': \[([^\]]+)\],/gm)] + .flatMap(m => [...m[2].matchAll(/'(\/[a-z-]+)'/g)].map(a => a[1])) + +const labelCommands = [...prefixedLabelCommands, ...fixedLabelCommands].map(cmd => cmd.command) +const labelRemoveCommands = labelCommands.map(removeCommandFor) + +const implemented = new Set([...handlerCommands, ...explicitAliases, ...labelCommands, ...labelRemoveCommands]) + +// the documented commands: the first cell of every table row that starts with a `/command` +const documented = new Set( + read(commandsDoc) + .split('\n') + .filter(line => line.startsWith('`/')) + .map(line => line.split(' | ')[0]) + .flatMap(cell => [...cell.matchAll(/`(\/[a-z-]+)[ `<]/g)].map(m => m[1])) + // `/` and `/remove-` describe the dynamic label commands, not a fixed command + .filter(cmd => !cmd.endsWith('-') && cmd !== '/'), +) + +const unsupported = ['/override', '/skip', '/retest-required'] + +describe(`${commandsDoc} agrees with the dispatcher`, () => { + it('finds the handlers, the aliases and the label commands in the source', () => { + expect(handlerCommands).toEqual(expect.arrayContaining(['/assign', '/lgtm', '/approve', '/retest', '/meow'])) + expect(explicitAliases).toEqual(expect.arrayContaining(['/remove-lgtm', '/remove-approve', '/unhold', '/remove-hold'])) + expect(new Set(handlerCommands).size).toBe(handlerCommands.length) + expect(new Set(explicitAliases).size).toBe(explicitAliases.length) + }) + + it('documents every implemented command', () => { + for (const cmd of implemented) { + expect(documented, `${cmd} is implemented but has no row in ${commandsDoc}`).toContain(cmd) + } + }) + + it('documents no command the dispatcher does not implement', () => { + for (const cmd of documented) { + expect(implemented, `${cmd} has a row in ${commandsDoc} but no handler`).toContain(cmd) + } + }) + + it('lists the unsupported Prow commands as unsupported, without a row and without a handler', () => { + const doc = read(commandsDoc) + for (const cmd of unsupported) { + expect(doc).toContain(`\`${cmd}\``) + expect(documented, `${cmd} must not have a command row`).not.toContain(cmd) + expect(implemented, `${cmd} must not be implemented`).not.toContain(cmd) + } + }) +}) + +describe(`${reusableWorkflowPath} prow-commands default`, () => { + interface Reusable { on: { workflow_call: { inputs: Record } } } + const reusable = yaml.load(read(reusableWorkflowPath)) as Reusable + const defaults = String(reusable.on.workflow_call.inputs['prow-commands'].default).split(/\s+/) + + it('is every handler and label command except /meow, with no alias and no remove form', () => { + const expected = [...handlerCommands.filter(cmd => cmd !== '/meow'), ...labelCommands] + expect([...defaults].sort()).toEqual([...expected].sort()) + }) +})