import { describe, it, expect } from 'vitest'; import { createGameContext } from '../../src/core/context'; import type { Command, CommandRunner, CommandRunnerContext } from '../../src/utils/command'; import { parseCommandSchema } from '../../src/utils/command/schema-parse'; describe('Command System', () => { function createTestGame() { const game = createGameContext(); return game; } function createRunner( schemaStr: string, fn: (this: CommandRunnerContext, cmd: Command) => Promise ): CommandRunner { return { schema: parseCommandSchema(schemaStr), run: fn, }; } describe('registerCommand', () => { it('should register and execute a command', async () => { const game = createTestGame(); game.registerCommand('look', createRunner('[--at]', async () => { return 'looked'; })); game.enqueue('look'); await new Promise(resolve => setTimeout(resolve, 50)); expect(game.commandRegistry.value.has('look')).toBe(true); }); it('should return error for unknown command', async () => { const game = createTestGame(); game.enqueue('unknown command'); await new Promise(resolve => setTimeout(resolve, 50)); }); }); describe('prompt and queue resolution', () => { it('should resolve prompt from queue input', async () => { const game = createTestGame(); let promptReceived: Command | null = null; game.registerCommand('move', createRunner(' ', async function(this: CommandRunnerContext, cmd) { const confirm = await this.prompt('confirm'); promptReceived = confirm; return { moved: cmd.params[0], confirmed: confirm.name }; })); game.enqueueAll([ 'move card1 hand', 'confirm', ]); await new Promise(resolve => setTimeout(resolve, 100)); expect(promptReceived).not.toBeNull(); expect(promptReceived!.name).toBe('confirm'); }); it('should handle multiple prompts in sequence', async () => { const game = createTestGame(); const prompts: Command[] = []; game.registerCommand('multi', createRunner('', async function() { const a = await this.prompt(''); prompts.push(a); const b = await this.prompt(''); prompts.push(b); return { a: a.params[0], b: b.params[0] }; })); game.enqueueAll([ 'multi init', 'first', 'second', ]); await new Promise(resolve => setTimeout(resolve, 100)); expect(prompts).toHaveLength(2); expect(prompts[0].params[0]).toBe('first'); expect(prompts[1].params[0]).toBe('second'); }); it('should handle command that completes without prompting', async () => { const game = createTestGame(); let executed = false; game.registerCommand('attack', createRunner(' [--power: number]', async function(cmd) { executed = true; return { target: cmd.params[0], power: cmd.options.power || '1' }; })); game.enqueue('attack goblin --power 5'); await new Promise(resolve => setTimeout(resolve, 50)); expect(executed).toBe(true); }); }); describe('nested command execution', () => { it('should allow a command to run another command', async () => { const game = createTestGame(); let childResult: unknown; game.registerCommand('child', createRunner('', async (cmd) => { return `child:${cmd.params[0]}`; })); game.registerCommand('parent', createRunner('', async function() { const output = await this.run('child test_arg'); if (!output.success) throw new Error(output.error); childResult = output.result; return `parent:${output.result}`; })); game.enqueue('parent start'); await new Promise(resolve => setTimeout(resolve, 100)); expect(childResult).toBe('child:test_arg'); }); it('should handle nested commands with prompts', async () => { const game = createTestGame(); let childPromptResult: Command | null = null; game.registerCommand('child', createRunner('', async function() { const confirm = await this.prompt('yes | no'); childPromptResult = confirm; return `child:${confirm.name}`; })); game.registerCommand('parent', createRunner('', async function() { const output = await this.run('child target1'); if (!output.success) throw new Error(output.error); return `parent:${output.result}`; })); game.enqueueAll([ 'parent start', 'yes', ]); await new Promise(resolve => setTimeout(resolve, 100)); expect(childPromptResult).not.toBeNull(); expect(childPromptResult!.name).toBe('yes'); }); }); describe('enqueueAll for action log replay', () => { it('should process all inputs in order', async () => { const game = createTestGame(); const results: string[] = []; game.registerCommand('step', createRunner('', async (cmd) => { results.push(cmd.params[0] as string); return cmd.params[0]; })); game.enqueueAll([ 'step one', 'step two', 'step three', ]); await new Promise(resolve => setTimeout(resolve, 100)); expect(results).toEqual(['one', 'two', 'three']); }); it('should buffer inputs and resolve prompts automatically', async () => { const game = createTestGame(); let prompted: Command | null = null; game.registerCommand('interactive', createRunner('', async function() { const response = await this.prompt(''); prompted = response; return { start: 'start', reply: response.params[0] }; })); game.enqueueAll([ 'interactive begin', 'hello', ]); await new Promise(resolve => setTimeout(resolve, 100)); expect(prompted).not.toBeNull(); expect(prompted!.params[0]).toBe('hello'); }); }); describe('command schema validation', () => { it('should reject commands that do not match schema', async () => { const game = createTestGame(); let errors: string[] = []; game.registerCommand('strict', createRunner('', async () => { return 'ok'; })); const originalError = console.error; console.error = (...args: unknown[]) => { errors.push(String(args[0])); }; game.enqueue('strict'); await new Promise(resolve => setTimeout(resolve, 50)); console.error = originalError; expect(errors.some(e => e.includes('Unknown') || e.includes('error'))).toBe(true); }); }); describe('context management', () => { it('should push and pop contexts', () => { const game = createTestGame(); game.pushContext({ type: 'sub-game' }); expect(game.contexts.value.length).toBe(2); game.popContext(); expect(game.contexts.value.length).toBe(1); }); it('should find latest context by type', () => { const game = createTestGame(); game.pushContext({ type: 'sub-game' }); const found = game.latestContext('sub-game'); expect(found).toBeDefined(); expect(found!.value.type).toBe('sub-game'); }); }); });