2026-04-02 11:21:57 +08:00
|
|
|
import { describe, it, expect } from 'vitest';
|
2026-04-04 22:23:15 +08:00
|
|
|
import { createGameContext, createGameCommandRegistry, IGameContext } from '@/core/game';
|
|
|
|
|
import type { PromptEvent, Command } from '@/utils/command';
|
2026-04-02 12:48:29 +08:00
|
|
|
|
|
|
|
|
type MyState = {
|
|
|
|
|
score: number;
|
|
|
|
|
round: number;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-02 11:21:57 +08:00
|
|
|
describe('createGameContext', () => {
|
2026-04-02 15:16:30 +08:00
|
|
|
it('should create a game context with state', () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry();
|
2026-04-02 11:21:57 +08:00
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._state).not.toBeNull();
|
|
|
|
|
expect(ctx._state.value).toBeDefined();
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should wire commands to the context', () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry();
|
2026-04-02 11:21:57 +08:00
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._commands).not.toBeNull();
|
|
|
|
|
expect(ctx._commands.registry).toBe(registry);
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-02 12:48:29 +08:00
|
|
|
it('should accept initial state as an object', () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry<MyState>();
|
2026-04-02 15:16:30 +08:00
|
|
|
const ctx = createGameContext<MyState>(registry, {
|
|
|
|
|
score: 0,
|
|
|
|
|
round: 1,
|
2026-04-02 12:48:29 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._state.value.score).toBe(0);
|
|
|
|
|
expect(ctx._state.value.round).toBe(1);
|
2026-04-02 12:48:29 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept initial state as a factory function', () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry<MyState>();
|
2026-04-02 15:16:30 +08:00
|
|
|
const ctx = createGameContext<MyState>(registry, () => ({
|
|
|
|
|
score: 10,
|
|
|
|
|
round: 3,
|
2026-04-02 12:48:29 +08:00
|
|
|
}));
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._state.value.score).toBe(10);
|
|
|
|
|
expect(ctx._state.value.round).toBe(3);
|
2026-04-02 12:48:29 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-02 15:16:30 +08:00
|
|
|
it('should forward prompt events via listener', async () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry();
|
2026-04-02 11:21:57 +08:00
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
registry.register('test <value>', async function (_ctx, value) {
|
|
|
|
|
return this.prompt<string>('prompt <answer>', () => 'ok');
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-02 15:16:30 +08:00
|
|
|
const promptPromise = new Promise<PromptEvent>(resolve => {
|
2026-04-04 22:23:15 +08:00
|
|
|
ctx._commands.on('prompt', resolve);
|
2026-04-02 15:16:30 +08:00
|
|
|
});
|
2026-04-04 22:23:15 +08:00
|
|
|
const runPromise = ctx.run('test hello');
|
2026-04-02 11:21:57 +08:00
|
|
|
|
2026-04-02 15:16:30 +08:00
|
|
|
const promptEvent = await promptPromise;
|
2026-04-02 11:21:57 +08:00
|
|
|
expect(promptEvent).not.toBeNull();
|
|
|
|
|
expect(promptEvent.schema.name).toBe('prompt');
|
|
|
|
|
|
2026-04-02 19:08:14 +08:00
|
|
|
const error = promptEvent.tryCommit({ name: 'prompt', params: ['yes'], options: {}, flags: {} });
|
|
|
|
|
expect(error).toBeNull();
|
2026-04-02 11:21:57 +08:00
|
|
|
|
|
|
|
|
const result = await runPromise;
|
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('createGameCommand', () => {
|
|
|
|
|
it('should run a command with access to game context', async () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry<{ marker: string }>();
|
|
|
|
|
|
|
|
|
|
registry.register('set-marker <id>', async function (ctx, id) {
|
|
|
|
|
ctx.produce(state => {
|
2026-04-02 15:16:30 +08:00
|
|
|
state.marker = id;
|
|
|
|
|
});
|
2026-04-02 11:21:57 +08:00
|
|
|
return id;
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
const ctx = createGameContext(registry, { marker: '' });
|
|
|
|
|
|
|
|
|
|
const result = await ctx.run('set-marker board');
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
console.error('Error:', result.error);
|
|
|
|
|
}
|
2026-04-02 11:21:57 +08:00
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect(result.result).toBe('board');
|
|
|
|
|
}
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._state.value.marker).toBe('board');
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|
2026-04-02 12:48:29 +08:00
|
|
|
|
|
|
|
|
it('should run a typed command with extended context', async () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry<MyState>();
|
2026-04-02 12:48:29 +08:00
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
registry.register(
|
2026-04-02 12:48:29 +08:00
|
|
|
'add-score <amount:number>',
|
2026-04-04 22:23:15 +08:00
|
|
|
async function (ctx, amount) {
|
|
|
|
|
ctx.produce(state => {
|
2026-04-02 15:16:30 +08:00
|
|
|
state.score += amount;
|
|
|
|
|
});
|
2026-04-04 22:23:15 +08:00
|
|
|
return ctx.value.score;
|
2026-04-02 12:48:29 +08:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-02 15:16:30 +08:00
|
|
|
const ctx = createGameContext<MyState>(registry, () => ({
|
|
|
|
|
score: 0,
|
|
|
|
|
round: 1,
|
2026-04-02 12:48:29 +08:00
|
|
|
}));
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
const result = await ctx.run('add-score 5');
|
2026-04-02 12:48:29 +08:00
|
|
|
expect(result.success).toBe(true);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
expect(result.result).toBe(5);
|
|
|
|
|
}
|
2026-04-04 22:23:15 +08:00
|
|
|
expect(ctx._state.value.score).toBe(5);
|
2026-04-02 15:16:30 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return error for unknown command', async () => {
|
2026-04-04 22:23:15 +08:00
|
|
|
const registry = createGameCommandRegistry();
|
2026-04-02 15:16:30 +08:00
|
|
|
const ctx = createGameContext(registry);
|
|
|
|
|
|
2026-04-04 22:23:15 +08:00
|
|
|
const result = await ctx.run('nonexistent');
|
2026-04-02 15:16:30 +08:00
|
|
|
expect(result.success).toBe(false);
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
expect(result.error).toContain('nonexistent');
|
|
|
|
|
}
|
2026-04-02 12:48:29 +08:00
|
|
|
});
|
2026-04-02 11:21:57 +08:00
|
|
|
});
|