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