Initial commit: boardgame-core with build fixes
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createGameState } from '../../src/core/GameState';
|
||||
import { CommandExecutor } from '../../src/commands/CommandExecutor';
|
||||
import { Command, CommandActionType } from '../../src/commands/Command';
|
||||
import { RegionType } from '../../src/core/Region';
|
||||
|
||||
describe('CommandExecutor', () => {
|
||||
let gameState: ReturnType<typeof createGameState>;
|
||||
let executor: CommandExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
|
||||
executor = new CommandExecutor(gameState);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute a simple command successfully', () => {
|
||||
const command: Command = {
|
||||
id: 'test-command',
|
||||
name: 'Test Command',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.CreateMeeple,
|
||||
params: { id: 'meeple-1', color: 'red' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.executedSteps).toBe(1);
|
||||
expect(result.totalSteps).toBe(1);
|
||||
expect(gameState.getPart('meeple-1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should execute multi-step command', () => {
|
||||
const command: Command = {
|
||||
id: 'setup-command',
|
||||
name: 'Setup Command',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.CreateRegion,
|
||||
params: { id: 'board', type: RegionType.Keyed },
|
||||
},
|
||||
{
|
||||
action: CommandActionType.CreateMeeple,
|
||||
params: { id: 'meeple-1', color: 'blue' },
|
||||
},
|
||||
{
|
||||
action: CommandActionType.CreatePlacement,
|
||||
params: {
|
||||
id: 'placement-1',
|
||||
partId: 'meeple-1',
|
||||
regionId: 'board',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.executedSteps).toBe(3);
|
||||
expect(result.totalSteps).toBe(3);
|
||||
expect(gameState.getRegion('board')).toBeDefined();
|
||||
expect(gameState.getPart('meeple-1')).toBeDefined();
|
||||
expect(gameState.getPlacement('placement-1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should stop execution on error', () => {
|
||||
const command: Command = {
|
||||
id: 'failing-command',
|
||||
name: 'Failing Command',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.CreateMeeple,
|
||||
params: { id: 'meeple-1', color: 'red' },
|
||||
},
|
||||
{
|
||||
action: CommandActionType.CreatePlacement,
|
||||
params: {
|
||||
id: 'placement-1',
|
||||
partId: 'non-existent',
|
||||
regionId: 'non-existent',
|
||||
},
|
||||
},
|
||||
{
|
||||
action: CommandActionType.CreateMeeple,
|
||||
params: { id: 'meeple-2', color: 'blue' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.executedSteps).toBe(1);
|
||||
expect(result.totalSteps).toBe(3);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(gameState.getPart('meeple-1')).toBeDefined();
|
||||
expect(gameState.getPart('meeple-2')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should execute createCard command', () => {
|
||||
const command: Command = {
|
||||
id: 'create-card',
|
||||
name: 'Create Card',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.CreateCard,
|
||||
params: { id: 'card-1', suit: 'hearts', value: 10 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const card = gameState.getPart('card-1');
|
||||
expect(card).toBeDefined();
|
||||
expect(card?.type).toBe('card');
|
||||
});
|
||||
|
||||
it('should execute createTile command', () => {
|
||||
const command: Command = {
|
||||
id: 'create-tile',
|
||||
name: 'Create Tile',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.CreateTile,
|
||||
params: { id: 'tile-1', pattern: 'forest', rotation: 90 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const tile = gameState.getPart('tile-1');
|
||||
expect(tile).toBeDefined();
|
||||
expect(tile?.type).toBe('tile');
|
||||
});
|
||||
|
||||
it('should execute movePlacement command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateRegion, params: { id: 'board1', type: RegionType.Unkeyed } },
|
||||
{ action: CommandActionType.CreateRegion, params: { id: 'board2', type: RegionType.Unkeyed } },
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board1' } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
const moveCommand: Command = {
|
||||
id: 'move',
|
||||
name: 'Move',
|
||||
steps: [
|
||||
{
|
||||
action: CommandActionType.MovePlacement,
|
||||
params: { placementId: 'p1', targetRegionId: 'board2' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(moveCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const placement = gameState.getPlacement('p1');
|
||||
expect(placement?.regionId).toBe('board2');
|
||||
});
|
||||
|
||||
it('should execute flipPlacement command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board', faceUp: true } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
const flipCommand: Command = {
|
||||
id: 'flip',
|
||||
name: 'Flip',
|
||||
steps: [
|
||||
{ action: CommandActionType.FlipPlacement, params: { placementId: 'p1' } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(flipCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const placement = gameState.getPlacement('p1');
|
||||
expect(placement?.faceUp).toBe(false);
|
||||
});
|
||||
|
||||
it('should execute setPhase command', () => {
|
||||
const command: Command = {
|
||||
id: 'set-phase',
|
||||
name: 'Set Phase',
|
||||
steps: [
|
||||
{ action: CommandActionType.SetPhase, params: { phase: 'midgame' } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(command);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(gameState.data.value.phase).toBe('midgame');
|
||||
});
|
||||
|
||||
it('should execute swapPlacements command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm2', color: 'blue' } },
|
||||
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } },
|
||||
{ action: CommandActionType.CreatePlacement, params: { id: 'p2', partId: 'm2', regionId: 'board' } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
const region = gameState.getRegion('board');
|
||||
region!.placements.value = ['p1', 'p2'];
|
||||
|
||||
const swapCommand: Command = {
|
||||
id: 'swap',
|
||||
name: 'Swap',
|
||||
steps: [
|
||||
{ action: CommandActionType.SwapPlacements, params: { placementId1: 'p1', placementId2: 'p2' } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(swapCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(region!.placements.value).toEqual(['p2', 'p1']);
|
||||
});
|
||||
|
||||
it('should execute clearRegion command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } },
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
{ action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } },
|
||||
{ action: CommandActionType.AddPlacementToRegion, params: { regionId: 'board', placementId: 'p1' } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
const clearCommand: Command = {
|
||||
id: 'clear',
|
||||
name: 'Clear',
|
||||
steps: [
|
||||
{ action: CommandActionType.ClearRegion, params: { regionId: 'board' } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(clearCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const region = gameState.getRegion('board');
|
||||
expect(region?.placements.value.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should execute updatePart command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red', name: 'Original' } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
const updateCommand: Command = {
|
||||
id: 'update',
|
||||
name: 'Update',
|
||||
steps: [
|
||||
{ action: CommandActionType.UpdatePart, params: { partId: 'm1', updates: { name: 'Updated', color: 'blue' } } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(updateCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const part = gameState.getPart('m1');
|
||||
expect(part?.name).toBe('Updated');
|
||||
expect(part?.color).toBe('blue');
|
||||
});
|
||||
|
||||
it('should execute removePart command', () => {
|
||||
const setupCommand: Command = {
|
||||
id: 'setup',
|
||||
name: 'Setup',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
],
|
||||
};
|
||||
executor.execute(setupCommand);
|
||||
|
||||
expect(gameState.getPart('m1')).toBeDefined();
|
||||
|
||||
const removeCommand: Command = {
|
||||
id: 'remove',
|
||||
name: 'Remove',
|
||||
steps: [
|
||||
{ action: CommandActionType.RemovePart, params: { partId: 'm1' } },
|
||||
],
|
||||
};
|
||||
|
||||
const result = executor.execute(removeCommand);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(gameState.getPart('m1')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CommandLog, createCommandLog } from '../../src/commands/CommandLog';
|
||||
import { Command, CommandActionType, CommandExecutionResult, StepResult } from '../../src/commands/Command';
|
||||
|
||||
describe('CommandLog', () => {
|
||||
let log: CommandLog;
|
||||
|
||||
beforeEach(() => {
|
||||
log = createCommandLog();
|
||||
});
|
||||
|
||||
const sampleCommand: Command = {
|
||||
id: 'test-command',
|
||||
name: 'Test Command',
|
||||
steps: [
|
||||
{ action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } },
|
||||
],
|
||||
};
|
||||
|
||||
const sampleResult: CommandExecutionResult = {
|
||||
success: true,
|
||||
executedSteps: 1,
|
||||
totalSteps: 1,
|
||||
};
|
||||
|
||||
const sampleStepResults: StepResult[] = [
|
||||
{
|
||||
stepIndex: 0,
|
||||
action: CommandActionType.CreateMeeple,
|
||||
success: true,
|
||||
params: { id: 'm1', color: 'red' },
|
||||
},
|
||||
];
|
||||
|
||||
describe('log', () => {
|
||||
it('should log a command execution', () => {
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
|
||||
const entries = log.getEntries();
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0].commandId).toBe('test-command');
|
||||
expect(entries[0].commandName).toBe('Test Command');
|
||||
expect(entries[0].result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should add timestamp to log entry', () => {
|
||||
const beforeTime = Date.now();
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
const afterTime = Date.now();
|
||||
|
||||
const entry = log.getEntries()[0];
|
||||
expect(entry.timestamp).toBeGreaterThanOrEqual(beforeTime);
|
||||
expect(entry.timestamp).toBeLessThanOrEqual(afterTime);
|
||||
});
|
||||
|
||||
it('should log multiple entries', () => {
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
|
||||
expect(log.getEntries().length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilteredEntries', () => {
|
||||
it('should filter by commandId', () => {
|
||||
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' };
|
||||
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' };
|
||||
|
||||
log.log(command1, sampleResult, sampleStepResults);
|
||||
log.log(command2, sampleResult, sampleStepResults);
|
||||
log.log(command1, sampleResult, sampleStepResults);
|
||||
|
||||
const filtered = log.getFilteredEntries({ commandId: 'cmd-1' });
|
||||
expect(filtered.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should filter by success status', () => {
|
||||
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
|
||||
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Failed' };
|
||||
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
log.log(sampleCommand, failResult, []);
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
|
||||
const failed = log.getFilteredEntries({ success: false });
|
||||
expect(failed.length).toBe(1);
|
||||
|
||||
const successful = log.getFilteredEntries({ success: true });
|
||||
expect(successful.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should filter by time range', () => {
|
||||
const startTime = Date.now();
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
|
||||
// 模拟时间流逝
|
||||
const midTime = Date.now() + 100;
|
||||
|
||||
// 手动创建一个带时间戳的条目来测试时间过滤
|
||||
const entries = log.getEntries();
|
||||
expect(entries[0].timestamp).toBeGreaterThanOrEqual(startTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommandHistory', () => {
|
||||
it('should return history for a specific command', () => {
|
||||
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' };
|
||||
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' };
|
||||
|
||||
log.log(command1, sampleResult, sampleStepResults);
|
||||
log.log(command2, sampleResult, sampleStepResults);
|
||||
log.log(command1, sampleResult, sampleStepResults);
|
||||
|
||||
const history = log.getCommandHistory('cmd-1');
|
||||
expect(history.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFailedCommands', () => {
|
||||
it('should return only failed commands', () => {
|
||||
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
|
||||
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' };
|
||||
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
log.log(sampleCommand, failResult, []);
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
|
||||
const failed = log.getFailedCommands();
|
||||
expect(failed.length).toBe(1);
|
||||
expect(failed[0].result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuccessfulCommands', () => {
|
||||
it('should return only successful commands', () => {
|
||||
const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
|
||||
const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' };
|
||||
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
log.log(sampleCommand, failResult, []);
|
||||
log.log(sampleCommand, successResult, sampleStepResults);
|
||||
|
||||
const successful = log.getSuccessfulCommands();
|
||||
expect(successful.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear all log entries', () => {
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
|
||||
expect(log.getEntries().length).toBe(2);
|
||||
|
||||
log.clear();
|
||||
|
||||
expect(log.getEntries().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportToJson', () => {
|
||||
it('should export logs as JSON string', () => {
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
|
||||
const json = log.exportToJson();
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.length).toBe(1);
|
||||
expect(parsed[0].commandId).toBe('test-command');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCount', () => {
|
||||
it('should return the number of log entries', () => {
|
||||
expect(log.getCount()).toBe(0);
|
||||
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
expect(log.getCount()).toBe(1);
|
||||
|
||||
log.log(sampleCommand, sampleResult, sampleStepResults);
|
||||
expect(log.getCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastEntry', () => {
|
||||
it('should return the last log entry', () => {
|
||||
const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'First' };
|
||||
const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Last' };
|
||||
|
||||
log.log(command1, sampleResult, sampleStepResults);
|
||||
log.log(command2, sampleResult, sampleStepResults);
|
||||
|
||||
const lastEntry = log.getLastEntry();
|
||||
expect(lastEntry).not.toBeNull();
|
||||
expect(lastEntry?.commandId).toBe('cmd-2');
|
||||
});
|
||||
|
||||
it('should return null when log is empty', () => {
|
||||
const lastEntry = log.getLastEntry();
|
||||
expect(lastEntry).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue management', () => {
|
||||
describe('enqueue', () => {
|
||||
it('should add command to queue', () => {
|
||||
const queued = log.enqueue(sampleCommand);
|
||||
|
||||
expect(queued.id).toBe('test-command');
|
||||
expect(log.getQueueLength()).toBe(1);
|
||||
});
|
||||
|
||||
it('should set initial status to Pending', () => {
|
||||
const queued = log.enqueue(sampleCommand);
|
||||
expect(queued.status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dequeue', () => {
|
||||
it('should remove and return the first command from queue', () => {
|
||||
const command1: Command = { ...sampleCommand, id: 'cmd-1' };
|
||||
const command2: Command = { ...sampleCommand, id: 'cmd-2' };
|
||||
|
||||
log.enqueue(command1);
|
||||
log.enqueue(command2);
|
||||
|
||||
const dequeued = log.dequeue();
|
||||
expect(dequeued?.command.id).toBe('cmd-1');
|
||||
expect(log.getQueueLength()).toBe(1);
|
||||
});
|
||||
|
||||
it('should return null when queue is empty', () => {
|
||||
const dequeued = log.dequeue();
|
||||
expect(dequeued).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateQueueStatus', () => {
|
||||
it('should update command status in queue', () => {
|
||||
log.enqueue(sampleCommand);
|
||||
|
||||
log.updateQueueStatus('test-command', 'executing');
|
||||
const queue = log.getQueue();
|
||||
expect(queue[0].status).toBe('executing');
|
||||
});
|
||||
|
||||
it('should set executedAt and result when status is Completed', () => {
|
||||
const result: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 };
|
||||
log.enqueue(sampleCommand);
|
||||
|
||||
const beforeTime = Date.now();
|
||||
log.updateQueueStatus('test-command', 'completed', result);
|
||||
const afterTime = Date.now();
|
||||
|
||||
const queue = log.getQueue();
|
||||
expect(queue[0].status).toBe('completed');
|
||||
expect(queue[0].result).toEqual(result);
|
||||
expect(queue[0].executedAt).toBeGreaterThanOrEqual(beforeTime);
|
||||
expect(queue[0].executedAt).toBeLessThanOrEqual(afterTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearQueue', () => {
|
||||
it('should clear all queued commands', () => {
|
||||
log.enqueue(sampleCommand);
|
||||
log.enqueue(sampleCommand);
|
||||
|
||||
expect(log.getQueueLength()).toBe(2);
|
||||
|
||||
log.clearQueue();
|
||||
|
||||
expect(log.getQueueLength()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CommandParser, createCommandParser } from '../../src/commands/CommandParser';
|
||||
import { CommandParseError } from '../../src/commands/CommandParser';
|
||||
|
||||
describe('CommandParser', () => {
|
||||
let parser: CommandParser;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = createCommandParser();
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('should parse simple command without args', () => {
|
||||
const result = parser.parse('shuffle');
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual([]);
|
||||
expect(result.args.flags).toEqual({});
|
||||
});
|
||||
|
||||
it('should parse command with positional args', () => {
|
||||
const result = parser.parse('move card-1 discard');
|
||||
|
||||
expect(result.commandName).toBe('move');
|
||||
expect(result.args.positional).toEqual(['card-1', 'discard']);
|
||||
expect(result.args.flags).toEqual({});
|
||||
});
|
||||
|
||||
it('should parse command with multiple positional args', () => {
|
||||
const result = parser.parse('position p1 3 5');
|
||||
|
||||
expect(result.commandName).toBe('position');
|
||||
expect(result.args.positional).toEqual(['p1', '3', '5']);
|
||||
});
|
||||
|
||||
it('should parse command with flag', () => {
|
||||
const result = parser.parse('shuffle discard --seed=2026');
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual(['discard']);
|
||||
expect(result.args.flags).toEqual({ seed: 2026 });
|
||||
});
|
||||
|
||||
it('should parse command with multiple flags', () => {
|
||||
const result = parser.parse('create meeple m1 --color=red --name=Player1');
|
||||
|
||||
expect(result.commandName).toBe('create');
|
||||
expect(result.args.positional).toEqual(['meeple', 'm1']);
|
||||
expect(result.args.flags).toEqual({ color: 'red', name: 'Player1' });
|
||||
});
|
||||
|
||||
it('should parse command with boolean flag', () => {
|
||||
const result = parser.parse('flip p1 --faceup');
|
||||
|
||||
expect(result.commandName).toBe('flip');
|
||||
expect(result.args.positional).toEqual(['p1']);
|
||||
expect(result.args.flags).toEqual({ faceup: true });
|
||||
});
|
||||
|
||||
it('should parse command with short flag', () => {
|
||||
const result = parser.parse('shuffle d1 -s');
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual(['d1']);
|
||||
expect(result.args.flags).toEqual({ s: true });
|
||||
});
|
||||
|
||||
it('should parse command with short flag and value', () => {
|
||||
const result = parser.parse('shuffle d1 --seed=2026');
|
||||
|
||||
expect(result.commandName).toBe('shuffle');
|
||||
expect(result.args.positional).toEqual(['d1']);
|
||||
expect(result.args.flags).toEqual({ seed: 2026 });
|
||||
});
|
||||
|
||||
it('should parse command with string number value', () => {
|
||||
const result = parser.parse('rotate p1 90');
|
||||
|
||||
expect(result.commandName).toBe('rotate');
|
||||
expect(result.args.positional).toEqual(['p1', '90']);
|
||||
});
|
||||
|
||||
it('should parse command with negative number', () => {
|
||||
const result = parser.parse('rotate p1 -45');
|
||||
|
||||
expect(result.commandName).toBe('rotate');
|
||||
expect(result.args.positional).toEqual(['p1', '-45']);
|
||||
});
|
||||
|
||||
it('should parse command with float number', () => {
|
||||
const result = parser.parse('rotate p1 45.5');
|
||||
|
||||
expect(result.commandName).toBe('rotate');
|
||||
expect(result.args.positional).toEqual(['p1', '45.5']);
|
||||
});
|
||||
|
||||
it('should parse command with quoted string', () => {
|
||||
const result = parser.parse('create meeple m1 --name="Red Player"');
|
||||
|
||||
expect(result.commandName).toBe('create');
|
||||
expect(result.args.positional).toEqual(['meeple', 'm1']);
|
||||
expect(result.args.flags).toEqual({ name: 'Red Player' });
|
||||
});
|
||||
|
||||
it('should parse command with single quoted string', () => {
|
||||
const result = parser.parse("create meeple m1 --name='Blue Player'");
|
||||
|
||||
expect(result.commandName).toBe('create');
|
||||
expect(result.args.positional).toEqual(['meeple', 'm1']);
|
||||
expect(result.args.flags).toEqual({ name: 'Blue Player' });
|
||||
});
|
||||
|
||||
it('should handle extra whitespace', () => {
|
||||
const result = parser.parse(' move card-1 discard ');
|
||||
|
||||
expect(result.commandName).toBe('move');
|
||||
expect(result.args.positional).toEqual(['card-1', 'discard']);
|
||||
});
|
||||
|
||||
it('should throw on empty command', () => {
|
||||
expect(() => parser.parse('')).toThrow(CommandParseError);
|
||||
expect(() => parser.parse(' ')).toThrow(CommandParseError);
|
||||
});
|
||||
|
||||
it('should throw on unclosed quote', () => {
|
||||
expect(() => parser.parse('create meeple m1 --name="Red')).toThrow(CommandParseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCommand', () => {
|
||||
it('should format simple command', () => {
|
||||
const formatted = CommandParser.formatCommand('shuffle');
|
||||
expect(formatted).toBe('shuffle');
|
||||
});
|
||||
|
||||
it('should format command with positional args', () => {
|
||||
const formatted = CommandParser.formatCommand('move', {
|
||||
positional: ['card-1', 'discard'],
|
||||
flags: {},
|
||||
});
|
||||
expect(formatted).toBe('move card-1 discard');
|
||||
});
|
||||
|
||||
it('should format command with flags', () => {
|
||||
const formatted = CommandParser.formatCommand('shuffle', {
|
||||
positional: ['discard'],
|
||||
flags: { seed: 2026 },
|
||||
});
|
||||
expect(formatted).toBe('shuffle discard --seed=2026');
|
||||
});
|
||||
|
||||
it('should format command with boolean flag', () => {
|
||||
const formatted = CommandParser.formatCommand('flip', {
|
||||
positional: ['p1'],
|
||||
flags: { faceup: true },
|
||||
});
|
||||
expect(formatted).toBe('flip p1 --faceup');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CommandRegistry, createCommandRegistry } from '../../src/commands/CommandRegistry';
|
||||
import type { CliCommand } from '../../src/commands/CliCommand';
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
let registry: CommandRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = createCommandRegistry();
|
||||
});
|
||||
|
||||
const sampleCommand: CliCommand = {
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
usage: 'test <arg>',
|
||||
args: [
|
||||
{ name: 'arg', description: 'Test argument', required: true },
|
||||
],
|
||||
handler: (args) => {
|
||||
return [
|
||||
{
|
||||
action: 'createMeeple',
|
||||
params: { id: args.positional[0], color: 'red' },
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a command', () => {
|
||||
registry.register(sampleCommand);
|
||||
|
||||
expect(registry.has('test')).toBe(true);
|
||||
expect(registry.get('test')).toBe(sampleCommand);
|
||||
});
|
||||
|
||||
it('should register multiple commands', () => {
|
||||
const cmd1: CliCommand = {
|
||||
name: 'cmd1',
|
||||
description: 'Command 1',
|
||||
usage: 'cmd1',
|
||||
handler: () => [],
|
||||
};
|
||||
const cmd2: CliCommand = {
|
||||
name: 'cmd2',
|
||||
description: 'Command 2',
|
||||
usage: 'cmd2',
|
||||
handler: () => [],
|
||||
};
|
||||
|
||||
registry.registerAll([cmd1, cmd2]);
|
||||
|
||||
expect(registry.has('cmd1')).toBe(true);
|
||||
expect(registry.has('cmd2')).toBe(true);
|
||||
expect(registry.getCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return undefined for non-existent command', () => {
|
||||
const cmd = registry.get('non-existent');
|
||||
expect(cmd).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return existing command', () => {
|
||||
registry.register(sampleCommand);
|
||||
const cmd = registry.get('test');
|
||||
expect(cmd?.name).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregister', () => {
|
||||
it('should remove a command', () => {
|
||||
registry.register(sampleCommand);
|
||||
expect(registry.has('test')).toBe(true);
|
||||
|
||||
registry.unregister('test');
|
||||
expect(registry.has('test')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all registered commands', () => {
|
||||
registry.register(sampleCommand);
|
||||
|
||||
const cmd2: CliCommand = {
|
||||
name: 'cmd2',
|
||||
description: 'Command 2',
|
||||
usage: 'cmd2',
|
||||
handler: () => [],
|
||||
};
|
||||
registry.register(cmd2);
|
||||
|
||||
const all = registry.getAll();
|
||||
expect(all.length).toBe(2);
|
||||
expect(all.map((c) => c.name)).toEqual(['test', 'cmd2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute a command successfully', () => {
|
||||
registry.register(sampleCommand);
|
||||
|
||||
const result = registry.execute('test meeple-1');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.steps.length).toBe(1);
|
||||
expect(result.steps[0].action).toBe('createMeeple');
|
||||
expect(result.steps[0].params).toEqual({ id: 'meeple-1', color: 'red' });
|
||||
});
|
||||
|
||||
it('should return error for unknown command', () => {
|
||||
const result = registry.execute('unknown arg1');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Unknown command');
|
||||
expect(result.steps).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error for missing required argument', () => {
|
||||
registry.register(sampleCommand);
|
||||
|
||||
const result = registry.execute('test');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Missing required argument');
|
||||
});
|
||||
|
||||
it('should execute command with flags', () => {
|
||||
const cmdWithFlags: CliCommand = {
|
||||
name: 'move',
|
||||
description: 'Move command',
|
||||
usage: 'move <id> [--to=region]',
|
||||
args: [
|
||||
{ name: 'id', description: 'ID', required: true },
|
||||
],
|
||||
flags: [
|
||||
{ name: 'to', description: 'Target', type: 'string' },
|
||||
],
|
||||
handler: (args) => {
|
||||
return [
|
||||
{
|
||||
action: 'movePlacement',
|
||||
params: {
|
||||
placementId: args.positional[0],
|
||||
targetRegionId: args.flags.to,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
registry.register(cmdWithFlags);
|
||||
|
||||
const result = registry.execute('move p1 --to=board');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.steps[0].params).toEqual({
|
||||
placementId: 'p1',
|
||||
targetRegionId: 'board',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle command with optional args', () => {
|
||||
const cmdOptional: CliCommand = {
|
||||
name: 'draw',
|
||||
description: 'Draw cards',
|
||||
usage: 'draw [count]',
|
||||
args: [
|
||||
{ name: 'count', description: 'Count', required: false, default: '1' },
|
||||
],
|
||||
handler: (args) => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
registry.register(cmdOptional);
|
||||
|
||||
const result = registry.execute('draw');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('help', () => {
|
||||
beforeEach(() => {
|
||||
registry.register({
|
||||
name: 'move',
|
||||
description: 'Move a placement',
|
||||
usage: 'move <id> <target>',
|
||||
args: [
|
||||
{ name: 'id', description: 'Placement ID', required: true },
|
||||
{ name: 'target', description: 'Target region', required: true },
|
||||
],
|
||||
flags: [
|
||||
{ name: 'key', description: 'Slot key', type: 'string', alias: 'k' },
|
||||
],
|
||||
handler: () => [],
|
||||
});
|
||||
|
||||
registry.register({
|
||||
name: 'flip',
|
||||
description: 'Flip a placement',
|
||||
usage: 'flip <id>',
|
||||
args: [
|
||||
{ name: 'id', description: 'Placement ID', required: true },
|
||||
],
|
||||
handler: () => [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should show all commands help', () => {
|
||||
const help = registry.help();
|
||||
|
||||
expect(help).toContain('Available commands');
|
||||
expect(help).toContain('move');
|
||||
expect(help).toContain('flip');
|
||||
expect(help).toContain('help <command>');
|
||||
});
|
||||
|
||||
it('should show specific command help', () => {
|
||||
const help = registry.help('move');
|
||||
|
||||
expect(help).toContain('Command: move');
|
||||
expect(help).toContain('Move a placement');
|
||||
expect(help).toContain('Arguments:');
|
||||
expect(help).toContain('Flags:');
|
||||
});
|
||||
|
||||
it('should show error for unknown command help', () => {
|
||||
const help = registry.help('unknown');
|
||||
expect(help).toContain('Unknown command');
|
||||
});
|
||||
|
||||
it('should show command with alias', () => {
|
||||
const help = registry.help('move');
|
||||
expect(help).toContain('-k,');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear all commands', () => {
|
||||
registry.register(sampleCommand);
|
||||
registry.register({
|
||||
name: 'cmd2',
|
||||
description: 'Command 2',
|
||||
usage: 'cmd2',
|
||||
handler: () => [],
|
||||
});
|
||||
|
||||
expect(registry.getCount()).toBe(2);
|
||||
registry.clear();
|
||||
expect(registry.getCount()).toBe(0);
|
||||
expect(registry.getAll()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user