From b14321c87f988f42a33a6ea58d144c2a397cad6a Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi Date: Wed, 15 Jul 2026 14:06:09 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E5=AD=98=E5=9C=A8=E3=81=97=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=82=B5=E3=83=96=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=B8=E3=81=AE=20--help=20=E3=82=92=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E6=89=B1=E3=81=84=E3=81=AB=E3=81=97=E3=81=A6=20exit?= =?UTF-8?q?=201=20=E3=82=92=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commander は --help/-h を検出すると、残りの operand が既知のサブコマンドか 検証せずにヘルプを表示して exit 0 していた (例: geonic hello --help)。 サブコマンドを持つコマンドの outputHelp をラップし、未解決の operand が 残っている場合は 'geonic help ' と同じエラー形式で exit 1 にする。 --- src/cli.ts | 4 +++- src/commands/help.ts | 38 ++++++++++++++++++++++++++++++- tests/help.test.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 7951b71..2c9caa7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { registerRulesCommand } from "./commands/rules.js"; import { registerModelsCommand } from "./commands/models.js"; import { registerCatalogCommand } from "./commands/catalog.js"; import { registerHealthCommand, registerVersionCommand } from "./commands/health.js"; -import { registerHelpCommand } from "./commands/help.js"; +import { enforceKnownCommandHelp, registerHelpCommand } from "./commands/help.js"; import { registerCliCommand } from "./commands/cli.js"; import { addAttrsSubcommands } from "./commands/attrs.js"; @@ -62,5 +62,7 @@ export function createProgram(): Command { addAttrsSubcommands(hiddenAttrs); program.addCommand(hiddenAttrs, { hidden: true }); + enforceKnownCommandHelp(program); + return program; } diff --git a/src/commands/help.ts b/src/commands/help.ts index a5c7f23..8cc40d4 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import type { Command, Help, Option } from "commander"; +import type { Command, Help, HelpContext, Option } from "commander"; interface Example { description: string; @@ -249,6 +249,42 @@ function showHelp(program: Command, args: string[]): void { console.log(formatCommandDetails(program, target, path)); } +function reportUnknownCommand(cmd: Command, operand: string): never { + // getCommandPath() starts with the program name ("geonic"); drop it + const prefix = getCommandPath(cmd).split(" ").slice(1); + const attempted = [...prefix, operand].join(" "); + console.error(chalk.red(`Error: '${attempted}' is not a geonic command.`)); + console.error(`\nSee 'geonic help' for available commands.`); + process.exit(1); +} + +export function enforceKnownCommandHelp(program: Command): void { + // Commander prints help and exits 0 when --help/-h is present, even if the + // remaining operand is not a known subcommand (e.g. `geonic hello --help`). + // Wrap outputHelp() on every command group so that case errors with exit 1, + // matching `geonic help `. Call after all commands are registered. + const visit = (cmd: Command): void => { + if (cmd.commands.length > 0) { + const originalOutputHelp = cmd.outputHelp.bind(cmd); + // Same parameter type as Command.outputHelp (including deprecated overload) + cmd.outputHelp = ( + contextOptions?: HelpContext | ((str: string) => string), + ): void => { + // cmd.args is operands followed by unparsed options such as --help + const operand = cmd.args.find((arg) => !arg.startsWith("-")); + if (operand !== undefined && !findCommand(cmd, operand)) { + reportUnknownCommand(cmd, operand); + } + originalOutputHelp(contextOptions as HelpContext | undefined); + }; + } + for (const sub of cmd.commands) { + visit(sub); + } + }; + visit(program); +} + export function registerHelpCommand(program: Command): void { // Disable Commander's built-in help command program.addHelpCommand(false); diff --git a/tests/help.test.ts b/tests/help.test.ts index 714a718..1791089 100644 --- a/tests/help.test.ts +++ b/tests/help.test.ts @@ -683,6 +683,60 @@ describe("help", () => { }); }); + describe("unknown subcommand with --help", () => { + async function runExpectingError(argv: string[]): Promise { + const prog = createProgram(); + prog.exitOverride(); + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + try { + await prog.parseAsync(["node", "geonic", ...argv]); + } catch { + // expected + } + const output = stripAnsi(errSpy.mock.calls.map((c) => c[0]).join("\n")); + expect(exitSpy).toHaveBeenCalledWith(1); + errSpy.mockRestore(); + exitSpy.mockRestore(); + return output; + } + + it("errors for unknown top-level command with --help", async () => { + const output = await runExpectingError(["hello", "--help"]); + expect(output).toContain("'hello' is not a geonic command"); + expect(output).toContain("geonic help"); + }); + + it("errors for unknown top-level command with -h", async () => { + const output = await runExpectingError(["hello", "-h"]); + expect(output).toContain("'hello' is not a geonic command"); + }); + + it("errors for unknown nested subcommand with --help", async () => { + const output = await runExpectingError(["entities", "hello", "--help"]); + expect(output).toContain("'entities hello' is not a geonic command"); + }); + + it("still shows help for a leaf command with an argument and --help", async () => { + const prog = createProgram(); + prog.exitOverride(); + const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + await prog.parseAsync(["node", "geonic", "entities", "get", "urn:x", "--help"]); + } catch { + // exitOverride throws on help + } + const output = stripAnsi(writeSpy.mock.calls.map((c) => c[0]).join("")); + expect(output).toContain("geonic entities get"); + expect(errSpy).not.toHaveBeenCalled(); + writeSpy.mockRestore(); + errSpy.mockRestore(); + }); + }); + describe("no-args handler", () => { it("shows help when geonic is run with no arguments", async () => { const prog = createProgram(); From 0ed82fb04f34b8ba5d2e297e593579501cc46c6d Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi Date: Wed, 15 Jul 2026 14:51:47 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20--help=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=AE=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - operand 判定を cmd.args 先頭トークンに限定し、未知オプションの値を サブコマンド名と誤認しないようにする (--bogus json --help 等)。 副作用として --help が先行する場合 (geonic --help hello) は 従来どおりヘルプ表示に戻る - エラーメッセージはユーザーが入力したエイリアス (models, batch) を そのまま echo し、正規名に置換しない (rawArgs から復元) - unknown-command エラー出力を unknownCommandError() に統合し、 showHelp との重複を解消 - テストの spy ボイラープレートを runExpectingError/runExpectingHelp に 集約し、既存テストも移行 --- src/commands/help.ts | 59 +++++++++++++++++++++--------- tests/help.test.ts | 85 +++++++++++++++++++++++++------------------- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/src/commands/help.ts b/src/commands/help.ts index 8cc40d4..a2b9aec 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -220,6 +220,12 @@ export function formatCommandDetails( return lines.join("\n"); } +function unknownCommandError(attempted: string): never { + console.error(chalk.red(`Error: '${attempted}' is not a geonic command.`)); + console.error(`\nSee 'geonic help' for available commands.`); + process.exit(1); +} + function showHelp(program: Command, args: string[]): void { if (args.length === 0) { console.log(formatTopLevelHelp(program)); @@ -232,12 +238,7 @@ function showHelp(program: Command, args: string[]): void { for (let i = 0; i < args.length; i++) { const found = findCommand(current, args[i]); if (!found) { - const attempted = args.slice(0, i + 1).join(" "); - console.error( - chalk.red(`Error: '${attempted}' is not a geonic command.`), - ); - console.error(`\nSee 'geonic help' for available commands.`); - process.exit(1); + unknownCommandError(args.slice(0, i + 1).join(" ")); } resolved.push(found); current = found; @@ -249,13 +250,31 @@ function showHelp(program: Command, args: string[]): void { console.log(formatCommandDetails(program, target, path)); } -function reportUnknownCommand(cmd: Command, operand: string): never { - // getCommandPath() starts with the program name ("geonic"); drop it - const prefix = getCommandPath(cmd).split(" ").slice(1); - const attempted = [...prefix, operand].join(" "); - console.error(chalk.red(`Error: '${attempted}' is not a geonic command.`)); - console.error(`\nSee 'geonic help' for available commands.`); - process.exit(1); +// Resolve the chain from the program down to cmd back to the tokens the user +// actually typed (rawArgs), so aliases are echoed as typed (e.g. "models"), +// not as their canonical name ("custom-data-models"). +function typedCommandPath(cmd: Command): string[] { + const chain: Command[] = []; + let current: Command | null = cmd; + while (current && current.parent) { + chain.unshift(current); + current = current.parent; + } + + // rawArgs exists at runtime but is missing from commander's typings + const root = getRootProgram(cmd) as Command & { rawArgs: string[] }; + const rawArgs = root.rawArgs.slice(2); // drop node + script + + const typed: string[] = []; + let i = 0; + for (const c of chain) { + const names = [c.name(), ...c.aliases()]; + while (i < rawArgs.length && !names.includes(rawArgs[i])) { + i++; + } + typed.push(i < rawArgs.length ? rawArgs[i] : c.name()); + } + return typed; } export function enforceKnownCommandHelp(program: Command): void { @@ -270,10 +289,16 @@ export function enforceKnownCommandHelp(program: Command): void { cmd.outputHelp = ( contextOptions?: HelpContext | ((str: string) => string), ): void => { - // cmd.args is operands followed by unparsed options such as --help - const operand = cmd.args.find((arg) => !arg.startsWith("-")); - if (operand !== undefined && !findCommand(cmd, operand)) { - reportUnknownCommand(cmd, operand); + // cmd.args lists operands first, then unparsed options such as --help. + // Only a leading non-dash token is a genuine operand — a non-dash token + // found later may be the value of an unrecognized option. + const operand = cmd.args[0]; + if ( + operand !== undefined && + !operand.startsWith("-") && + !findCommand(cmd, operand) + ) { + unknownCommandError([...typedCommandPath(cmd), operand].join(" ")); } originalOutputHelp(contextOptions as HelpContext | undefined); }; diff --git a/tests/help.test.ts b/tests/help.test.ts index 1791089..97cb213 100644 --- a/tests/help.test.ts +++ b/tests/help.test.ts @@ -16,6 +16,27 @@ function findCommand(program: ReturnType, ...names: string return current; } +// Parses argv expecting the unknown-command error path: captures stderr, +// asserts process.exit(1), and returns the stripped stderr output. +async function runExpectingError(argv: string[]): Promise { + const prog = createProgram(); + prog.exitOverride(); + const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + try { + await prog.parseAsync(["node", "geonic", ...argv]); + } catch { + // expected + } + const output = stripAnsi(errSpy.mock.calls.map((c) => c[0]).join("\n")); + expect(exitSpy).toHaveBeenCalledWith(1); + errSpy.mockRestore(); + exitSpy.mockRestore(); + return output; +} + describe("help", () => { const program = createProgram(); @@ -609,21 +630,8 @@ describe("help", () => { }); it("shows error for invalid command name in help", async () => { - const prog = createProgram(); - prog.exitOverride(); - const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit"); - }); - try { - await prog.parseAsync(["node", "geonic", "help", "nonexistent"]); - } catch { - // expected - } - const output = errSpy.mock.calls.map((c) => c[0]).join("\n"); - expect(stripAnsi(output)).toContain("'nonexistent' is not a geonic command"); - errSpy.mockRestore(); - exitSpy.mockRestore(); + const output = await runExpectingError(["help", "nonexistent"]); + expect(output).toContain("'nonexistent' is not a geonic command"); }); it("shows top-level help when help is called with no args", async () => { @@ -684,22 +692,22 @@ describe("help", () => { }); describe("unknown subcommand with --help", () => { - async function runExpectingError(argv: string[]): Promise { + // Parses argv expecting help to be shown: captures stdout and returns it, + // asserting no error was printed. + async function runExpectingHelp(argv: string[]): Promise { const prog = createProgram(); prog.exitOverride(); + const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit"); - }); try { await prog.parseAsync(["node", "geonic", ...argv]); } catch { - // expected + // exitOverride throws on help } - const output = stripAnsi(errSpy.mock.calls.map((c) => c[0]).join("\n")); - expect(exitSpy).toHaveBeenCalledWith(1); + const output = stripAnsi(writeSpy.mock.calls.map((c) => c[0]).join("")); + expect(errSpy).not.toHaveBeenCalled(); + writeSpy.mockRestore(); errSpy.mockRestore(); - exitSpy.mockRestore(); return output; } @@ -720,20 +728,25 @@ describe("help", () => { }); it("still shows help for a leaf command with an argument and --help", async () => { - const prog = createProgram(); - prog.exitOverride(); - const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); - const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - try { - await prog.parseAsync(["node", "geonic", "entities", "get", "urn:x", "--help"]); - } catch { - // exitOverride throws on help - } - const output = stripAnsi(writeSpy.mock.calls.map((c) => c[0]).join("")); + const output = await runExpectingHelp(["entities", "get", "urn:x", "--help"]); expect(output).toContain("geonic entities get"); - expect(errSpy).not.toHaveBeenCalled(); - writeSpy.mockRestore(); - errSpy.mockRestore(); + }); + + it("echoes the alias the user typed, not the canonical command name", async () => { + const output = await runExpectingError(["models", "badsub", "--help"]); + expect(output).toContain("'models badsub' is not a geonic command"); + expect(output).not.toContain("custom-data-models"); + }); + + it("does not mistake an unknown option's value for a subcommand", async () => { + // "--bogus json" must not be reported as `geonic entities json` + const output = await runExpectingHelp(["entities", "--bogus", "json", "--help"]); + expect(output).toContain("geonic entities"); + }); + + it("shows top-level help when --help precedes a non-command token", async () => { + const output = await runExpectingHelp(["--help", "hello"]); + expect(output).toContain("AVAILABLE COMMANDS"); }); }); From 6b8eabb7a63e1f7a5e5bd1d4311778d98c4b00f4 Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi Date: Wed, 15 Jul 2026 15:22:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20CHANGELOG=20=E3=81=AB=20#147=20?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=88=E3=83=AA=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5bf71..58a7beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ## [Unreleased] +### 2026-07-15 +- **Fix**: 存在しないサブコマンドへの `--help` (`geonic hello --help` 等) がヘルプ表示 + exit 0 で成功扱いになっていた問題を修正 — `geonic help ` と同じエラーを stderr に出力し exit 1 を返す (#147) + - エラーメッセージはユーザーが入力したエイリアス表記をそのまま echo する(`models badsub` を `custom-data-models badsub` に置換しない) + - 未知オプションの値はサブコマンド名と誤認しない(`geonic entities --bogus json --help` は従来通りヘルプ表示) + ## [0.18.0] - 2026-07-15 ### 2026-07-15 From 4a98dbd48fcc22b523a35e73a80c44970d0c7838 Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi Date: Wed, 15 Jul 2026 15:49:00 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20typed=20=E3=83=88=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E5=BE=A9=E5=85=83=E3=82=92=20rawArgs=20?= =?UTF-8?q?=E3=82=B9=E3=82=AD=E3=83=A3=E3=83=B3=E3=81=8B=E3=82=89=20preSub?= =?UTF-8?q?command=20=E3=83=95=E3=83=83=E3=82=AF=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E6=8F=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rawArgs の文字列スキャンは、グローバルオプション (--service 等) の値が コマンド名/エイリアスと衝突すると誤ったトークンを echo していた (例: geonic --service custom-data-models models badsub --help が 'custom-data-models badsub' と表示)。 preSubcommand フックで dispatch 時に commander 自身が解決した operand (parent.args[0]) を WeakMap に記録する方式へ変更。オプション値は parseOptions で消費済みのため衝突しない。あわせて: - 空文字 operand を unknown 扱いしない (truthiness ガード) - 回帰テスト 3 件追加 (オプション値衝突 / サブグループで --help 先行 / 空文字 operand) --- src/commands/help.ts | 64 +++++++++++++++++++++----------------------- tests/help.test.ts | 19 +++++++++++++ 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/commands/help.ts b/src/commands/help.ts index a2b9aec..f3198c1 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -250,54 +250,50 @@ function showHelp(program: Command, args: string[]): void { console.log(formatCommandDetails(program, target, path)); } -// Resolve the chain from the program down to cmd back to the tokens the user -// actually typed (rawArgs), so aliases are echoed as typed (e.g. "models"), -// not as their canonical name ("custom-data-models"). -function typedCommandPath(cmd: Command): string[] { - const chain: Command[] = []; - let current: Command | null = cmd; - while (current && current.parent) { - chain.unshift(current); - current = current.parent; - } - - // rawArgs exists at runtime but is missing from commander's typings - const root = getRootProgram(cmd) as Command & { rawArgs: string[] }; - const rawArgs = root.rawArgs.slice(2); // drop node + script - - const typed: string[] = []; - let i = 0; - for (const c of chain) { - const names = [c.name(), ...c.aliases()]; - while (i < rawArgs.length && !names.includes(rawArgs[i])) { - i++; - } - typed.push(i < rawArgs.length ? rawArgs[i] : c.name()); - } - return typed; -} - export function enforceKnownCommandHelp(program: Command): void { // Commander prints help and exits 0 when --help/-h is present, even if the // remaining operand is not a known subcommand (e.g. `geonic hello --help`). // Wrap outputHelp() on every command group so that case errors with exit 1, // matching `geonic help `. Call after all commands are registered. + + // Token the user actually typed to invoke each command (name or alias), + // recorded at dispatch time so error messages echo e.g. "models" as typed, + // not the canonical name "custom-data-models". + const typedNames = new WeakMap(); + + // Path from the program down to cmd, using the recorded typed tokens. + const typedCommandPath = (cmd: Command): string[] => { + const path: string[] = []; + let current: Command | null = cmd; + while (current && current.parent) { + path.unshift(typedNames.get(current) ?? current.name()); + current = current.parent; + } + return path; + }; + const visit = (cmd: Command): void => { if (cmd.commands.length > 0) { + // preSubcommand fires on dispatch, before commander's --help + // short-circuit; parent.args[0] is the operand commander resolved the + // subcommand from. Hooks are not inherited, so attach per group. + cmd.hook("preSubcommand", (parent, subCommand) => { + const token = parent.args[0]; + if (token !== undefined) { + typedNames.set(subCommand, token); + } + }); + const originalOutputHelp = cmd.outputHelp.bind(cmd); // Same parameter type as Command.outputHelp (including deprecated overload) cmd.outputHelp = ( contextOptions?: HelpContext | ((str: string) => string), ): void => { // cmd.args lists operands first, then unparsed options such as --help. - // Only a leading non-dash token is a genuine operand — a non-dash token - // found later may be the value of an unrecognized option. + // Only a leading non-empty, non-dash token is a genuine operand — a + // non-dash token found later may be the value of an unrecognized option. const operand = cmd.args[0]; - if ( - operand !== undefined && - !operand.startsWith("-") && - !findCommand(cmd, operand) - ) { + if (operand && !operand.startsWith("-") && !findCommand(cmd, operand)) { unknownCommandError([...typedCommandPath(cmd), operand].join(" ")); } originalOutputHelp(contextOptions as HelpContext | undefined); diff --git a/tests/help.test.ts b/tests/help.test.ts index 97cb213..366cdbd 100644 --- a/tests/help.test.ts +++ b/tests/help.test.ts @@ -738,6 +738,15 @@ describe("help", () => { expect(output).not.toContain("custom-data-models"); }); + it("echoes the typed alias even when an option value collides with a command name", async () => { + // The --service value must not be mistaken for the typed command token + const output = await runExpectingError([ + "--service", "custom-data-models", "models", "badsub", "--help", + ]); + expect(output).toContain("'models badsub' is not a geonic command"); + expect(output).not.toContain("custom-data-models badsub"); + }); + it("does not mistake an unknown option's value for a subcommand", async () => { // "--bogus json" must not be reported as `geonic entities json` const output = await runExpectingHelp(["entities", "--bogus", "json", "--help"]); @@ -748,6 +757,16 @@ describe("help", () => { const output = await runExpectingHelp(["--help", "hello"]); expect(output).toContain("AVAILABLE COMMANDS"); }); + + it("shows group help when --help precedes a non-command token in a subgroup", async () => { + const output = await runExpectingHelp(["entities", "--help", "badsub"]); + expect(output).toContain("geonic entities"); + }); + + it("does not report an empty-string operand as an unknown command", async () => { + const output = await runExpectingHelp(["entities", "", "--help"]); + expect(output).toContain("geonic entities"); + }); }); describe("no-args handler", () => { From 4fbde3ed0813b74750be5e6835566717c51c004c Mon Sep 17 00:00:00 2001 From: Takayuki Miyauchi Date: Wed, 15 Jul 2026 15:57:12 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E5=AF=BE=E5=BF=9C=20=E2=80=94=20?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=83=98=E3=83=AB=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=81=AE=20spy=20=E5=BE=A9=E5=85=83=E3=82=92=20try/finally=20?= =?UTF-8?q?=E3=81=A7=E4=BF=9D=E8=A8=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/help.test.ts | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/help.test.ts b/tests/help.test.ts index 366cdbd..5970127 100644 --- a/tests/help.test.ts +++ b/tests/help.test.ts @@ -26,15 +26,18 @@ async function runExpectingError(argv: string[]): Promise { throw new Error("process.exit"); }); try { - await prog.parseAsync(["node", "geonic", ...argv]); - } catch { - // expected + try { + await prog.parseAsync(["node", "geonic", ...argv]); + } catch { + // expected + } + const output = stripAnsi(errSpy.mock.calls.map((c) => c[0]).join("\n")); + expect(exitSpy).toHaveBeenCalledWith(1); + return output; + } finally { + errSpy.mockRestore(); + exitSpy.mockRestore(); } - const output = stripAnsi(errSpy.mock.calls.map((c) => c[0]).join("\n")); - expect(exitSpy).toHaveBeenCalledWith(1); - errSpy.mockRestore(); - exitSpy.mockRestore(); - return output; } describe("help", () => { @@ -700,15 +703,18 @@ describe("help", () => { const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { - await prog.parseAsync(["node", "geonic", ...argv]); - } catch { - // exitOverride throws on help + try { + await prog.parseAsync(["node", "geonic", ...argv]); + } catch { + // exitOverride throws on help + } + const output = stripAnsi(writeSpy.mock.calls.map((c) => c[0]).join("")); + expect(errSpy).not.toHaveBeenCalled(); + return output; + } finally { + writeSpy.mockRestore(); + errSpy.mockRestore(); } - const output = stripAnsi(writeSpy.mock.calls.map((c) => c[0]).join("")); - expect(errSpy).not.toHaveBeenCalled(); - writeSpy.mockRestore(); - errSpy.mockRestore(); - return output; } it("errors for unknown top-level command with --help", async () => {