fix(commands): await async command validation

This commit is contained in:
Tam Nhu Tran
2026-02-12 14:18:48 +07:00
parent fc4b77bc52
commit b98335c162
2 changed files with 55 additions and 2 deletions
+2 -2
View File
@@ -7,7 +7,7 @@
export interface CommandExecutionContract<TParsedArgs, TExecutionResult> {
parse(rawArgs: string[]): TParsedArgs;
validate(parsedArgs: TParsedArgs): void;
validate(parsedArgs: TParsedArgs): void | Promise<void>;
execute(parsedArgs: TParsedArgs): Promise<TExecutionResult> | TExecutionResult;
render(
result: TExecutionResult,
@@ -23,7 +23,7 @@ export async function runCommandWithContract<TParsedArgs, TExecutionResult>(
contract: CommandExecutionContract<TParsedArgs, TExecutionResult>
): 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 };
@@ -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']);
});
});