From 3e513aaabdec34ae0ab05339f5a38e6a0f2dadb9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 24 Jul 2026 16:05:53 +1200 Subject: [PATCH 01/18] Remove duplicate test --- tests/options.variadic.test.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index 072fa2277..47cebfa5b 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -180,12 +180,4 @@ describe('variadic options', () => { assert.deepEqual(program.opts().comma, ['CCC']); }); - - test('when option has default array then specified value is used instead of default (not appended)', () => { - const program = new commander.Command(); - program.option('-c,--comma [value...]', 'values', ['default']); - program.parse(['--comma', 'CCC'], { from: 'user' }); - - assert.deepEqual(program.opts().comma, ['CCC']); - }); }); From f9c56f631b3aaab9d6c73ddd52c1bfbafe3b883c Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 24 Jul 2026 16:06:18 +1200 Subject: [PATCH 02/18] Remove bogus call to configureHelp in test --- examples/help-centered.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/help-centered.mjs b/examples/help-centered.mjs index 206a23cdd..35528b4f5 100644 --- a/examples/help-centered.mjs +++ b/examples/help-centered.mjs @@ -25,8 +25,6 @@ class MyCommand extends Command { const program = new MyCommand(); -program.configureHelp({ MyCommand }); - program .option('-s', 'short flag') .option('-f, --flag', 'short and long flag') From c479eada8372468809a9165f97e3b79d08f15710 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 24 Jul 2026 17:34:13 +1200 Subject: [PATCH 03/18] Add eslint-node-test --- eslint.config.js | 8 ++++++++ package-lock.json | 34 ++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 43 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index e6454c97c..707b50bc6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import esLintjs from '@eslint/js'; import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import prettier from 'eslint-config-prettier'; +import nodeTest from 'eslint-node-test'; // Only run tseslint on the files that we have included for TypeScript. const tsconfigTsFiles = ['**/*.{ts,mts}']; // match "include" in tsconfig.ts.json; @@ -65,4 +66,11 @@ export default defineConfig( ], }, }, + { + files: ['**/*.js'], + plugins: { + 'node-test': nodeTest, + }, + extends: ['node-test/unopinionated'], // switch to recommended if like how unopinionated goes + }, ); diff --git a/package-lock.json b/package-lock.json index 9e09a8beb..72dad93ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^22.7.4", "eslint": "^10.0.2", "eslint-config-prettier": "^10.0.1", + "eslint-node-test": "^0.3.0", "globals": "^17.3.0", "prettier": "^3.2.5", "tsd": "^0.33.0", @@ -1037,6 +1038,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-node-test": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-node-test/-/eslint-node-test-0.3.0.tgz", + "integrity": "sha512-aV1YX7ydDH7VCDsBWNNuicISdujCaHiprs+vYSdechPAAS3HWR00HTHgDYTSnMqTlLJOnkvgi1jV1cB1TNV56Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "quote-js-string": "^0.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-node-test?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=10.4" + } + }, "node_modules/eslint-rule-docs": { "version": "1.1.235", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", @@ -2047,6 +2068,19 @@ "node": ">=8" } }, + "node_modules/quote-js-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/quote-js-string/-/quote-js-string-0.1.0.tgz", + "integrity": "sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/quote-js-string?sponsor=1" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index d8e5fd2aa..63bf1fcdc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/node": "^22.7.4", "eslint": "^10.0.2", "eslint-config-prettier": "^10.0.1", + "eslint-node-test": "^0.3.0", "globals": "^17.3.0", "prettier": "^3.2.5", "tsd": "^0.33.0", From 5e3537bbcfa37ddda8f1bff27458d062d31510bf Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 24 Jul 2026 17:34:39 +1200 Subject: [PATCH 04/18] Add checks on what throws in tests --- tests/options.registerClash.test.js | 4 ++-- tests/options.variadic.test.js | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/options.registerClash.test.js b/tests/options.registerClash.test.js index 9a73aa3f7..ffa76ad19 100644 --- a/tests/options.registerClash.test.js +++ b/tests/options.registerClash.test.js @@ -52,7 +52,7 @@ describe('.addOption()', () => { program .option('-c, --cheese ', 'cheese type') .addOption(new Option('-c, --conflict')); - }); + }, /conflicting flag/); }); test('when long option flags conflicts then throws', () => { @@ -61,6 +61,6 @@ describe('.addOption()', () => { program .option('-c, --cheese ', 'cheese type') .addOption(new Option('-H, --cheese')); - }); + }, /conflicting flag/); }); }); diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index 47cebfa5b..58e57588b 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -9,9 +9,14 @@ describe('variadic options', () => { const program = createTestCommand(); program.option('-r,--required '); - assert.throws(() => { - program.parse(['--required'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--required'], { from: 'user' }); + }, + { + code: 'commander.optionMissingArgument', + }, + ); }); test('when variadic with one value then set in array', () => { From acbd58e482c64714f5a54f75e85b2deabee0ab6b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 10:00:49 +1200 Subject: [PATCH 05/18] Update eslint --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72dad93ad..00025628a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -941,9 +941,9 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", "workspaces": [ From 4d6bd4989cdf1775566a176800bdb1f771f6af89 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 10:53:09 +1200 Subject: [PATCH 06/18] Lint for tests, mostly adding some sort of test on what is thrown by assert.throws --- tests/argument.choices.test.js | 18 ++++--- tests/argument.custom-processing.test.js | 2 +- tests/argument.variadic.test.js | 15 +----- tests/command.action.test.js | 9 ++-- tests/command.addCommand.test.js | 2 +- tests/command.addHelpText.test.js | 65 ++++++++++++++++-------- tests/options.mandatory.test.js | 45 ++++++++++------ 7 files changed, 94 insertions(+), 62 deletions(-) diff --git a/tests/argument.choices.test.js b/tests/argument.choices.test.js index ac5afef14..111209263 100644 --- a/tests/argument.choices.test.js +++ b/tests/argument.choices.test.js @@ -22,9 +22,12 @@ describe('Argument.choices()', () => { program.addArgument( new commander.Argument('').choices(['red', 'blue']), ); - assert.throws(() => { - program.parse(['orange'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['orange'], { from: 'user' }); + }, + { code: 'commander.invalidArgument' }, + ); }); }); @@ -50,8 +53,11 @@ describe('Argument.choices() parameter is treated as readonly, per TypeScript de const param = ['red', 'blue']; program.addArgument(new commander.Argument('').choices(param)); param.push('orange'); - assert.throws(() => { - program.parse(['orange'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['orange'], { from: 'user' }); + }, + { code: 'commander.invalidArgument' }, + ); }); }); diff --git a/tests/argument.custom-processing.test.js b/tests/argument.custom-processing.test.js index 544cc3748..9f658afcf 100644 --- a/tests/argument.custom-processing.test.js +++ b/tests/argument.custom-processing.test.js @@ -171,7 +171,7 @@ describe('custom processing function for command-argument', () => { const program = new commander.Command(); assert.throws(() => { program.argument('', 'float argument', 4); - }); + }, /a default value for a required argument is never used/); }); test('when custom processing for argument throws plain error then not CommanderError caught', () => { diff --git a/tests/argument.variadic.test.js b/tests/argument.variadic.test.js index cf16055b9..8593f3251 100644 --- a/tests/argument.variadic.test.js +++ b/tests/argument.variadic.test.js @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; // Testing variadic arguments. Testing all the action arguments, but could test just variadicArg. -describe('Command variadic argument using .argument()', (t) => { +describe('Command variadic argument using .argument()', () => { test('when no extra arguments specified for program then variadic arg is empty array', (t) => { const actionMock = t.mock.fn(); const program = new commander.Command(); @@ -117,17 +117,4 @@ describe('Command variadic argument using .argument()', (t) => { program.parse(['one', 'two'], { from: 'user' }); assert.deepEqual(passedArg, ['one', 'two']); }); - - test('when variadic has default array then specified value is used instead of default (not appended)', () => { - const program = new commander.Command(); - let passedArg; - program - .addArgument(new commander.Argument('[value...]').default(['DEFAULT'])) - .action((value) => { - passedArg = value; - }); - - program.parse(['one', 'two'], { from: 'user' }); - assert.deepEqual(passedArg, ['one', 'two']); - }); }); diff --git a/tests/command.action.test.js b/tests/command.action.test.js index cafd8e1c8..c0ab1f33e 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -54,9 +54,12 @@ describe('Command.action()', () => { test(`via ${methodName}`, (t) => { const actionMock = t.mock.fn(); program.action(actionMock); - assert.throws(() => { - program.parse(['node', 'test']); - }); + assert.throws( + () => { + program.parse(['node', 'test']); + }, + { code: 'commander.missingArgument' }, + ); assert.equal(actionMock.mock.callCount(), 0); }); }); diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js index 80f217167..a881eaabc 100644 --- a/tests/command.addCommand.test.js +++ b/tests/command.addCommand.test.js @@ -47,7 +47,7 @@ describe('Command.addCommand()', () => { const cmd = new commander.Command(); assert.throws(() => { program.addCommand(cmd); - }); + }, /Command passed to \.addCommand\(\) must have a name/); }); test('when executable command with custom executableFile passed to .addCommand then ok', () => { diff --git a/tests/command.addHelpText.test.js b/tests/command.addHelpText.test.js index c0eb1858f..a055a8d48 100644 --- a/tests/command.addHelpText.test.js +++ b/tests/command.addHelpText.test.js @@ -97,7 +97,7 @@ describe('Command.addHelpText(): program calls to addHelpText', () => { const program = new commander.Command(); assert.throws(() => { program.addHelpText('silly', 'text'); - }); + }, /Unexpected value for position to addHelpText/); }); }); @@ -157,18 +157,24 @@ describe('Command.addHelpText(): context checks with full parse', () => { test('when help requested then text is on stdout', () => { const program = new commander.Command(); program.exitOverride().addHelpText('before', 'text'); - assert.throws(() => { - program.parse(['--help'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--help'], { from: 'user' }); + }, + { code: 'commander.helpDisplayed' }, + ); assert.equal(stdoutSpy.mock.calls[0].arguments[0], 'text\n'); }); test('when help for error then text is on stderr', () => { const program = new commander.Command(); program.exitOverride().addHelpText('before', 'text').command('sub'); - assert.throws(() => { - program.parse([], { from: 'user' }); - }); + assert.throws( + () => { + program.parse([], { from: 'user' }); + }, + { code: 'commander.help' }, + ); assert.equal(stderrSpy.mock.calls[0].arguments[0], 'text\n'); }); @@ -178,9 +184,12 @@ describe('Command.addHelpText(): context checks with full parse', () => { program.exitOverride().addHelpText('before', (contextParam) => { context = contextParam; }); - assert.throws(() => { - program.parse(['--help'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--help'], { from: 'user' }); + }, + { code: 'commander.helpDisplayed' }, + ); assert.equal(context.error, false); }); @@ -193,9 +202,12 @@ describe('Command.addHelpText(): context checks with full parse', () => { context = contextParam; }) .command('sub'); - assert.throws(() => { - program.parse([], { from: 'user' }); - }); + assert.throws( + () => { + program.parse([], { from: 'user' }); + }, + { code: 'commander.help' }, + ); assert.equal(context.error, true); }); @@ -205,9 +217,12 @@ describe('Command.addHelpText(): context checks with full parse', () => { program.exitOverride().addHelpText('before', (contextParam) => { context = contextParam; }); - assert.throws(() => { - program.parse(['--help'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--help'], { from: 'user' }); + }, + { code: 'commander.helpDisplayed' }, + ); assert.equal(context.command, program); }); @@ -218,9 +233,12 @@ describe('Command.addHelpText(): context checks with full parse', () => { const sub = program.command('sub').addHelpText('before', (contextParam) => { context = contextParam; }); - assert.throws(() => { - program.parse(['sub', '--help'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['sub', '--help'], { from: 'user' }); + }, + { code: 'commander.helpDisplayed' }, + ); assert.equal(context.command, sub); }); @@ -231,9 +249,12 @@ describe('Command.addHelpText(): context checks with full parse', () => { context = contextParam; }); const sub = program.command('sub'); - assert.throws(() => { - program.parse(['sub', '--help'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['sub', '--help'], { from: 'user' }); + }, + { code: 'commander.helpDisplayed' }, + ); assert.equal(context.command, sub); }); }); diff --git a/tests/options.mandatory.test.js b/tests/options.mandatory.test.js index 85a34aa8b..5278b0a6a 100644 --- a/tests/options.mandatory.test.js +++ b/tests/options.mandatory.test.js @@ -95,18 +95,24 @@ describe('Command.requiredOption()', () => { const program = createTestCommand(); program.requiredOption('--cheese ', 'cheese type'); - assert.throws(() => { - program.parse(['node', 'test']); - }); + assert.throws( + () => { + program.parse(['node', 'test']); + }, + { code: 'commander.missingMandatoryOptionValue' }, + ); }); test('when program has optional option not specified then error', () => { const program = createTestCommand(); program.requiredOption('--cheese [type]', 'cheese type'); - assert.throws(() => { - program.parse(['node', 'test']); - }); + assert.throws( + () => { + program.parse(['node', 'test']); + }, + { code: 'commander.missingMandatoryOptionValue' }, + ); }); test('when program has yes/no not specified then error', () => { @@ -115,9 +121,12 @@ describe('Command.requiredOption()', () => { .requiredOption('--cheese', 'cheese type') .option('--no-cheese', 'no cheese thanks'); - assert.throws(() => { - program.parse(['node', 'test']); - }); + assert.throws( + () => { + program.parse(['node', 'test']); + }, + { code: 'commander.missingMandatoryOptionValue' }, + ); }); test('when program has required value not specified and subcommand then error', () => { @@ -127,9 +136,12 @@ describe('Command.requiredOption()', () => { .command('sub') .action(() => {}); - assert.throws(() => { - program.parse(['node', 'test', 'sub']); - }); + assert.throws( + () => { + program.parse(['node', 'test', 'sub']); + }, + { code: 'commander.missingMandatoryOptionValue' }, + ); }); }); @@ -173,9 +185,12 @@ describe('Command.requiredOption()', () => { .requiredOption('--subby ', 'description') .action(() => {}); - assert.throws(() => { - program.parse(['node', 'test', 'sub']); - }); + assert.throws( + () => { + program.parse(['node', 'test', 'sub']); + }, + { code: 'commander.missingMandatoryOptionValue' }, + ); }); test('when command has required value but not called then no error', () => { From f8f24f4717b2ae83c48c65c4dca827cb89fd3262 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 12:41:52 +1200 Subject: [PATCH 07/18] Lint for tests, mostly adding some sort of test on what is thrown by assert.throws --- eslint.config.js | 3 ++ tests/command.addCommand.test.js | 1 + tests/command.allowExcessArguments.test.js | 54 ++++++++++++------- tests/command.allowUnknownOption.test.js | 27 ++++++---- tests/command.configureOutput.test.js | 36 ++++++++----- ...ommand.executableSubcommand.lookup.test.js | 5 +- tests/command.help.test.js | 2 +- tests/command.hook.test.js | 2 +- tests/command.parse.test.js | 6 +-- tests/command.positionalOptions.test.js | 4 +- tests/command.showHelpAfterError.test.js | 16 +++--- tests/command.unknownCommand.test.js | 9 ++-- ...commander.storeOptionsAsProperties.test.js | 4 +- tests/negatives.test.js | 16 ++++-- tests/options.choices.test.js | 20 ++++--- tests/options.implies.test.js | 11 ++-- 16 files changed, 139 insertions(+), 77 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 707b50bc6..81e181474 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -72,5 +72,8 @@ export default defineConfig( 'node-test': nodeTest, }, extends: ['node-test/unopinionated'], // switch to recommended if like how unopinionated goes + rules: { + 'node-test/no-constant-assertion': 'off', // getting false positives, opened issue, disable for now + }, }, ); diff --git a/tests/command.addCommand.test.js b/tests/command.addCommand.test.js index a881eaabc..d91f23439 100644 --- a/tests/command.addCommand.test.js +++ b/tests/command.addCommand.test.js @@ -36,6 +36,7 @@ describe('Command.addCommand()', () => { case 'number': case 'undefined': // Compare values in a way that will be readable in test failure message. + // eslint-disable-next-line node-test/no-conditional-assertion assert.equal(`${key}:${cmd1[key]}`, `${key}:${cmd2[key]}`); break; } diff --git a/tests/command.allowExcessArguments.test.js b/tests/command.allowExcessArguments.test.js index 4f125bd54..9bafced4f 100644 --- a/tests/command.allowExcessArguments.test.js +++ b/tests/command.allowExcessArguments.test.js @@ -16,9 +16,12 @@ describe('Command.allowExcessArguments()', () => { const program = createTestCommand(); configureCommand(program); - assert.throws(() => { - program.parse(['excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify excess program argument and allowExcessArguments(false) then error', () => { @@ -26,9 +29,12 @@ describe('Command.allowExcessArguments()', () => { configureCommand(program); program.allowExcessArguments(false); - assert.throws(() => { - program.parse(['excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify excess program argument and allowExcessArguments() then no error', () => { @@ -56,9 +62,12 @@ describe('Command.allowExcessArguments()', () => { const sub = program.command('sub'); configureCommand(sub); - assert.throws(() => { - program.parse(['sub', 'excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['sub', 'excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify excess command argument and allowExcessArguments(false) then error', () => { @@ -66,9 +75,12 @@ describe('Command.allowExcessArguments()', () => { const sub = program.command('sub').allowExcessArguments(false); configureCommand(sub); - assert.throws(() => { - program.parse(['sub', 'excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['sub', 'excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify expected arg and allowExcessArguments(false) then no error', () => { @@ -86,9 +98,12 @@ describe('Command.allowExcessArguments()', () => { configureCommand(program); program.argument('').allowExcessArguments(false); - assert.throws(() => { - program.parse(['file', 'excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['file', 'excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify excess after [arg] and allowExcessArguments(false) then error', () => { @@ -96,9 +111,12 @@ describe('Command.allowExcessArguments()', () => { configureCommand(program); program.argument('[file]').allowExcessArguments(false); - assert.throws(() => { - program.parse(['file', 'excess'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['file', 'excess'], { from: 'user' }); + }, + { code: 'commander.excessArguments' }, + ); }); test('when specify args for [args...] and allowExcessArguments(false) then no error', () => { diff --git a/tests/command.allowUnknownOption.test.js b/tests/command.allowUnknownOption.test.js index d801084c2..3989b93b5 100644 --- a/tests/command.allowUnknownOption.test.js +++ b/tests/command.allowUnknownOption.test.js @@ -9,18 +9,24 @@ describe('Command.allowUnknownOption()', () => { const program = createTestCommand(); program.option('-p, --pepper', 'add pepper'); - assert.throws(() => { - program.parse(['node', 'test', '-m']); - }); + assert.throws( + () => { + program.parse(['node', 'test', '-m']); + }, + { code: 'commander.unknownOption' }, + ); }); test('when specify unknown program option and allowUnknownOption(false) then error', () => { const program = createTestCommand(); program.allowUnknownOption(false).option('-p, --pepper', 'add pepper'); - assert.throws(() => { - program.parse(['node', 'test', '-m']); - }); + assert.throws( + () => { + program.parse(['node', 'test', '-m']); + }, + { code: 'commander.unknownOption' }, + ); }); test('when specify unknown program option and allowUnknownOption() then no error', () => { @@ -57,9 +63,12 @@ describe('Command.allowUnknownOption()', () => { .option('-p, --pepper', 'add pepper') .action(() => {}); - assert.throws(() => { - program.parse(['node', 'test', 'sub', '-m']); - }); + assert.throws( + () => { + program.parse(['node', 'test', 'sub', '-m']); + }, + { code: 'commander.unknownOption' }, + ); }); test('when specify unknown command option and allowUnknownOption then no error', () => { diff --git a/tests/command.configureOutput.test.js b/tests/command.configureOutput.test.js index 1924c2ab9..f949d7a65 100644 --- a/tests/command.configureOutput.test.js +++ b/tests/command.configureOutput.test.js @@ -39,9 +39,12 @@ describe('Command.configureOutput()', () => { const program = new commander.Command(); program.exitOverride().version('1.2.3'); - assert.throws(() => { - program.parse(['--version'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--version'], { from: 'user' }); + }, + { code: 'commander.version' }, + ); assert.equal(writeSpy.mock.callCount(), 1); }); @@ -55,9 +58,12 @@ describe('Command.configureOutput()', () => { .version('1.2.3') .configureOutput({ writeOut: customWrite }); - assert.throws(() => { - program.parse(['--version'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--version'], { from: 'user' }); + }, + { code: 'commander.version' }, + ); assert.equal(writeSpy.mock.callCount(), 0); assert.equal(customWrite.mock.callCount(), 1); @@ -284,9 +290,12 @@ describe('Command.configureOutput()', () => { outputError, }); - assert.throws(() => { - program.parse(['--unknownOption'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--unknownOption'], { from: 'user' }); + }, + { code: 'commander.unknownOption' }, + ); assert.equal( outputError.mock.calls[0].arguments[0], "error: unknown option '--unknownOption'\n", @@ -303,9 +312,12 @@ describe('Command.configureOutput()', () => { const program = new commander.Command(); program.exitOverride().configureOutput({ writeErr, outputError }); - assert.throws(() => { - program.parse(['--unknownOption'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--unknownOption'], { from: 'user' }); + }, + { code: 'commander.unknownOption' }, + ); assert.equal( outputError.mock.calls[0].arguments[0], "error: unknown option '--unknownOption'\n", diff --git a/tests/command.executableSubcommand.lookup.test.js b/tests/command.executableSubcommand.lookup.test.js index f6a0d57d5..99ec30809 100644 --- a/tests/command.executableSubcommand.lookup.test.js +++ b/tests/command.executableSubcommand.lookup.test.js @@ -1,3 +1,4 @@ +/* eslint-disable node-test/no-conditional-assertion */ import * as childProcess from 'child_process'; import * as path from 'path'; import * as util from 'util'; @@ -19,7 +20,7 @@ describe('executable subcommand lookup ', () => { return execFileAsync('node', [pm, 'list']).catch((err) => { if (process.platform === 'win32') { // Get uncaught thrown error on Windows - assert.ok(err.stderr !== undefined); + assert.notStrictEqual(err.stderr, undefined); } else { assert.match(err.stderr, /Error: 'pm-list' does not exist/); } @@ -30,7 +31,7 @@ describe('executable subcommand lookup ', () => { return execFileAsync('node', [pm, 'lst']).catch((err) => { if (process.platform === 'win32') { // Get uncaught thrown error on Windows - assert.ok(err.stderr !== undefined); + assert.notStrictEqual(err.stderr, undefined); } else { assert.match(err.stderr, /Error: 'pm-list' does not exist/); } diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 030cb858b..61e77ba33 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -90,7 +90,7 @@ Commands: const program = new commander.Command(); assert.throws(() => { program.outputHelp((helpInformation) => 3); - }); + }, /outputHelp callback must return a string or a Buffer/); }); test('when command sets deprecated noHelp then not displayed in helpInformation', () => { diff --git a/tests/command.hook.test.js b/tests/command.hook.test.js index dbede8946..d963275fe 100644 --- a/tests/command.hook.test.js +++ b/tests/command.hook.test.js @@ -7,7 +7,7 @@ describe('Command.hook()', () => { const program = new commander.Command(); assert.throws(() => { program.hook('silly', () => {}); - }); + }, /Unexpected value for event passed to hook/); }); test('when no action then action hooks not called', (t) => { diff --git a/tests/command.parse.test.js b/tests/command.parse.test.js index 61912b404..2f59f5152 100644 --- a/tests/command.parse.test.js +++ b/tests/command.parse.test.js @@ -79,7 +79,7 @@ describe('Command.parse()', () => { program.argument('[args...]'); assert.throws(() => { program.parse(['node', 'script.js'], { from: 'silly' }); - }); + }, /unexpected parse option/); }); describe('when node execArgv includes node flags', () => { @@ -127,7 +127,7 @@ describe('Command.parse()', () => { program.argument('[args...]'); assert.throws(() => { program.parse('node', 'test'); - }); + }, /first parameter to parse must be array or undefined/); }); describe('parse parameter is treated as readonly, per TypeScript declaration', () => { @@ -436,7 +436,7 @@ describe('Command.parse()', () => { program.parse(); assert.throws(() => { program.parse(); - }); + }, /Can not call parse again when storeOptionsAsProperties is true/); }); }); }); diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index fe7a96af1..d2506dc50 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -385,7 +385,7 @@ describe('positional options using Command.passThroughOptions() and Command.enab assert.throws(() => { sub.passThroughOptions(); - }); + }, /passThroughOptions cannot be used for 'sub' without turning on enablePositionalOptions/); }); test('when program not positional and add subcommand with passThroughOptions then error', () => { @@ -394,7 +394,7 @@ describe('positional options using Command.passThroughOptions() and Command.enab assert.throws(() => { program.addCommand(sub); - }); + }, /passThroughOptions cannot be used for 'sub' without turning on enablePositionalOptions/); }); }); diff --git a/tests/command.showHelpAfterError.test.js b/tests/command.showHelpAfterError.test.js index c39c4e064..6f591c712 100644 --- a/tests/command.showHelpAfterError.test.js +++ b/tests/command.showHelpAfterError.test.js @@ -27,7 +27,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.missingArgument', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -42,7 +42,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.optionMissingArgument', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -57,7 +57,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.missingMandatoryOptionValue', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -71,7 +71,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.unknownOption', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -86,7 +86,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.excessArguments', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -101,7 +101,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.unknownCommand', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); @@ -116,7 +116,7 @@ describe('Command.showHelpAfterError(message)', () => { code: 'commander.invalidArgument', }, ); - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); }); @@ -134,6 +134,6 @@ test('when Command.showHelpAfterError() and error and then shows full help', () } catch (err) { /* empty */ } - const lastCall = writeMock.mock.calls[writeMock.mock.calls.length - 1]; + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], program.helpInformation()); }); diff --git a/tests/command.unknownCommand.test.js b/tests/command.unknownCommand.test.js index a2f51f29e..7b9af4b30 100644 --- a/tests/command.unknownCommand.test.js +++ b/tests/command.unknownCommand.test.js @@ -5,9 +5,12 @@ import assert from 'node:assert/strict'; describe('parsing unknown command', () => { test('when unknown argument in simple program then error', () => { const program = createTestCommand(); - assert.throws(() => { - program.parse('node test.js unknown'.split(' ')); - }); + assert.throws( + () => { + program.parse('node test.js unknown'.split(' ')); + }, + { code: 'commander.excessArguments' }, + ); }); test('when unknown command but action handler taking arg then no error', () => { diff --git a/tests/commander.storeOptionsAsProperties.test.js b/tests/commander.storeOptionsAsProperties.test.js index 490cfa427..1e169741c 100644 --- a/tests/commander.storeOptionsAsProperties.test.js +++ b/tests/commander.storeOptionsAsProperties.test.js @@ -86,7 +86,7 @@ describe('Command.storeOptionsAsProperties()', () => { program.option('--port ', 'port number', '80'); assert.throws(() => { program.storeOptionsAsProperties(); - }); + }, /call \.storeOptionsAsProperties\(\) before adding options/); }); test('when storeOptionsAsProperties() after setting option value then throw', () => { @@ -94,6 +94,6 @@ describe('Command.storeOptionsAsProperties()', () => { program.setOptionValue('foo', 'bar'); assert.throws(() => { program.storeOptionsAsProperties(); - }); + }, /call \.storeOptionsAsProperties\(\) before setting option values/); }); }); diff --git a/tests/negatives.test.js b/tests/negatives.test.js index 1bd4a0194..a0097ba4a 100644 --- a/tests/negatives.test.js +++ b/tests/negatives.test.js @@ -98,7 +98,7 @@ describe('negative numbers', () => { ); }); - test(`when digit option defined and option-argument is %s then negative not consumed`, () => { + test(`when digit option defined and option-argument is ${value} then negative not consumed`, () => { const program = createTestCommand(); program .option('-o, --optional [value]', 'optional option') @@ -109,7 +109,7 @@ describe('negative numbers', () => { assert.equal(program.opts()['optional'], customConsume ? value : true); }); - test(`when digit option defined and command-argument is %s then negative not consumed`, () => { + test(`when digit option defined and command-argument is ${value} then negative not consumed`, () => { const program = createTestCommand(); program.argument('[value]').option('-9', 'register option using digit'); const args = [value]; @@ -166,7 +166,9 @@ describe('negative numbers', () => { leafArgs = args; }); const args = ['leaf', '-1']; - assert.throws(() => program.parse(args, { from: 'user' })); + assert.throws(() => program.parse(args, { from: 'user' }), { + code: 'commander.unknownOption', + }); }); test('when default command without digit option then negatives accepted', () => { @@ -189,7 +191,9 @@ describe('negative numbers', () => { .option('-2') .argument('[value...]') .action(() => {}); - assert.throws(() => program.parse(['-1'], { from: 'user' })); + assert.throws(() => program.parse(['-1'], { from: 'user' }), { + code: 'commander.unknownOption', + }); }); test('when program has subcommand and action handler then negative command-argument unsupported', () => { @@ -198,6 +202,8 @@ describe('negative numbers', () => { const program = createTestCommand(); program.argument('[value...]').action(() => {}); program.command('leaf').action(() => {}); - assert.throws(() => program.parse(['-1'], { from: 'user' })); + assert.throws(() => program.parse(['-1'], { from: 'user' }), { + code: 'commander.unknownOption', + }); }); }); diff --git a/tests/options.choices.test.js b/tests/options.choices.test.js index f49437607..42ce8b09c 100644 --- a/tests/options.choices.test.js +++ b/tests/options.choices.test.js @@ -19,9 +19,12 @@ describe('Option.choices()', () => { program.addOption( new commander.Option('--colour ').choices(['red', 'blue']), ); - assert.throws(() => { - program.parse(['--colour', 'orange'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--colour', 'orange'], { from: 'user' }); + }, + { code: 'commander.invalidArgument' }, + ); }); describe('choices parameter is treated as readonly, per TypeScript declaration', () => { @@ -41,16 +44,19 @@ describe('Option.choices()', () => { assert.deepEqual(param, original); }); - test('when choices called and parameter changed the choices does not change', () => { + test('when choices called and parameter changed then accepted choices does not change', () => { const program = createTestCommand(); const param = ['red', 'blue']; program.addOption( new commander.Option('--colour ').choices(param), ); param.push('orange'); - assert.throws(() => { - program.parse(['--colour', 'orange'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--colour', 'orange'], { from: 'user' }); + }, + { code: 'commander.invalidArgument' }, + ); }); }); }); diff --git a/tests/options.implies.test.js b/tests/options.implies.test.js index e79e1e1a0..4b17fe491 100644 --- a/tests/options.implies.test.js +++ b/tests/options.implies.test.js @@ -159,9 +159,12 @@ describe('Option.implies()', () => { .addOption(new Option('--binary').conflicts('unary')) .addOption(new Option('--one').implies({ unary: true })); - assert.throws(() => { - program.parse(['--binary', '--one'], { from: 'user' }); - }); + assert.throws( + () => { + program.parse(['--binary', '--one'], { from: 'user' }); + }, + { code: 'commander.conflictingOption' }, + ); }); test('when requiredOption with implied value then not throw', () => { @@ -191,7 +194,7 @@ describe('Option.implies()', () => { .addOption(new Option('--foo').implies({ bar: 'implied' })) .option('-b, --bar '); program.parse(['--foo', '--foo'], { from: 'user' }); - assert.deepEqual(program.opts().bar, 'implied'); + assert.equal(program.opts().bar, 'implied'); }); test('when implied option has custom processing then custom processing not called', () => { From 3b13cb85a33c93832338860365b27dc4477485eb Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:10:59 +1200 Subject: [PATCH 08/18] remove trailing spaces from test names --- tests/command.executableSubcommand.inspect.test.js | 2 +- tests/command.executableSubcommand.lookup.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/command.executableSubcommand.inspect.test.js b/tests/command.executableSubcommand.inspect.test.js index 1415bcce9..71e047c6f 100644 --- a/tests/command.executableSubcommand.inspect.test.js +++ b/tests/command.executableSubcommand.inspect.test.js @@ -15,7 +15,7 @@ const inspectCommand = path.join( 'inspect.js', ); -describe('executable subcommand support for --inspect ', () => { +describe('executable subcommand support for --inspect', () => { test('when execArgv empty then spawn execArgs empty', async () => { const { stdout } = await execFileAsync('node', [inspectCommand, 'sub']); assert.equal(stdout, '[]\n'); diff --git a/tests/command.executableSubcommand.lookup.test.js b/tests/command.executableSubcommand.lookup.test.js index 99ec30809..c019fb74e 100644 --- a/tests/command.executableSubcommand.lookup.test.js +++ b/tests/command.executableSubcommand.lookup.test.js @@ -15,7 +15,7 @@ import assert from 'node:assert/strict'; const testOrSkipOnWindows = process.platform === 'win32' ? test.skip : test; const pm = path.join(import.meta.dirname, './fixtures/pm'); -describe('executable subcommand lookup ', () => { +describe('executable subcommand lookup', () => { test('when subcommand file missing then error', () => { return execFileAsync('node', [pm, 'list']).catch((err) => { if (process.platform === 'win32') { From 469ef4dfebc1c4d7963dd7eed7d7f50e31664434 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:11:45 +1200 Subject: [PATCH 09/18] Single line of code in assert.throws() --- tests/options.registerClash.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/options.registerClash.test.js b/tests/options.registerClash.test.js index ffa76ad19..0a596c4f0 100644 --- a/tests/options.registerClash.test.js +++ b/tests/options.registerClash.test.js @@ -4,9 +4,9 @@ import assert from 'node:assert/strict'; describe('registering clashing options', () => { test('when short option flag conflicts then throws', () => { + const program = new Command(); assert.throws( () => { - const program = new Command(); program .option('-c, --cheese ', 'cheese type') .option('-c, --conflict'); @@ -18,9 +18,9 @@ describe('registering clashing options', () => { }); test('when long option flag conflicts then throws', () => { + const program = new Command(); assert.throws( () => { - const program = new Command(); program .option('-c, --cheese ', 'cheese type') .option('-H, --cheese'); @@ -47,8 +47,8 @@ describe('registering clashing options', () => { describe('.addOption()', () => { test('when short option flags conflicts then throws', () => { + const program = new Command(); assert.throws(() => { - const program = new Command(); program .option('-c, --cheese ', 'cheese type') .addOption(new Option('-c, --conflict')); @@ -56,8 +56,8 @@ describe('.addOption()', () => { }); test('when long option flags conflicts then throws', () => { + const program = new Command(); assert.throws(() => { - const program = new Command(); program .option('-c, --cheese ', 'cheese type') .addOption(new Option('-H, --cheese')); From c9a9e98188d035cc87ebf433a0ff630f72fc7090 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:12:15 +1200 Subject: [PATCH 10/18] Use assert.of(value) rather than assert(value) --- tests/command.help.test.js | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 61e77ba33..a3be82b72 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -97,14 +97,14 @@ Commands: const program = new commander.Command(); program.command('secret', 'secret description', { noHelp: true }); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('secret')); + assert.ok(!helpInformation.includes('secret')); }); test('when command sets hidden then not displayed in helpInformation', () => { const program = new commander.Command(); program.command('secret', 'secret description', { hidden: true }); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('secret')); + assert.ok(!helpInformation.includes('secret')); }); test('when addCommand with hidden:true then not displayed in helpInformation', () => { @@ -113,23 +113,23 @@ Commands: const program = new commander.Command(); program.addCommand(secretCmd, { hidden: true }); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('secret')); + assert.ok(!helpInformation.includes('secret')); }); test('when help short flag masked then not displayed in helpInformation', () => { const program = new commander.Command(); program.option('-h, --host', 'select host'); const helpInformation = program.helpInformation(); - assert(helpInformation.includes(' -h, --host')); - assert(!helpInformation.includes(' -h, --help')); - assert(helpInformation.includes(' --help')); + assert.ok(helpInformation.includes(' -h, --host')); + assert.ok(!helpInformation.includes(' -h, --help')); + assert.ok(helpInformation.includes(' --help')); }); test('when both help flags masked then not displayed in helpInformation', () => { const program = new commander.Command(); program.option('-h, --help', 'custom'); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('display help')); + assert.ok(!helpInformation.includes('display help')); }); test('when call .help then output on stdout', (t) => { @@ -169,15 +169,15 @@ Commands: // No custom options, no version option, no help option program.helpOption(false); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('Options')); + assert.ok(!helpInformation.includes('Options')); }); test('when negated option then option included in helpInformation', () => { const program = new commander.Command(); program.option('-C, --no-colour', 'colourless'); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('--no-colour')); - assert(helpInformation.includes('colourless')); + assert.ok(helpInformation.includes('--no-colour')); + assert.ok(helpInformation.includes('colourless')); }); test('when option.hideHelp() then option not included in helpInformation', () => { @@ -186,7 +186,7 @@ Commands: new commander.Option('-s,--secret', 'secret option').hideHelp(), ); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('secret')); + assert.ok(!helpInformation.includes('secret')); }); test('when option.hideHelp(true) then option not included in helpInformation', () => { @@ -195,7 +195,7 @@ Commands: new commander.Option('-s,--secret', 'secret option').hideHelp(true), ); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('secret')); + assert.ok(!helpInformation.includes('secret')); }); test('when option.hideHelp(false) then option included in helpInformation', () => { @@ -204,14 +204,14 @@ Commands: new commander.Option('-s,--secret', 'secret option').hideHelp(false), ); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('secret')); + assert.ok(helpInformation.includes('secret')); }); test('when option has default value then default included in helpInformation', () => { const program = new commander.Command(); program.option('-p, --port ', 'port number', 80); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('(default: 80)')); + assert.ok(helpInformation.includes('(default: 80)')); }); test('when option has default value description then default description included in helpInformation', () => { @@ -223,7 +223,7 @@ Commands: ), ); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('(default: home)')); + assert.ok(helpInformation.includes('(default: home)')); }); test('when option has choices then choices included in helpInformation', () => { @@ -232,7 +232,7 @@ Commands: new commander.Option('-c, --colour ').choices(['red', 'blue']), ); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('(choices: "red", "blue")')); + assert.ok(helpInformation.includes('(choices: "red", "blue")')); }); test('when option has choices and default then both included in helpInformation', () => { @@ -243,7 +243,7 @@ Commands: .default('red'), ); const helpInformation = program.helpInformation(); - assert( + assert.ok( helpInformation.includes('(choices: "red", "blue", default: "red")'), ); }); @@ -252,7 +252,7 @@ Commands: const program = new commander.Command(); program.name('foo').argument(''); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('Usage: foo [options] ')); + assert.ok(helpInformation.includes('Usage: foo [options] ')); }); test('when argument described then included in helpInformation', () => { @@ -301,7 +301,7 @@ Commands: ]), ); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('(choices: "red", "blue")')); + assert.ok(helpInformation.includes('(choices: "red", "blue")')); }); test('when argument has choices and default then both included in helpInformation', () => { @@ -312,7 +312,7 @@ Commands: .default('red'), ); const helpInformation = program.helpInformation(); - assert( + assert.ok( helpInformation.includes('(choices: "red", "blue", default: "red")'), ); }); From b9944669ad674535af24617c437e161e9a146732 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:39:09 +1200 Subject: [PATCH 11/18] Major refactor to simplify tests --- tests/command.exitOverride.test.js | 650 ++++++++++++++--------------- 1 file changed, 314 insertions(+), 336 deletions(-) diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 5afdcdad0..6894652fd 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -1,272 +1,245 @@ import * as commander from '../index.js'; import * as path from 'path'; +import { createTestCommand } from './testHelpers.js'; // createTestCommand sets exitOverride() import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; // Test details of the exitOverride errors. -// The important checks are the exitCode and code which are intended to be stable for -// semver minor versions. For now, also testing the error.message and that output occurred -// to detect accidental changes in behaviour. - -function expectCommanderError(err, exitCode, code, message) { - assert(err instanceof commander.CommanderError); - assert.equal(err.exitCode, exitCode); - assert.equal(err.code, code); - assert.equal(err.message, message); -} +// `exitCode` and `code` are intended to be stable for semver minor versions. +// Also testing `message` to detect accidental changes in behaviour. describe('Command.exitOverride', () => { - // Use internal knowledge to suppress output to keep test output clean. - - test('when specify unknown program option then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.exitOverride(); - - let caughtErr; - try { - program.parse(['node', 'test', '-m']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.unknownOption', - "error: unknown option '-m'", + test('when specify unknown program option then throw CommanderError', () => { + const program = createTestCommand(); + + assert.throws( + () => { + program.parse(['node', 'test', '-m']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.unknownOption'); + assert.equal(err.message, "error: unknown option '-m'"); + return true; + }, ); }); - test('when specify unknown command then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.name('prog').exitOverride().command('sub'); - - let caughtErr; - try { - program.parse(['node', 'test', 'oops']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.unknownCommand', - "error: unknown command 'oops'", + test('when specify unknown command then throw CommanderError', () => { + const program = createTestCommand(); + program.name('prog').command('sub'); + + assert.throws( + () => { + program.parse(['node', 'test', 'oops']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.unknownCommand'); + assert.equal(err.message, "error: unknown command 'oops'"); + return true; + }, ); }); // Same error as above, but with custom handler. - test('when supply custom handler then throw custom error', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when supply custom handler then throw custom error', () => { const customError = new commander.CommanderError( 123, 'custom-code', 'custom-message', ); - const program = new commander.Command(); + const program = createTestCommand(); program.exitOverride((_err) => { throw customError; }); - let caughtErr; - try { - program.parse(['node', 'test', '-m']); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - customError.exitCode, - customError.code, - customError.message, + assert.throws( + () => { + program.parse(['node', 'test', '-m']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, customError.exitCode); + assert.equal(err.code, customError.code); + assert.equal(err.message, customError.message); + return true; + }, ); }); - test('when specify option without required value then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); + test('when specify option without required value then throw CommanderError', () => { const optionFlags = '-p, --pepper '; - const program = new commander.Command(); - program.exitOverride().option(optionFlags, 'add pepper'); - - let caughtErr; - try { - program.parse(['node', 'test', '--pepper']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.optionMissingArgument', - `error: option '${optionFlags}' argument missing`, + const program = createTestCommand(); + program.option(optionFlags, 'add pepper'); + + assert.throws( + () => { + program.parse(['node', 'test', '--pepper']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.optionMissingArgument'); + assert.equal( + err.message, + `error: option '${optionFlags}' argument missing`, + ); + return true; + }, ); }); - test('when specify command without required argument then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program - .exitOverride() - .command('compress ') - .action(() => {}); - - let caughtErr; - try { - program.parse(['node', 'test', 'compress']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.missingArgument', - "error: missing required argument 'arg-name'", + test('when specify command without required argument then throw CommanderError', () => { + const program = createTestCommand(); + program.command('compress ').action(() => {}); + + assert.throws( + () => { + program.parse(['node', 'test', 'compress']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.missingArgument'); + assert.equal( + err.message, + "error: missing required argument 'arg-name'", + ); + return true; + }, ); }); - test('when specify program without required argument and no action handler then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.exitOverride().argument(''); - - let caughtErr; - try { - program.parse(['node', 'test']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.missingArgument', - "error: missing required argument 'arg-name'", + test('when specify program without required argument and no action handler then throw CommanderError', () => { + const program = createTestCommand(); + program.argument(''); + + assert.throws( + () => { + program.parse(['node', 'test']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.missingArgument'); + assert.equal( + err.message, + "error: missing required argument 'arg-name'", + ); + return true; + }, ); }); - test('when specify excess argument then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program - .exitOverride() - .allowExcessArguments(false) - .action(() => {}); - - let caughtErr; - try { - program.parse(['node', 'test', 'excess']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.excessArguments', - 'error: too many arguments. Expected 0 arguments but got 1: excess.', + test('when specify excess argument then throw CommanderError', () => { + const program = createTestCommand(); + program.action(() => {}); + + assert.throws( + () => { + program.parse(['node', 'test', 'excess']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.excessArguments'); + assert.equal( + err.message, + 'error: too many arguments. Expected 0 arguments but got 1: excess.', + ); + return true; + }, ); }); - test('when specify command with excess argument then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program - .exitOverride() - .command('speak') - .allowExcessArguments(false) - .action(() => {}); - - let caughtErr; - try { - program.parse(['node', 'test', 'speak', 'excess']); - } catch (err) { - caughtErr = err; - } - - assert(stderrSpy.mock.callCount() > 0); - expectCommanderError( - caughtErr, - 1, - 'commander.excessArguments', - "error: too many arguments for 'speak'. Expected 0 arguments but got 1: excess.", + test('when specify command with excess argument then throw CommanderError', () => { + const program = createTestCommand(); + program.command('speak').action(() => {}); + + assert.throws( + () => { + program.parse(['node', 'test', 'speak', 'excess']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.excessArguments'); + assert.equal( + err.message, + "error: too many arguments for 'speak'. Expected 0 arguments but got 1: excess.", + ); + return true; + }, ); }); - test('when specify --help then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.exitOverride(); - - let caughtErr; - try { - program.parse(['node', 'test', '--help']); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 0, - 'commander.helpDisplayed', - '(outputHelp)', + test('when specify --help then throw CommanderError', () => { + const program = createTestCommand(); + + assert.throws( + () => { + program.parse(['node', 'test', '--help']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 0); + assert.equal(err.code, 'commander.helpDisplayed'); + assert.equal(err.message, '(outputHelp)'); + return true; + }, ); }); - test('when executable subcommand and no command specified then throw CommanderError', (t) => { - const stderrSpy = t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.exitOverride().command('compress', 'compress description'); - - let caughtErr; - try { - program.parse(['node', 'test']); - } catch (err) { - caughtErr = err; - } - - expectCommanderError(caughtErr, 1, 'commander.help', '(outputHelp)'); + test('when executable subcommand and no command specified then throw CommanderError', () => { + const program = createTestCommand(); + program.command('compress', 'compress description'); + + assert.throws( + () => { + program.parse(['node', 'test']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.help'); + assert.equal(err.message, '(outputHelp)'); + return true; + }, + ); }); - test('when specify --version then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when specify --version then throw CommanderError', () => { const myVersion = '1.2.3'; - const program = new commander.Command(); - program.exitOverride().version(myVersion); - - let caughtErr; - try { - program.parse(['node', 'test', '--version']); - } catch (err) { - caughtErr = err; - } - - expectCommanderError(caughtErr, 0, 'commander.version', myVersion); + const program = createTestCommand(); + program.version(myVersion); + + assert.throws( + () => { + program.parse(['node', 'test', '--version']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 0); + assert.equal(err.code, 'commander.version'); + assert.equal(err.message, myVersion); + return true; + }, + ); }); test('when executableSubcommand succeeds then call exitOverride', async () => { const pm = path.join(import.meta.dirname, 'fixtures/pm'); - const program = new commander.Command(); + const program = createTestCommand(); await new Promise((resolve) => { program .exitOverride((err) => { - expectCommanderError( - err, - 0, - 'commander.executeSubCommandAsync', - '(close)', - ); + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 0); + assert.equal(err.code, 'commander.executeSubCommandAsync'); + assert.equal(err.message, '(close)'); resolve(); }) .command('silent', 'description'); @@ -274,159 +247,162 @@ describe('Command.exitOverride', () => { }); }); - test('when mandatory program option missing then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when mandatory program option missing then throw CommanderError', () => { const optionFlags = '-p, --pepper '; - const program = new commander.Command(); - program.exitOverride().requiredOption(optionFlags, 'add pepper'); - - let caughtErr; - try { - program.parse(['node', 'test']); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 1, - 'commander.missingMandatoryOptionValue', - `error: required option '${optionFlags}' not specified`, + const program = createTestCommand(); + program.requiredOption(optionFlags, 'add pepper'); + + assert.throws( + () => { + program.parse(['node', 'test']); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.missingMandatoryOptionValue'); + assert.equal( + err.message, + `error: required option '${optionFlags}' not specified`, + ); + return true; + }, ); }); - test('when option argument not in choices then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when option argument not in choices then throw CommanderError', () => { const optionFlags = '--colour '; - const program = new commander.Command(); - program - .exitOverride() - .addOption(new commander.Option(optionFlags).choices(['red', 'blue'])); - - let caughtErr; - try { - program.parse(['--colour', 'green'], { from: 'user' }); - } catch (err) { - caughtErr = err; - } + const program = createTestCommand(); + program.addOption( + new commander.Option(optionFlags).choices(['red', 'blue']), + ); - expectCommanderError( - caughtErr, - 1, - 'commander.invalidArgument', - "error: option '--colour ' argument 'green' is invalid. Allowed choices are red, blue.", + assert.throws( + () => { + program.parse(['--colour', 'green'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.invalidArgument'); + assert.equal( + err.message, + "error: option '--colour ' argument 'green' is invalid. Allowed choices are red, blue.", + ); + return true; + }, ); }); - test('when command argument not in choices then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); + test('when command argument not in choices then throw CommanderError', () => { + const program = createTestCommand(); program - .exitOverride() .addArgument(new commander.Argument('').choices(['red', 'blue'])) .action(() => {}); - let caughtErr; - try { - program.parse(['green'], { from: 'user' }); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 1, - 'commander.invalidArgument', - "error: command-argument value 'green' is invalid for argument 'shade'. Allowed choices are red, blue.", + assert.throws( + () => { + program.parse(['green'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.invalidArgument'); + assert.equal( + err.message, + "error: command-argument value 'green' is invalid for argument 'shade'. Allowed choices are red, blue.", + ); + return true; + }, ); }); - test('when custom processing for option throws InvalidArgumentError then catch CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when custom processing for option throws InvalidArgumentError then catch CommanderError', () => { function justSayNo(value) { throw new commander.InvalidArgumentError('NO'); } const optionFlags = '--colour '; - const program = new commander.Command(); - program.exitOverride().option(optionFlags, 'specify shade', justSayNo); - - let caughtErr; - try { - program.parse(['--colour', 'green'], { from: 'user' }); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 1, - 'commander.invalidArgument', - "error: option '--colour ' argument 'green' is invalid. NO", + const program = createTestCommand(); + program.option(optionFlags, 'specify shade', justSayNo); + + assert.throws( + () => { + program.parse(['--colour', 'green'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.invalidArgument'); + assert.equal( + err.message, + "error: option '--colour ' argument 'green' is invalid. NO", + ); + return true; + }, ); }); - test('when custom processing for argument throws InvalidArgumentError then catch CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); + test('when custom processing for argument throws InvalidArgumentError then catch CommanderError', () => { function justSayNo(value) { throw new commander.InvalidArgumentError('NO'); } - const program = new commander.Command(); - program - .exitOverride() - .argument('[n]', 'number', justSayNo) - .action(() => {}); - - let caughtErr; - try { - program.parse(['green'], { from: 'user' }); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 1, - 'commander.invalidArgument', - "error: command-argument value 'green' is invalid for argument 'n'. NO", + const program = createTestCommand(); + program.argument('[n]', 'number', justSayNo).action(() => {}); + + assert.throws( + () => { + program.parse(['green'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.invalidArgument'); + assert.equal( + err.message, + "error: command-argument value 'green' is invalid for argument 'n'. NO", + ); + return true; + }, ); }); - test('when has conflicting option then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); + test('when has conflicting option then throw CommanderError', () => { + const program = createTestCommand(); program - .exitOverride() .addOption(new commander.Option('--silent')) .addOption(new commander.Option('--debug').conflicts(['silent'])); - let caughtErr; - try { - program.parse(['--debug', '--silent'], { from: 'user' }); - } catch (err) { - caughtErr = err; - } - - expectCommanderError( - caughtErr, - 1, - 'commander.conflictingOption', - "error: option '--debug' cannot be used with option '--silent'", + assert.throws( + () => { + program.parse(['--debug', '--silent'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.conflictingOption'); + assert.equal( + err.message, + "error: option '--debug' cannot be used with option '--silent'", + ); + return true; + }, ); }); - test('when call error() then throw CommanderError', (t) => { - t.mock.method(process.stderr, 'write', () => {}); - const program = new commander.Command(); - program.exitOverride(); - - let caughtErr; - try { - program.error('message'); - } catch (err) { - caughtErr = err; - } - - expectCommanderError(caughtErr, 1, 'commander.error', 'message'); + test('when call error() then throw CommanderError', () => { + const program = createTestCommand(); + + assert.throws( + () => { + program.error('message'); + }, + (err) => { + assert.ok(err instanceof commander.CommanderError); + assert.equal(err.exitCode, 1); + assert.equal(err.code, 'commander.error'); + assert.equal(err.message, 'message'); + return true; + }, + ); }); }); @@ -435,7 +411,7 @@ test('when no override and error then exit(1)', (t) => { const program = new commander.Command(); program.configureOutput({ outputError: () => {} }); program.parse(['--unknownOption'], { from: 'user' }); - assert(exitSpy.mock.callCount() >= 1); + assert.ok(exitSpy.mock.callCount() >= 1); assert.deepEqual(exitSpy.mock.calls[0].arguments, [1]); }); @@ -443,15 +419,17 @@ test('when custom processing throws custom error then throw custom error', () => function justSayNo(value) { throw new Error('custom'); } - const program = new commander.Command(); - program - .exitOverride() - .option('-s, --shade ', 'specify shade', justSayNo); + const program = createTestCommand(); + program.option('-s, --shade ', 'specify shade', justSayNo); assert.throws( () => { program.parse(['--shade', 'green'], { from: 'user' }); }, - { message: 'custom' }, + (err) => { + assert.ok(err instanceof Error); + assert.equal(err.message, 'custom'); + return true; + }, ); }); From 98c275fcaa06285ea7b2023ad5546f1c77c8063a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:45:06 +1200 Subject: [PATCH 12/18] Use assert.ok(foo) rather than assert(foo) --- tests/command.helpOption.test.js | 6 +++--- tests/command.parseOptions.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/command.helpOption.test.js b/tests/command.helpOption.test.js index f5d8a1aa4..653d7f652 100644 --- a/tests/command.helpOption.test.js +++ b/tests/command.helpOption.test.js @@ -73,7 +73,7 @@ describe('Command.helpOption()', () => { const program = new commander.Command(); program.helpOption(false); const helpInformation = program.helpInformation(); - assert(!helpInformation.includes('--help')); + assert.ok(!helpInformation.includes('--help')); }); test('when helpOption(false) then --help is an unknown option', (t) => { @@ -114,7 +114,7 @@ describe('Command.helpOption()', () => { program.helpOption(false); program.helpOption(true); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('--help')); + assert.ok(helpInformation.includes('--help')); }); test('when helpOption(true) after customise then helpInformation still customised', () => { @@ -122,6 +122,6 @@ describe('Command.helpOption()', () => { program.helpOption('--ASSIST'); program.helpOption(true); const helpInformation = program.helpInformation(); - assert(helpInformation.includes('--ASSIST')); + assert.ok(helpInformation.includes('--ASSIST')); }); }); diff --git a/tests/command.parseOptions.test.js b/tests/command.parseOptions.test.js index 54c1f397b..1ce8f98eb 100644 --- a/tests/command.parseOptions.test.js +++ b/tests/command.parseOptions.test.js @@ -26,14 +26,14 @@ describe('parsing regression tests', () => { test('when specify subcommand and argument then program.args not empty', () => { const program = createProgram1032(); program.parse(['node', 'test.js', 'doit', 'myid']); - assert(program.args.length > 0); + assert.ok(program.args.length > 0); }); // https://github.com/tj/commander.js/issues/1032 test('when specify subcommand and option and argument then program.args not empty', () => { const program = createProgram1032(); program.parse(['node', 'test.js', 'doit', '--better', 'myid']); - assert(program.args.length > 0); + assert.ok(program.args.length > 0); }); // https://github.com/tj/commander.js/issues/508 From d74dd8b5a23f49f7cfc20a44b8303329f95b43c8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:45:26 +1200 Subject: [PATCH 13/18] Just have one line of code inside assert.throws() --- tests/command.registerClash.test.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/command.registerClash.test.js b/tests/command.registerClash.test.js index 764984b8c..5ea960d64 100644 --- a/tests/command.registerClash.test.js +++ b/tests/command.registerClash.test.js @@ -4,10 +4,10 @@ import assert from 'node:assert/strict'; describe('registering clashing subcommands', () => { test('when command name conflicts with existing name then throw', () => { + const program = new Command(); + program.command('one'); assert.throws( () => { - const program = new Command(); - program.command('one'); program.command('one'); }, { message: /cannot add command/ }, @@ -15,10 +15,10 @@ describe('registering clashing subcommands', () => { }); test('when command name conflicts with existing alias then throw', () => { + const program = new Command(); + program.command('one').alias('1'); assert.throws( () => { - const program = new Command(); - program.command('one').alias('1'); program.command('1'); }, { message: /cannot add command/ }, @@ -26,10 +26,10 @@ describe('registering clashing subcommands', () => { }); test('when command alias conflicts with existing name then throw', () => { + const program = new Command(); + program.command('one'); assert.throws( () => { - const program = new Command(); - program.command('one'); program.command('1').alias('one'); }, { message: /cannot add alias/ }, @@ -37,10 +37,10 @@ describe('registering clashing subcommands', () => { }); test('when command alias conflicts with existing alias then throw', () => { + const program = new Command(); + program.command('one').alias('1'); assert.throws( () => { - const program = new Command(); - program.command('one').alias('1'); program.command('unity').alias('1'); }, { message: /cannot add alias/ }, @@ -48,10 +48,10 @@ describe('registering clashing subcommands', () => { }); test('when .addCommand name conflicts with existing name then throw', () => { + const program = new Command(); + program.command('one'); assert.throws( () => { - const program = new Command(); - program.command('one'); program.addCommand(new Command('one')); }, { message: /cannot add command/ }, @@ -59,10 +59,10 @@ describe('registering clashing subcommands', () => { }); test('when .addCommand alias conflicts with existing name then throw', () => { + const program = new Command(); + program.command('one'); assert.throws( () => { - const program = new Command(); - program.command('one'); program.addCommand(new Command('unity').alias('one')); }, { message: /cannot add command/ }, From d1fa7add9f885c456688dcb9b8aee828201111d3 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:45:57 +1200 Subject: [PATCH 14/18] Use local assert context for mocks --- tests/command.showHelpAfterError.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/command.showHelpAfterError.test.js b/tests/command.showHelpAfterError.test.js index 6f591c712..b785604eb 100644 --- a/tests/command.showHelpAfterError.test.js +++ b/tests/command.showHelpAfterError.test.js @@ -121,8 +121,8 @@ describe('Command.showHelpAfterError(message)', () => { }); }); -test('when Command.showHelpAfterError() and error and then shows full help', () => { - const writeMock = mock.fn(); +test('when Command.showHelpAfterError() and error and then shows full help', (t) => { + const writeMock = t.mock.fn(); const program = new commander.Command(); program .exitOverride() From 8b51112f74591f20a065b48c638f350135a25fa9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 14:46:37 +1200 Subject: [PATCH 15/18] Switch note-test lint to use recommended --- eslint.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 81e181474..f6d8d5677 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -71,9 +71,12 @@ export default defineConfig( plugins: { 'node-test': nodeTest, }, - extends: ['node-test/unopinionated'], // switch to recommended if like how unopinionated goes + extends: ['node-test/recommended'], rules: { 'node-test/no-constant-assertion': 'off', // getting false positives, opened issue, disable for now + 'node-test/prefer-test-context-assert': 'off', // use callback parameter t to access mock, but do not want to change assert + 'node-test/no-useless-assertion': 'off', // use `assert.doesNotThrow()` as only assert in multiple tests (and get another error if no asserts) + 'node-test/no-process-env-mutation': 'off', // going to revisit... }, }, ); From 67d7c8a41cfb21aa20c96a78797faa68da862d8c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 25 Jul 2026 15:20:56 +1200 Subject: [PATCH 16/18] Turn off lint on env in tests --- eslint.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index f6d8d5677..b2dd17dfd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -74,9 +74,9 @@ export default defineConfig( extends: ['node-test/recommended'], rules: { 'node-test/no-constant-assertion': 'off', // getting false positives, opened issue, disable for now - 'node-test/prefer-test-context-assert': 'off', // use callback parameter t to access mock, but do not want to change assert - 'node-test/no-useless-assertion': 'off', // use `assert.doesNotThrow()` as only assert in multiple tests (and get another error if no asserts) - 'node-test/no-process-env-mutation': 'off', // going to revisit... + 'node-test/prefer-test-context-assert': 'off', // we use callback parameter t to access mock, but do not want to change assert (as not automatically strict) + 'node-test/no-useless-assertion': 'off', // we use `assert.doesNotThrow()` as only assert in multiple tests (and get another error if no asserts) + 'node-test/no-process-env-mutation': 'off', // we do our own management which node-test does not recognise }, }, ); From bf0ea6d887575a0180e7b9ffa41fd3e9b66405be Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 26 Jul 2026 11:07:21 +1200 Subject: [PATCH 17/18] Update eslint-node-test and reenable no-constant-assertion --- eslint.config.js | 8 ++++---- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b2dd17dfd..92d37a4a6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -73,10 +73,10 @@ export default defineConfig( }, extends: ['node-test/recommended'], rules: { - 'node-test/no-constant-assertion': 'off', // getting false positives, opened issue, disable for now - 'node-test/prefer-test-context-assert': 'off', // we use callback parameter t to access mock, but do not want to change assert (as not automatically strict) - 'node-test/no-useless-assertion': 'off', // we use `assert.doesNotThrow()` as only assert in multiple tests (and get another error if no asserts) - 'node-test/no-process-env-mutation': 'off', // we do our own management which node-test does not recognise + 'node-test/prefer-test-context-assert': 'off', // we use callback parameter t to generate mocks, but do not want to use t.assertX (as t.assertX not automatically strict) + 'node-test/no-useless-assertion': 'off', // we use `assert.doesNotThrow()` as only assert in multiple tests (so removing that assert triggers a different lint error) + 'node-test/no-process-env-mutation': 'off', // we manage env in ways node-test does not recognise + // 'node-test/require-top-level-describe': 'on', }, }, ); diff --git a/package-lock.json b/package-lock.json index 00025628a..fc5c6f8bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@types/node": "^22.7.4", "eslint": "^10.0.2", "eslint-config-prettier": "^10.0.1", - "eslint-node-test": "^0.3.0", + "eslint-node-test": "^0.4.0", "globals": "^17.3.0", "prettier": "^3.2.5", "tsd": "^0.33.0", @@ -1039,9 +1039,9 @@ } }, "node_modules/eslint-node-test": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-node-test/-/eslint-node-test-0.3.0.tgz", - "integrity": "sha512-aV1YX7ydDH7VCDsBWNNuicISdujCaHiprs+vYSdechPAAS3HWR00HTHgDYTSnMqTlLJOnkvgi1jV1cB1TNV56Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/eslint-node-test/-/eslint-node-test-0.4.0.tgz", + "integrity": "sha512-PnHvPZASsqWFoRlyivgQa4VFcA5o8psqp6SWrDG2e0MJBcq7ajRDV2H7oqiWOa3qmFLvCm2cZ/+hv+y455ZgEQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 63bf1fcdc..2e33cf127 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/node": "^22.7.4", "eslint": "^10.0.2", "eslint-config-prettier": "^10.0.1", - "eslint-node-test": "^0.3.0", + "eslint-node-test": "^0.4.0", "globals": "^17.3.0", "prettier": "^3.2.5", "tsd": "^0.33.0", From 6d98ccb865f3aad09712fa31b8884f129ca27247 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 26 Jul 2026 11:30:08 +1200 Subject: [PATCH 18/18] Turn on node-test/require-top-level-describe and fix resulting lint errors --- eslint.config.js | 2 +- tests/command.commandHelp.test.js | 26 +++++----- ...mmand.executableSubcommand.signals.test.js | 42 ++++++++------- tests/command.executableSubcommand.test.js | 3 +- tests/command.exitOverride.test.js | 52 +++++++++---------- tests/command.nested.test.js | 1 + tests/command.showHelpAfterError.test.js | 30 +++++------ tests/command.summary.test.js | 1 + tests/help.preformatted.test.js | 22 ++++---- tests/options.bool.small.combined.test.js | 1 + 10 files changed, 94 insertions(+), 86 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 92d37a4a6..c824fb045 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -76,7 +76,7 @@ export default defineConfig( 'node-test/prefer-test-context-assert': 'off', // we use callback parameter t to generate mocks, but do not want to use t.assertX (as t.assertX not automatically strict) 'node-test/no-useless-assertion': 'off', // we use `assert.doesNotThrow()` as only assert in multiple tests (so removing that assert triggers a different lint error) 'node-test/no-process-env-mutation': 'off', // we manage env in ways node-test does not recognise - // 'node-test/require-top-level-describe': 'on', + 'node-test/require-top-level-describe': 'error', // Enforce top-level describe for providing context in test output. Disable by hand on single test files. }, }, ); diff --git a/tests/command.commandHelp.test.js b/tests/command.commandHelp.test.js index eadc6c912..7cff96bd9 100644 --- a/tests/command.commandHelp.test.js +++ b/tests/command.commandHelp.test.js @@ -1,19 +1,21 @@ import * as commander from '../index.js'; -import { test } from 'node:test'; +import { describe, test } from 'node:test'; import assert from 'node:assert/strict'; // This is a ported legacy test. -test('when program has command then appears in help', () => { - const program = new commander.Command(); - program.command('bare'); - const commandHelp = program.helpInformation(); - assert.match(commandHelp, /Commands:\n +bare\n/); -}); +describe('simple legacy help tests', () => { + test('when program has command then appears in help', () => { + const program = new commander.Command(); + program.command('bare'); + const commandHelp = program.helpInformation(); + assert.match(commandHelp, /Commands:\n +bare\n/); + }); -test('when program has command with optional arg then appears in help', () => { - const program = new commander.Command(); - program.command('bare [bare-arg]'); - const commandHelp = program.helpInformation(); - assert.match(commandHelp, /Commands:\n +bare \[bare-arg\]\n/); + test('when program has command with optional arg then appears in help', () => { + const program = new commander.Command(); + program.command('bare [bare-arg]'); + const commandHelp = program.helpInformation(); + assert.match(commandHelp, /Commands:\n +bare \[bare-arg\]\n/); + }); }); diff --git a/tests/command.executableSubcommand.signals.test.js b/tests/command.executableSubcommand.signals.test.js index 1d0508ffb..309febd66 100644 --- a/tests/command.executableSubcommand.signals.test.js +++ b/tests/command.executableSubcommand.signals.test.js @@ -36,27 +36,29 @@ describeOrSkipOnWindows('executable subcommand signals', () => { } }); - test('when executable subcommand sent signal then program exit code is non-zero', () => { - const { status } = childProcess.spawnSync(pmPath, ['terminate'], {}); - assert.equal(status > 0, true); - }); + describe('signal exit codes', () => { + test('when executable subcommand sent signal then program exit code is non-zero', () => { + const { status } = childProcess.spawnSync(pmPath, ['terminate'], {}); + assert.equal(status > 0, true); + }); - test('when command has exitOverride and executable subcommand sent signal then exit code is non-zero', () => { - const { status } = childProcess.spawnSync( - pmPath, - ['exit-override', 'terminate'], - {}, - ); - assert.equal(status > 0, true); - }); + test('when command has exitOverride and executable subcommand sent signal then exit code is non-zero', () => { + const { status } = childProcess.spawnSync( + pmPath, + ['exit-override', 'terminate'], + {}, + ); + assert.equal(status > 0, true); + }); - // Not a signal test, but closely related code so adding here. - test('when command has exitOverride and executable subcommand fails then program exit code is subcommand exit code', () => { - const { status } = childProcess.spawnSync( - pmPath, - ['exit-override', 'fail'], - {}, - ); - assert.equal(status, 42); + // Not a signal test, but closely related code so adding here. + test('when command has exitOverride and executable subcommand fails then program exit code is subcommand exit code', () => { + const { status } = childProcess.spawnSync( + pmPath, + ['exit-override', 'fail'], + {}, + ); + assert.equal(status, 42); + }); }); }); diff --git a/tests/command.executableSubcommand.test.js b/tests/command.executableSubcommand.test.js index 30bde7318..23a45373a 100644 --- a/tests/command.executableSubcommand.test.js +++ b/tests/command.executableSubcommand.test.js @@ -1,10 +1,11 @@ import { createTestCommand } from './testHelpers.js'; -import { test } from 'node:test'; +import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; // Executable subcommand tests that didn't fit in elsewhere. // This is the default behaviour when no default command and no action handlers +// eslint-disable-next-line node-test/require-top-level-describe test('when no command specified and executable subcommand then display help', (t) => { const program = createTestCommand(); program.command('install', 'install description'); diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 6894652fd..3969d12dd 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -404,32 +404,32 @@ describe('Command.exitOverride', () => { }, ); }); -}); -test('when no override and error then exit(1)', (t) => { - const exitSpy = t.mock.method(process, 'exit', () => {}); - const program = new commander.Command(); - program.configureOutput({ outputError: () => {} }); - program.parse(['--unknownOption'], { from: 'user' }); - assert.ok(exitSpy.mock.callCount() >= 1); - assert.deepEqual(exitSpy.mock.calls[0].arguments, [1]); -}); + test('when no override and error then exit(1)', (t) => { + const exitSpy = t.mock.method(process, 'exit', () => {}); + const program = new commander.Command(); + program.configureOutput({ outputError: () => {} }); + program.parse(['--unknownOption'], { from: 'user' }); + assert.ok(exitSpy.mock.callCount() >= 1); + assert.deepEqual(exitSpy.mock.calls[0].arguments, [1]); + }); + + test('when custom processing throws custom error then throw custom error', () => { + function justSayNo(value) { + throw new Error('custom'); + } + const program = createTestCommand(); + program.option('-s, --shade ', 'specify shade', justSayNo); -test('when custom processing throws custom error then throw custom error', () => { - function justSayNo(value) { - throw new Error('custom'); - } - const program = createTestCommand(); - program.option('-s, --shade ', 'specify shade', justSayNo); - - assert.throws( - () => { - program.parse(['--shade', 'green'], { from: 'user' }); - }, - (err) => { - assert.ok(err instanceof Error); - assert.equal(err.message, 'custom'); - return true; - }, - ); + assert.throws( + () => { + program.parse(['--shade', 'green'], { from: 'user' }); + }, + (err) => { + assert.ok(err instanceof Error); + assert.equal(err.message, 'custom'); + return true; + }, + ); + }); }); diff --git a/tests/command.nested.test.js b/tests/command.nested.test.js index 7ffd75302..056df618a 100644 --- a/tests/command.nested.test.js +++ b/tests/command.nested.test.js @@ -2,6 +2,7 @@ import * as commander from '../index.js'; import { test } from 'node:test'; import assert from 'node:assert/strict'; +// eslint-disable-next-line node-test/require-top-level-describe test('when call nested subcommand then runs', (t) => { const program = new commander.Command(); const leafAction = t.mock.fn(); diff --git a/tests/command.showHelpAfterError.test.js b/tests/command.showHelpAfterError.test.js index b785604eb..fcbfaa6ed 100644 --- a/tests/command.showHelpAfterError.test.js +++ b/tests/command.showHelpAfterError.test.js @@ -119,21 +119,21 @@ describe('Command.showHelpAfterError(message)', () => { const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; assert.equal(lastCall.arguments[0], `${customHelpMessage}\n`); }); -}); -test('when Command.showHelpAfterError() and error and then shows full help', (t) => { - const writeMock = t.mock.fn(); - const program = new commander.Command(); - program - .exitOverride() - .showHelpAfterError() - .configureOutput({ writeErr: writeMock }); + test('when Command.showHelpAfterError() and error and then shows full help', (t) => { + const writeMock = t.mock.fn(); + const program = new commander.Command(); + program + .exitOverride() + .showHelpAfterError() + .configureOutput({ writeErr: writeMock }); - try { - program.parse(['--unknown-option'], { from: 'user' }); - } catch (err) { - /* empty */ - } - const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; - assert.equal(lastCall.arguments[0], program.helpInformation()); + try { + program.parse(['--unknown-option'], { from: 'user' }); + } catch (err) { + /* empty */ + } + const lastCall = writeMock.mock.calls[writeMock.mock.callCount() - 1]; + assert.equal(lastCall.arguments[0], program.helpInformation()); + }); }); diff --git a/tests/command.summary.test.js b/tests/command.summary.test.js index 170f55fb4..188f19b19 100644 --- a/tests/command.summary.test.js +++ b/tests/command.summary.test.js @@ -2,6 +2,7 @@ import * as commander from '../index.js'; import { test } from 'node:test'; import assert from 'node:assert/strict'; +// eslint-disable-next-line node-test/require-top-level-describe test('Command.summary(): when set summary then get summary', () => { const program = new commander.Command(); const summary = 'abcdef'; diff --git a/tests/help.preformatted.test.js b/tests/help.preformatted.test.js index 19377cc31..db1e0722a 100644 --- a/tests/help.preformatted.test.js +++ b/tests/help.preformatted.test.js @@ -42,22 +42,21 @@ describe('Help.preformatted()', () => { const helper = new Help(); assert.equal(helper.preformatted('a\r\n\r\n'), false); }); -}); -test('end-to-end: when option description is preformatted then manual format is preserved', () => { - // #396: leave custom format alone, apart from space-space indent - const optionSpec = '-t, --time '; - const program = new Command(); - program.configureHelp({ helpWidth: 80 }).option( - optionSpec, - `select time + test('end-to-end: when option description is preformatted then manual format is preserved', () => { + // #396: leave custom format alone, apart from space-space indent + const optionSpec = '-t, --time '; + const program = new Command(); + program.configureHelp({ helpWidth: 80 }).option( + optionSpec, + `select time Time can also be specified using special values: "dawn" - From night to sunrise. `, - ); + ); - const expectedOutput = `Usage: [options] + const expectedOutput = `Usage: [options] Options: ${optionSpec} select time @@ -68,5 +67,6 @@ Options: -h, --help display help for command `; - assert.equal(program.helpInformation(), expectedOutput); + assert.equal(program.helpInformation(), expectedOutput); + }); }); diff --git a/tests/options.bool.small.combined.test.js b/tests/options.bool.small.combined.test.js index 6d0276607..88cdb5907 100644 --- a/tests/options.bool.small.combined.test.js +++ b/tests/options.bool.small.combined.test.js @@ -2,6 +2,7 @@ import * as commander from '../index.js'; import { test } from 'node:test'; import assert from 'node:assert/strict'; +// eslint-disable-next-line node-test/require-top-level-describe test('when when multiple short flags specified then parsed as short option group', () => { const program = new commander.Command(); program