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" }); }); }); });