From 974c1a828cddda813abbf60914100bb0170e1073 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Apr 2026 15:37:15 +0800 Subject: [PATCH] test: reformat and expand test coverage --- tests/core/game.test.ts | 5 +- .../grid-inventory.test.ts | 2 +- tests/utils/command-runner.test.ts | 936 +++++++++--------- 3 files changed, 495 insertions(+), 448 deletions(-) diff --git a/tests/core/game.test.ts b/tests/core/game.test.ts index d2a17bb..8981f46 100644 --- a/tests/core/game.test.ts +++ b/tests/core/game.test.ts @@ -63,7 +63,10 @@ describe("createGameContext", () => { registry.register( "test ", async function (this: CommandRunnerContext, _ctx, value) { - return this.prompt(createPromptDef("prompt "), () => "ok"); + return this.prompt( + createPromptDef("prompt "), + (answer) => answer, + ); }, ); diff --git a/tests/samples/slay-the-spire-like/grid-inventory.test.ts b/tests/samples/slay-the-spire-like/grid-inventory.test.ts index 62e78af..94077cf 100644 --- a/tests/samples/slay-the-spire-like/grid-inventory.test.ts +++ b/tests/samples/slay-the-spire-like/grid-inventory.test.ts @@ -23,7 +23,7 @@ function createTestItem( id: string, shapeStr: string, transform = IDENTITY_TRANSFORM, -): InventoryItem { +): InventoryItem> { const shape = parseShapeString(shapeStr); return { id, diff --git a/tests/utils/command-runner.test.ts b/tests/utils/command-runner.test.ts index abb1e03..997eb05 100644 --- a/tests/utils/command-runner.test.ts +++ b/tests/utils/command-runner.test.ts @@ -1,523 +1,567 @@ -import { describe, it, expect } from 'vitest'; -import { parseCommandSchema } from '@/utils/command/schema-parse'; +import { describe, it, expect } from "vitest"; +import { parseCommandSchema } from "@/utils/command/schema-parse"; import { - createCommandRegistry, - registerCommand, - unregisterCommand, - hasCommand, - getCommand, - runCommand, - createCommandRunnerContext, - type CommandRegistry, - type CommandRunnerContextExport, -} from '@/utils/command/command-registry'; -import type { CommandRunner, PromptEvent } from '@/utils/command/command-runner'; + createCommandRegistry, + registerCommand, + unregisterCommand, + hasCommand, + getCommand, + runCommand, + createCommandRunnerContext, + type CommandRegistry, + type CommandRunnerContextExport, +} from "@/utils/command/command-registry"; +import type { + CommandRunner, + PromptEvent, +} from "@/utils/command/command-runner"; type TestContext = { - counter: number; - log: string[]; + counter: number; + log: string[]; }; -describe('CommandRegistry', () => { - it('should create an empty registry', () => { - const registry = createCommandRegistry(); - expect(registry.size).toBe(0); - }); +describe("CommandRegistry", () => { + it("should create an empty registry", () => { + const registry = createCommandRegistry(); + expect(registry.size).toBe(0); + }); - it('should register a command', () => { - const registry = createCommandRegistry(); - const runner: CommandRunner = { - schema: parseCommandSchema('add '), - 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 register a command", () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + schema: parseCommandSchema("add "), + 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(); - const runner: CommandRunner = { - 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 unregister a command", () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + 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(); - const runner: CommandRunner = { - schema: parseCommandSchema('get'), - run: async () => {}, - }; - registerCommand(registry, runner); - const retrieved = getCommand(registry, 'get'); - expect(retrieved).toBe(runner); - }); + it("should get a command runner", () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + 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(); - const retrieved = getCommand(registry, 'unknown'); - expect(retrieved).toBeUndefined(); - }); + it("should return undefined for unknown command", () => { + const registry = createCommandRegistry(); + const retrieved = getCommand(registry, "unknown"); + expect(retrieved).toBeUndefined(); + }); }); -describe('runCommand', () => { - it('should run a command successfully', async () => { - const registry = createCommandRegistry(); - const runner: CommandRunner = { - schema: parseCommandSchema('add '), - run: async function (cmd) { - return Number(cmd.params[0]) + Number(cmd.params[1]); - }, - }; - registerCommand(registry, runner); +describe("runCommand", () => { + it("should run a command successfully", async () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + schema: parseCommandSchema("add "), + 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); - } - }); + 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(); - 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 unknown command", async () => { + const registry = createCommandRegistry(); + 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(); - const runner: CommandRunner = { - schema: parseCommandSchema('add '), - run: async () => {}, - }; - registerCommand(registry, runner); + it("should fail for invalid command params", async () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + schema: parseCommandSchema("add "), + 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('参数不足'); - } - }); + 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(); - const runner: CommandRunner = { - schema: parseCommandSchema('increment'), - run: async function () { - this.context.counter++; - return this.context.counter; - }, - }; - registerCommand(registry, runner); + it("should access context via this.context", async () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + 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); - }); + const ctx = { counter: 0, log: [] }; + await runCommand(registry, ctx, "increment"); + expect(ctx.counter).toBe(1); + }); - it('should handle async errors', async () => { - const registry = createCommandRegistry(); - const runner: CommandRunner = { - schema: parseCommandSchema('fail'), - run: async () => { - throw new Error('Something went wrong'); - }, - }; - registerCommand(registry, runner); + it("should handle async errors", async () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + 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'); - } - }); + 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(); - const ctx = { counter: 0, log: [] }; - const runnerCtx = createCommandRunnerContext(registry, ctx); - expect(runnerCtx.registry).toBe(registry); - expect(runnerCtx.context).toBe(ctx); - }); +describe("CommandRunnerContext", () => { + it("should create a runner context", () => { + const registry = createCommandRegistry(); + 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(); - const runner: CommandRunner = { - schema: parseCommandSchema('greet '), - run: async function (cmd) { - this.context.log.push(`Hello, ${cmd.params[0]}!`); - return `Hello, ${cmd.params[0]}!`; - }, - }; - registerCommand(registry, runner); + it("should run commands via runner context", async () => { + const registry = createCommandRegistry(); + const runner: CommandRunner = { + schema: parseCommandSchema("greet "), + 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!']); - }); + 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(); + it("should allow commands to call other commands via this.run", async () => { + const registry = createCommandRegistry(); - const addRunner: CommandRunner = { - schema: parseCommandSchema('add '), - run: async function (cmd) { - return Number(cmd.params[0]) + Number(cmd.params[1]); - }, - }; + const addRunner: CommandRunner = { + schema: parseCommandSchema("add "), + run: async function (cmd) { + return Number(cmd.params[0]) + Number(cmd.params[1]); + }, + }; - registerCommand(registry, addRunner); + registerCommand(registry, addRunner); - const multiplyRunner: CommandRunner = { - schema: parseCommandSchema('multiply '), - 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; - }, - }; + const multiplyRunner: CommandRunner = { + schema: parseCommandSchema("multiply "), + 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); + 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); - } - }); + 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(); + it("should allow commands to call other commands via this.runParsed", async () => { + const registry = createCommandRegistry(); - const doubleRunner: CommandRunner = { - schema: parseCommandSchema('double '), - run: async function (cmd) { - return Number(cmd.params[0]) * 2; - }, - }; + const doubleRunner: CommandRunner = { + schema: parseCommandSchema("double "), + run: async function (cmd) { + return Number(cmd.params[0]) * 2; + }, + }; - registerCommand(registry, doubleRunner); + registerCommand(registry, doubleRunner); - const quadrupleRunner: CommandRunner = { - schema: parseCommandSchema('quadruple '), - 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; - }, - }; + const quadrupleRunner: CommandRunner = { + schema: parseCommandSchema("quadruple "), + 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); + 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); - } - }); + 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); + } + }); }); -describe('prompt', () => { - it('should dispatch prompt event with string schema', async () => { - const registry = createCommandRegistry(); +describe("prompt", () => { + it("should dispatch prompt event with string schema", async () => { + const registry = createCommandRegistry(); - const chooseRunner: CommandRunner = { - schema: parseCommandSchema('choose'), - run: async function () { - const result = await this.prompt('select ', (cmd) => cmd.params[0] as string); - return result; - }, - }; + const chooseRunner: CommandRunner = { + schema: parseCommandSchema("choose"), + run: async function () { + const result = await this.prompt( + parseCommandSchema("select "), + (cmd) => cmd.params[0] as string, + ); + return result; + }, + }; - registerCommand(registry, chooseRunner); + registerCommand(registry, chooseRunner); - const ctx = { counter: 0, log: [] }; - let promptEvent: PromptEvent | null = null; + 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'); - - promptEvent!.cancel('test cleanup'); - await runPromise; + const runnerCtx = createCommandRunnerContext(registry, ctx); + runnerCtx.on("prompt", (e) => { + promptEvent = e; }); - it('should resolve prompt with valid input', async () => { - const registry = createCommandRegistry(); + const runPromise = runnerCtx.run("choose"); - const chooseRunner: CommandRunner = { - schema: parseCommandSchema('choose'), - run: async function () { - const result = await this.prompt('select ', (card) => card as string); - this.context.log.push(`selected ${result}`); - return result; - }, - }; + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvent).not.toBeNull(); + expect(promptEvent!.schema.name).toBe("select"); - registerCommand(registry, chooseRunner); + promptEvent!.cancel("test cleanup"); + await runPromise; + }); - const ctx = { counter: 0, log: [] }; - let promptEvent: PromptEvent | null = null; + it("should resolve prompt with valid input", async () => { + const registry = createCommandRegistry(); - const runnerCtx = createCommandRunnerContext(registry, ctx); - runnerCtx.on('prompt', (e) => { - promptEvent = e; - }); + const chooseRunner: CommandRunner = { + schema: parseCommandSchema("choose"), + run: async function () { + const result = await this.prompt( + parseCommandSchema("select "), + (card) => card as string, + ); + this.context.log.push(`selected ${result}`); + return result; + }, + }; - const runPromise = runnerCtx.run('choose'); + registerCommand(registry, chooseRunner); - await new Promise((r) => setTimeout(r, 0)); - expect(promptEvent).not.toBeNull(); + const ctx = { counter: 0, log: [] }; + let promptEvent: PromptEvent | null = null; - const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} }; - const error = promptEvent!.tryCommit(parsed); - expect(error).toBeNull(); + const runnerCtx = createCommandRunnerContext(registry, ctx); + runnerCtx.on("prompt", (e) => { + promptEvent = e; + }); - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('Ace'); + const runPromise = runnerCtx.run("choose"); + + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvent).not.toBeNull(); + + const parsed = { name: "select", params: ["Ace"], options: {}, flags: {} }; + const error = promptEvent!.tryCommit(parsed); + expect(error).toBeNull(); + + 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(); + + const chooseRunner: CommandRunner = { + schema: parseCommandSchema("choose"), + run: async function () { + try { + await this.prompt("select ", (card) => card as string); + return "unexpected success"; + } catch (e) { + return (e as Error).message; } - expect(ctx.log).toEqual(['selected Ace']); + }, + }; + + registerCommand(registry, chooseRunner); + + const ctx = { counter: 0, log: [] }; + let promptEvent: PromptEvent | null = null; + + const runnerCtx = createCommandRunnerContext(registry, ctx); + runnerCtx.on("prompt", (e) => { + promptEvent = e; }); - it('should reject prompt with invalid input', async () => { - const registry = createCommandRegistry(); + const runPromise = runnerCtx.run("choose"); - const chooseRunner: CommandRunner = { - schema: parseCommandSchema('choose'), - run: async function () { - try { - await this.prompt('select ', (card) => card as string); - return 'unexpected success'; - } catch (e) { - return (e as Error).message; - } - }, - }; + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvent).not.toBeNull(); - registerCommand(registry, chooseRunner); + promptEvent!.cancel("user cancelled"); - const ctx = { counter: 0, log: [] }; - let promptEvent: PromptEvent | null = null; + const result = await runPromise; + expect(result.success).toBe(true); + if (result.success) { + expect(result.result).toBe("user cancelled"); + } + }); - const runnerCtx = createCommandRunnerContext(registry, ctx); - runnerCtx.on('prompt', (e) => { - promptEvent = e; + it("should accept CommandSchema object in prompt", async () => { + const registry = createCommandRegistry(); + const schema = parseCommandSchema("pick "); + + const pickRunner: CommandRunner = { + schema: parseCommandSchema("pick"), + run: async function () { + const result = await this.prompt(schema, (item) => item as string); + return result; + }, + }; + + 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"); + + const error = promptEvent!.tryCommit({ + name: "pick", + params: ["sword"], + options: {}, + flags: {}, + }); + expect(error).toBeNull(); + + 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(); + + const multiPromptRunner: CommandRunner = { + schema: parseCommandSchema("multi"), + run: async function () { + const first = await this.prompt("first ", (a) => a as string); + const second = await this.prompt("second ", (b) => b as string); + return [first, second]; + }, + }; + + 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"); + + const error1 = promptEvents[0].tryCommit({ + name: "first", + params: ["one"], + options: {}, + flags: {}, + }); + expect(error1).toBeNull(); + + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvents.length).toBe(2); + expect(promptEvents[1].schema.name).toBe("second"); + + const error2 = promptEvents[1].tryCommit({ + name: "second", + params: ["two"], + options: {}, + flags: {}, + }); + expect(error2).toBeNull(); + + const result = await runPromise; + expect(result.success).toBe(true); + if (result.success) { + expect(result.result).toEqual(["one", "two"]); + } + }); + + it("should validate input with validator function", async () => { + const registry = createCommandRegistry(); + + const chooseRunner: CommandRunner = { + schema: parseCommandSchema("choose"), + run: async function () { + const result = await this.prompt("select ", (card) => { + const cardStr = card as string; + if (!["Ace", "King", "Queen"].includes(cardStr)) { + throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`; + } + return cardStr; }); + return result; + }, + }; - const runPromise = runnerCtx.run('choose'); + registerCommand(registry, chooseRunner); - await new Promise((r) => setTimeout(r, 0)); - expect(promptEvent).not.toBeNull(); + const ctx = { counter: 0, log: [] }; + let promptEvent: PromptEvent | null = null; - promptEvent!.cancel('user cancelled'); - - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('user cancelled'); - } + const runnerCtx = createCommandRunnerContext(registry, ctx); + runnerCtx.on("prompt", (e) => { + promptEvent = e; }); - it('should accept CommandSchema object in prompt', async () => { - const registry = createCommandRegistry(); - const schema = parseCommandSchema('pick '); + const runPromise = runnerCtx.run("choose"); - const pickRunner: CommandRunner = { - schema: parseCommandSchema('pick'), - run: async function () { - const result = await this.prompt(schema, (item) => item as string); - return result; - }, - }; + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvent).not.toBeNull(); - registerCommand(registry, pickRunner); + // Try invalid input + const invalidError = promptEvent!.tryCommit({ + name: "select", + params: ["Jack"], + options: {}, + flags: {}, + }); + expect(invalidError).toContain("Invalid card: Jack"); - const ctx = { counter: 0, log: [] }; - let promptEvent: PromptEvent | null = null; + // Try valid input + const validError = promptEvent!.tryCommit({ + name: "select", + params: ["Ace"], + options: {}, + flags: {}, + }); + expect(validError).toBeNull(); - const runnerCtx = createCommandRunnerContext(registry, ctx); - runnerCtx.on('prompt', (e) => { - promptEvent = e; - }); + const result = await runPromise; + expect(result.success).toBe(true); + if (result.success) { + expect(result.result).toBe("Ace"); + } + }); - const runPromise = runnerCtx.run('pick'); + it("should allow cancel with custom reason", async () => { + const registry = createCommandRegistry(); - await new Promise((r) => setTimeout(r, 0)); - expect(promptEvent).not.toBeNull(); - expect(promptEvent!.schema.name).toBe('pick'); - - const error = promptEvent!.tryCommit({ name: 'pick', params: ['sword'], options: {}, flags: {} }); - expect(error).toBeNull(); - - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('sword'); + const chooseRunner: CommandRunner = { + schema: parseCommandSchema("choose"), + run: async function () { + try { + await this.prompt("select ", (cmd) => cmd.params[0] as string); + 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; }); - it('should allow multiple sequential prompts', async () => { - const registry = createCommandRegistry(); + const runPromise = runnerCtx.run("choose"); - const multiPromptRunner: CommandRunner = { - schema: parseCommandSchema('multi'), - run: async function () { - const first = await this.prompt('first ', (a) => a as string); - const second = await this.prompt('second ', (b) => b as string); - return [first, second]; - }, - }; + await new Promise((r) => setTimeout(r, 0)); + expect(promptEvent).not.toBeNull(); - registerCommand(registry, multiPromptRunner); + promptEvent!.cancel("custom cancellation reason"); - 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'); - - const error1 = promptEvents[0].tryCommit({ name: 'first', params: ['one'], options: {}, flags: {} }); - expect(error1).toBeNull(); - - await new Promise((r) => setTimeout(r, 0)); - expect(promptEvents.length).toBe(2); - expect(promptEvents[1].schema.name).toBe('second'); - - const error2 = promptEvents[1].tryCommit({ name: 'second', params: ['two'], options: {}, flags: {} }); - expect(error2).toBeNull(); - - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toEqual(['one', 'two']); - } - }); - - it('should validate input with validator function', async () => { - const registry = createCommandRegistry(); - - const chooseRunner: CommandRunner = { - schema: parseCommandSchema('choose'), - run: async function () { - const result = await this.prompt( - 'select ', - (card) => { - const cardStr = card as string; - if (!['Ace', 'King', 'Queen'].includes(cardStr)) { - throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`; - } - return cardStr; - } - ); - return result; - }, - }; - - 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(); - - // Try invalid input - const invalidError = promptEvent!.tryCommit({ name: 'select', params: ['Jack'], options: {}, flags: {} }); - expect(invalidError).toContain('Invalid card: Jack'); - - // Try valid input - const validError = promptEvent!.tryCommit({ name: 'select', params: ['Ace'], options: {}, flags: {} }); - expect(validError).toBeNull(); - - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('Ace'); - } - }); - - it('should allow cancel with custom reason', async () => { - const registry = createCommandRegistry(); - - const chooseRunner: CommandRunner = { - schema: parseCommandSchema('choose'), - run: async function () { - try { - await this.prompt('select ', (cmd) => cmd.params[0] as string); - 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!.cancel('custom cancellation reason'); - - const result = await runPromise; - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('custom cancellation reason'); - } - }); + const result = await runPromise; + expect(result.success).toBe(true); + if (result.success) { + expect(result.result).toBe("custom cancellation reason"); + } + }); });