refactor: move runner context to handler's this

This commit is contained in:
2026-04-02 09:05:47 +08:00
parent 3bc35df63c
commit bcb31da773
4 changed files with 270 additions and 30 deletions
+189 -2
View File
@@ -9,9 +9,9 @@ import {
runCommand,
createCommandRunnerContext,
type CommandRegistry,
type CommandRunnerContext,
type CommandRunnerContextExport,
} from '../../src/utils/command/command-registry';
import type { CommandRunner } from '../../src/utils/command/command-runner';
import type { CommandRunner, PromptEvent } from '../../src/utils/command/command-runner';
type TestContext = {
counter: number;
@@ -238,3 +238,190 @@ describe('CommandRunnerContext', () => {
}
});
});
describe('prompt', () => {
it('should dispatch prompt event with string schema', async () => {
const registry = createCommandRegistry<TestContext>();
const chooseRunner: CommandRunner<TestContext, string> = {
schema: parseCommandSchema('choose'),
run: async function () {
const result = await this.prompt('select <card>');
return result.params[0] as string;
},
};
registerCommand(registry, chooseRunner);
const ctx = { counter: 0, log: [] };
let promptEvent: PromptEvent | null = null;
const runnerCtx = createCommandRunnerContext(registry, ctx);
runnerCtx.on('prompt', (e) => {
promptEvent = e;
});
const runPromise = runnerCtx.run('choose');
await new Promise((r) => setTimeout(r, 0));
expect(promptEvent).not.toBeNull();
expect(promptEvent!.schema.name).toBe('select');
});
it('should resolve prompt with valid input', async () => {
const registry = createCommandRegistry<TestContext>();
const chooseRunner: CommandRunner<TestContext, string> = {
schema: parseCommandSchema('choose'),
run: async function () {
const result = await this.prompt('select <card>');
this.context.log.push(`selected ${result.params[0]}`);
return result.params[0] as string;
},
};
registerCommand(registry, chooseRunner);
const ctx = { counter: 0, log: [] };
let promptEvent: PromptEvent | null = null;
const runnerCtx = createCommandRunnerContext(registry, ctx);
runnerCtx.on('prompt', (e) => {
promptEvent = e;
});
const runPromise = runnerCtx.run('choose');
await new Promise((r) => setTimeout(r, 0));
expect(promptEvent).not.toBeNull();
const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} };
promptEvent!.resolve(parsed);
const result = await runPromise;
expect(result.success).toBe(true);
if (result.success) {
expect(result.result).toBe('Ace');
}
expect(ctx.log).toEqual(['selected Ace']);
});
it('should reject prompt with invalid input', async () => {
const registry = createCommandRegistry<TestContext>();
const chooseRunner: CommandRunner<TestContext, string> = {
schema: parseCommandSchema('choose'),
run: async function () {
try {
await this.prompt('select <card>');
return 'unexpected success';
} catch (e) {
return (e as Error).message;
}
},
};
registerCommand(registry, chooseRunner);
const ctx = { counter: 0, log: [] };
let promptEvent: PromptEvent | null = null;
const runnerCtx = createCommandRunnerContext(registry, ctx);
runnerCtx.on('prompt', (e) => {
promptEvent = e;
});
const runPromise = runnerCtx.run('choose');
await new Promise((r) => setTimeout(r, 0));
expect(promptEvent).not.toBeNull();
promptEvent!.reject(new Error('user cancelled'));
const result = await runPromise;
expect(result.success).toBe(true);
if (result.success) {
expect(result.result).toBe('user cancelled');
}
});
it('should accept CommandSchema object in prompt', async () => {
const registry = createCommandRegistry<TestContext>();
const schema = parseCommandSchema('pick <item>');
const pickRunner: CommandRunner<TestContext, string> = {
schema: parseCommandSchema('pick'),
run: async function () {
const result = await this.prompt(schema);
return result.params[0] as string;
},
};
registerCommand(registry, pickRunner);
const ctx = { counter: 0, log: [] };
let promptEvent: PromptEvent | null = null;
const runnerCtx = createCommandRunnerContext(registry, ctx);
runnerCtx.on('prompt', (e) => {
promptEvent = e;
});
const runPromise = runnerCtx.run('pick');
await new Promise((r) => setTimeout(r, 0));
expect(promptEvent).not.toBeNull();
expect(promptEvent!.schema.name).toBe('pick');
promptEvent!.resolve({ name: 'pick', params: ['sword'], options: {}, flags: {} });
const result = await runPromise;
expect(result.success).toBe(true);
if (result.success) {
expect(result.result).toBe('sword');
}
});
it('should allow multiple sequential prompts', async () => {
const registry = createCommandRegistry<TestContext>();
const multiPromptRunner: CommandRunner<TestContext, string[]> = {
schema: parseCommandSchema('multi'),
run: async function () {
const first = await this.prompt('first <a>');
const second = await this.prompt('second <b>');
return [first.params[0] as string, second.params[0] as string];
},
};
registerCommand(registry, multiPromptRunner);
const ctx = { counter: 0, log: [] };
const promptEvents: PromptEvent[] = [];
const runnerCtx = createCommandRunnerContext(registry, ctx);
runnerCtx.on('prompt', (e) => {
promptEvents.push(e);
});
const runPromise = runnerCtx.run('multi');
await new Promise((r) => setTimeout(r, 0));
expect(promptEvents.length).toBe(1);
expect(promptEvents[0].schema.name).toBe('first');
promptEvents[0].resolve({ name: 'first', params: ['one'], options: {}, flags: {} });
await new Promise((r) => setTimeout(r, 0));
expect(promptEvents.length).toBe(2);
expect(promptEvents[1].schema.name).toBe('second');
promptEvents[1].resolve({ name: 'second', params: ['two'], options: {}, flags: {} });
const result = await runPromise;
expect(result.success).toBe(true);
if (result.success) {
expect(result.result).toEqual(['one', 'two']);
}
});
});