From b98335c1620bda79a3e27e5392f5e77f6bdd7f03 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 14:18:48 +0700 Subject: [PATCH] fix(commands): await async command validation --- src/commands/command-execution-contract.ts | 4 +- .../command-execution-contract.test.ts | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/commands/command-execution-contract.ts b/src/commands/command-execution-contract.ts index f52af12c..3fff0098 100644 --- a/src/commands/command-execution-contract.ts +++ b/src/commands/command-execution-contract.ts @@ -7,7 +7,7 @@ export interface CommandExecutionContract { parse(rawArgs: string[]): TParsedArgs; - validate(parsedArgs: TParsedArgs): void; + validate(parsedArgs: TParsedArgs): void | Promise; execute(parsedArgs: TParsedArgs): Promise | TExecutionResult; render( result: TExecutionResult, @@ -23,7 +23,7 @@ export async function runCommandWithContract( contract: CommandExecutionContract ): Promise<{ parsedArgs: TParsedArgs; result: TExecutionResult }> { const parsedArgs = contract.parse(rawArgs); - contract.validate(parsedArgs); + await contract.validate(parsedArgs); const result = await contract.execute(parsedArgs); await contract.render(result, { rawArgs, parsedArgs }); return { parsedArgs, result }; diff --git a/tests/unit/commands/command-execution-contract.test.ts b/tests/unit/commands/command-execution-contract.test.ts index 2e116766..e9cd4eb6 100644 --- a/tests/unit/commands/command-execution-contract.test.ts +++ b/tests/unit/commands/command-execution-contract.test.ts @@ -56,4 +56,57 @@ describe('runCommandWithContract', () => { await expect(promise).rejects.toThrow('validation failed'); expect(lifecycle).toEqual(['parse', 'validate']); }); + + it('awaits async validate before execute and render', async () => { + const lifecycle: string[] = []; + + const result = await runCommandWithContract(['--flag'], { + parse: (rawArgs) => { + lifecycle.push('parse'); + return { parsed: true, value: rawArgs[0] }; + }, + validate: async () => { + lifecycle.push('validate:start'); + await Promise.resolve(); + lifecycle.push('validate:end'); + }, + execute: (parsedArgs) => { + lifecycle.push('execute'); + return { output: parsedArgs.value.toUpperCase() }; + }, + render: () => { + lifecycle.push('render'); + }, + }); + + expect(lifecycle).toEqual(['parse', 'validate:start', 'validate:end', 'execute', 'render']); + expect(result.result).toEqual({ output: '--FLAG' }); + }); + + it('short-circuits after async validate failure', async () => { + const lifecycle: string[] = []; + + const promise = runCommandWithContract([], { + parse: () => { + lifecycle.push('parse'); + return { valid: false }; + }, + validate: async () => { + lifecycle.push('validate:start'); + await Promise.resolve(); + lifecycle.push('validate:reject'); + throw new Error('async validation failed'); + }, + execute: () => { + lifecycle.push('execute'); + return { ok: true }; + }, + render: () => { + lifecycle.push('render'); + }, + }); + + await expect(promise).rejects.toThrow('async validation failed'); + expect(lifecycle).toEqual(['parse', 'validate:start', 'validate:reject']); + }); });