feat: add command runner and registry
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseCommandSchema } from '../../src/utils/command/schema-parse';
|
||||
import {
|
||||
createCommandRegistry,
|
||||
registerCommand,
|
||||
unregisterCommand,
|
||||
hasCommand,
|
||||
getCommand,
|
||||
runCommand,
|
||||
createCommandRunnerContext,
|
||||
type CommandRegistry,
|
||||
type CommandRunnerContext,
|
||||
} from '../../src/utils/command/command-registry';
|
||||
import type { CommandRunner } from '../../src/utils/command/command-runner';
|
||||
|
||||
type TestContext = {
|
||||
counter: number;
|
||||
log: string[];
|
||||
};
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
it('should create an empty registry', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
expect(registry.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should register a command', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('add <a> <b>'),
|
||||
run: async function (cmd) {
|
||||
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||
},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
expect(registry.size).toBe(1);
|
||||
expect(hasCommand(registry, 'add')).toBe(true);
|
||||
});
|
||||
|
||||
it('should unregister a command', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext> = {
|
||||
schema: parseCommandSchema('remove'),
|
||||
run: async () => {},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
expect(hasCommand(registry, 'remove')).toBe(true);
|
||||
unregisterCommand(registry, 'remove');
|
||||
expect(hasCommand(registry, 'remove')).toBe(false);
|
||||
});
|
||||
|
||||
it('should get a command runner', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext> = {
|
||||
schema: parseCommandSchema('get'),
|
||||
run: async () => {},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
const retrieved = getCommand(registry, 'get');
|
||||
expect(retrieved).toBe(runner);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown command', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const retrieved = getCommand(registry, 'unknown');
|
||||
expect(retrieved).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('runCommand', () => {
|
||||
it('should run a command successfully', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('add <a> <b>'),
|
||||
run: async function (cmd) {
|
||||
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||
},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
|
||||
const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1 2');
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.result).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail for unknown command', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const result = await runCommand(registry, { counter: 0, log: [] }, 'unknown');
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('Unknown command');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail for invalid command params', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext> = {
|
||||
schema: parseCommandSchema('add <a> <b>'),
|
||||
run: async () => {},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
|
||||
const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1');
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('参数不足');
|
||||
}
|
||||
});
|
||||
|
||||
it('should access context via this.context', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('increment'),
|
||||
run: async function () {
|
||||
this.context.counter++;
|
||||
return this.context.counter;
|
||||
},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
|
||||
const ctx = { counter: 0, log: [] };
|
||||
await runCommand(registry, ctx, 'increment');
|
||||
expect(ctx.counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle async errors', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext> = {
|
||||
schema: parseCommandSchema('fail'),
|
||||
run: async () => {
|
||||
throw new Error('Something went wrong');
|
||||
},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
|
||||
const result = await runCommand(registry, { counter: 0, log: [] }, 'fail');
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error).toBe('Something went wrong');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CommandRunnerContext', () => {
|
||||
it('should create a runner context', () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const ctx = { counter: 0, log: [] };
|
||||
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||
expect(runnerCtx.registry).toBe(registry);
|
||||
expect(runnerCtx.context).toBe(ctx);
|
||||
});
|
||||
|
||||
it('should run commands via runner context', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
const runner: CommandRunner<TestContext, string> = {
|
||||
schema: parseCommandSchema('greet <name>'),
|
||||
run: async function (cmd) {
|
||||
this.context.log.push(`Hello, ${cmd.params[0]}!`);
|
||||
return `Hello, ${cmd.params[0]}!`;
|
||||
},
|
||||
};
|
||||
registerCommand(registry, runner);
|
||||
|
||||
const ctx = { counter: 0, log: [] };
|
||||
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||
const result = await runnerCtx.run('greet World');
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.result).toBe('Hello, World!');
|
||||
}
|
||||
expect(ctx.log).toEqual(['Hello, World!']);
|
||||
});
|
||||
|
||||
it('should allow commands to call other commands via this.run', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
|
||||
const addRunner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('add <a> <b>'),
|
||||
run: async function (cmd) {
|
||||
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||
},
|
||||
};
|
||||
|
||||
registerCommand(registry, addRunner);
|
||||
|
||||
const multiplyRunner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('multiply <a> <b>'),
|
||||
run: async function (cmd) {
|
||||
const a = Number(cmd.params[0]);
|
||||
const b = Number(cmd.params[1]);
|
||||
const addResult = await this.run(`add ${a} ${a}`);
|
||||
if (!addResult.success) throw new Error('add failed');
|
||||
return (addResult.result as number) * b;
|
||||
},
|
||||
};
|
||||
|
||||
registerCommand(registry, multiplyRunner);
|
||||
|
||||
const ctx = { counter: 0, log: [] };
|
||||
const result = await runCommand(registry, ctx, 'multiply 3 4');
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.result).toBe(24);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow commands to call other commands via this.runParsed', async () => {
|
||||
const registry = createCommandRegistry<TestContext>();
|
||||
|
||||
const doubleRunner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('double <n>'),
|
||||
run: async function (cmd) {
|
||||
return Number(cmd.params[0]) * 2;
|
||||
},
|
||||
};
|
||||
|
||||
registerCommand(registry, doubleRunner);
|
||||
|
||||
const quadrupleRunner: CommandRunner<TestContext, number> = {
|
||||
schema: parseCommandSchema('quadruple <n>'),
|
||||
run: async function (cmd) {
|
||||
const n = Number(cmd.params[0]);
|
||||
const doubleResult = await this.runParsed({ name: 'double', params: [String(n)], options: {}, flags: {} });
|
||||
if (!doubleResult.success) throw new Error('double failed');
|
||||
return (doubleResult.result as number) * 2;
|
||||
},
|
||||
};
|
||||
|
||||
registerCommand(registry, quadrupleRunner);
|
||||
|
||||
const ctx = { counter: 0, log: [] };
|
||||
const result = await runCommand(registry, ctx, 'quadruple 5');
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.result).toBe(20);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user