220 lines
7.4 KiB
TypeScript
220 lines
7.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { createPromptContext } from "@/utils/command/command-prompt";
|
|
import type { CommandSchema } from "@/utils/command/types";
|
|
|
|
const mockSchema: CommandSchema = {
|
|
name: "test",
|
|
params: [{ name: "value", required: true, variadic: false }],
|
|
options: {},
|
|
flags: {},
|
|
};
|
|
|
|
describe("createPromptContext", () => {
|
|
describe("prompt", () => {
|
|
it("should store prompt and return promise", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
const result = ctx.tryCommit("global", "hello");
|
|
expect(result).toEqual({ ok: true });
|
|
await expect(promise).resolves.toBe("hello");
|
|
});
|
|
|
|
it("should associate prompt with specific player", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
const result = ctx.tryCommit("player1", "hello");
|
|
expect(result).toEqual({ ok: true });
|
|
await expect(promise).resolves.toBe("hello");
|
|
});
|
|
|
|
it("should cancel existing prompt when new prompt starts for same player", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
|
|
// Resolve promise2 first, then both will settle
|
|
ctx.tryCommit("player1", "test");
|
|
const [result1, result2] = await Promise.allSettled([promise1, promise2]);
|
|
|
|
expect(result1.status).toBe("rejected");
|
|
expect((result1 as PromiseRejectedResult).reason).toBe(
|
|
"Prompt overriden",
|
|
);
|
|
expect(result2.status).toBe("fulfilled");
|
|
});
|
|
|
|
it("should allow different players to have independent prompts", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player2");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.tryCommit("player1", "hello");
|
|
ctx.tryCommit("player2", "world");
|
|
|
|
await expect(promise1).resolves.toBe("hello");
|
|
await expect(promise2).resolves.toBe("world");
|
|
});
|
|
});
|
|
|
|
describe("tryCommit", () => {
|
|
it("should return ok:true when prompt exists and validation passes", () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value.toUpperCase();
|
|
|
|
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
const result = ctx.tryCommit("player1", "hello");
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
});
|
|
|
|
it("should return ok:false with reason when no prompt exists", () => {
|
|
const ctx = createPromptContext();
|
|
const result = ctx.tryCommit("nonexistent", "hello");
|
|
|
|
expect(result).toEqual({ ok: false, reason: "No Prompt" });
|
|
});
|
|
|
|
it("should return ok:false when validator throws string", () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => {
|
|
if (value.length < 3) throw "Value too short";
|
|
return value;
|
|
};
|
|
|
|
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
const result = ctx.tryCommit("player1", "ab");
|
|
|
|
expect(result).toEqual({ ok: false, reason: "Value too short" });
|
|
});
|
|
|
|
it("should throw when validator throws non-string error", () => {
|
|
const ctx = createPromptContext();
|
|
const validator = () => {
|
|
throw new Error("unexpected");
|
|
};
|
|
|
|
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
expect(() => ctx.tryCommit("player1", "test")).toThrow("unexpected");
|
|
});
|
|
|
|
it("should pass validator return value to promise resolver", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (a: number, b: number) => a + b;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.tryCommit("player1", 2, 3);
|
|
await expect(promise).resolves.toBe(5);
|
|
});
|
|
});
|
|
|
|
describe("cancel", () => {
|
|
it("should reject promise with default message", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.cancel("player1");
|
|
|
|
const result = await Promise.allSettled([promise]);
|
|
expect(result[0].status).toBe("rejected");
|
|
expect((result[0] as PromiseRejectedResult).reason).toBe(
|
|
"Prompt Cancelled",
|
|
);
|
|
});
|
|
|
|
it("should reject promise with custom reason", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.cancel("player1", "Custom cancel reason");
|
|
|
|
const result = await Promise.allSettled([promise]);
|
|
expect(result[0].status).toBe("rejected");
|
|
expect((result[0] as PromiseRejectedResult).reason).toBe(
|
|
"Custom cancel reason",
|
|
);
|
|
});
|
|
|
|
it("should do nothing when no prompt exists for player", () => {
|
|
const ctx = createPromptContext();
|
|
ctx.cancel("nonexistent");
|
|
});
|
|
|
|
it("should only cancel prompt for specific player", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player2");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.cancel("player1");
|
|
ctx.tryCommit("player2", "test");
|
|
|
|
const [result1, result2] = await Promise.allSettled([promise1, promise2]);
|
|
expect(result1.status).toBe("rejected");
|
|
expect((result1 as PromiseRejectedResult).reason).toBe(
|
|
"Prompt Cancelled",
|
|
);
|
|
|
|
expect(result2.status).toBe("fulfilled");
|
|
});
|
|
});
|
|
|
|
describe("handleCall", () => {
|
|
it("should be a middleware chain that can be extended", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
let middlewareCalled = false;
|
|
ctx.handleCall.use(async (call, next) => {
|
|
middlewareCalled = true;
|
|
return next();
|
|
});
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
expect(middlewareCalled).toBe(true);
|
|
ctx.tryCommit("global", "test");
|
|
await expect(promise).resolves.toBe("test");
|
|
});
|
|
|
|
it("should remove prompt from map after resolution", async () => {
|
|
const ctx = createPromptContext();
|
|
const validator = (value: string) => value;
|
|
|
|
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
ctx.tryCommit("global", "test");
|
|
await promise;
|
|
|
|
const result = ctx.tryCommit("global", "another");
|
|
expect(result).toEqual({ ok: false, reason: "No Prompt" });
|
|
});
|
|
});
|
|
});
|