From 34cb828f60e4a4d20bd2c93089e1fa99fb70406e Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 20 Apr 2026 15:50:32 +0800 Subject: [PATCH] test: refactor and format test files - Update `game.test.ts` to use `_ctx` when `this` is not required - Fix type assertion in `boop.test.ts` - Update `command-runner.test.ts` to use `parseCommandSchema` in prompt tests - Reformat `middleware.test.ts` to use double quotes and consistent indentation --- tests/core/game.test.ts | 15 +- tests/samples/boop.test.ts | 26 +- tests/utils/command-runner.test.ts | 37 +- tests/utils/middleware.test.ts | 783 +++++++++++++++-------------- 4 files changed, 441 insertions(+), 420 deletions(-) diff --git a/tests/core/game.test.ts b/tests/core/game.test.ts index 8981f46..06a3915 100644 --- a/tests/core/game.test.ts +++ b/tests/core/game.test.ts @@ -60,15 +60,12 @@ describe("createGameContext", () => { const registry = createGameCommandRegistry(); const ctx = createGameContext(registry); - registry.register( - "test ", - async function (this: CommandRunnerContext, _ctx, value) { - return this.prompt( - createPromptDef("prompt "), - (answer) => answer, - ); - }, - ); + registry.register("test ", async function (_ctx, value) { + return _ctx.prompt( + createPromptDef("prompt "), + (answer) => answer, + ); + }); const promptPromise = new Promise((resolve) => { ctx._commands.on("prompt", resolve); diff --git a/tests/samples/boop.test.ts b/tests/samples/boop.test.ts index a92d593..a10bcbc 100644 --- a/tests/samples/boop.test.ts +++ b/tests/samples/boop.test.ts @@ -201,7 +201,7 @@ describe("Boop Game", () => { ctx.produce((state) => { const whiteKitten = state.pieces["white-kitten-1"]; if (whiteKitten && whiteKitten.regionId === "board") { - whiteKitten.type = "cat"; + (whiteKitten as { type: string }).type = "cat"; } }); @@ -464,7 +464,7 @@ describe("Boop Game", () => { const k3 = state.pieces["white-kitten-3"]; if (k1) { - k1.type = "cat"; + (k1 as { type: string }).type = "cat"; k1.regionId = "board"; k1.position = [0, 0]; state.regions.board.partMap["0,0"] = k1.id; @@ -473,7 +473,7 @@ describe("Boop Game", () => { ); } if (k2) { - k2.type = "cat"; + (k2 as { type: string }).type = "cat"; k2.regionId = "board"; k2.position = [0, 1]; state.regions.board.partMap["0,1"] = k2.id; @@ -482,7 +482,7 @@ describe("Boop Game", () => { ); } if (k3) { - k3.type = "cat"; + (k3 as { type: string }).type = "cat"; k3.regionId = "board"; k3.position = [0, 2]; state.regions.board.partMap["0,2"] = k3.id; @@ -510,7 +510,7 @@ describe("Boop Game", () => { const k3 = state.pieces["black-kitten-3"]; if (k1) { - k1.type = "cat"; + (k1 as { type: string }).type = "cat"; k1.regionId = "board"; k1.position = [0, 0]; state.regions.board.partMap["0,0"] = k1.id; @@ -519,7 +519,7 @@ describe("Boop Game", () => { ); } if (k2) { - k2.type = "cat"; + (k2 as { type: string }).type = "cat"; k2.regionId = "board"; k2.position = [1, 0]; state.regions.board.partMap["1,0"] = k2.id; @@ -528,7 +528,7 @@ describe("Boop Game", () => { ); } if (k3) { - k3.type = "cat"; + (k3 as { type: string }).type = "cat"; k3.regionId = "board"; k3.position = [2, 0]; state.regions.board.partMap["2,0"] = k3.id; @@ -556,7 +556,7 @@ describe("Boop Game", () => { const k3 = state.pieces["white-kitten-3"]; if (k1) { - k1.type = "cat"; + (k1 as { type: string }).type = "cat"; k1.regionId = "board"; k1.position = [0, 0]; state.regions.board.partMap["0,0"] = k1.id; @@ -565,7 +565,7 @@ describe("Boop Game", () => { ); } if (k2) { - k2.type = "cat"; + (k2 as { type: string }).type = "cat"; k2.regionId = "board"; k2.position = [1, 1]; state.regions.board.partMap["1,1"] = k2.id; @@ -574,7 +574,7 @@ describe("Boop Game", () => { ); } if (k3) { - k3.type = "cat"; + (k3 as { type: string }).type = "cat"; k3.regionId = "board"; k3.position = [2, 2]; state.regions.board.partMap["2,2"] = k3.id; @@ -740,7 +740,7 @@ describe("Boop Game", () => { const c3 = state.pieces["white-cat-3"]; if (c1) { - c1.type = "cat"; + (c1 as { type: string }).type = "cat"; c1.regionId = "board"; c1.position = [0, 0]; state.regions.board.partMap["0,0"] = c1.id; @@ -749,7 +749,7 @@ describe("Boop Game", () => { ); } if (c2) { - c2.type = "cat"; + (c2 as { type: string }).type = "cat"; c2.regionId = "board"; c2.position = [0, 1]; state.regions.board.partMap["0,1"] = c2.id; @@ -758,7 +758,7 @@ describe("Boop Game", () => { ); } if (c3) { - c3.type = "cat"; + (c3 as { type: string }).type = "cat"; c3.regionId = "board"; c3.position = [0, 2]; state.regions.board.partMap["0,2"] = c3.id; diff --git a/tests/utils/command-runner.test.ts b/tests/utils/command-runner.test.ts index 997eb05..1f7bd6e 100644 --- a/tests/utils/command-runner.test.ts +++ b/tests/utils/command-runner.test.ts @@ -339,7 +339,10 @@ describe("prompt", () => { schema: parseCommandSchema("choose"), run: async function () { try { - await this.prompt("select ", (card) => card as string); + await this.prompt( + parseCommandSchema("select "), + (card) => card as string, + ); return "unexpected success"; } catch (e) { return (e as Error).message; @@ -420,8 +423,14 @@ describe("prompt", () => { const multiPromptRunner: CommandRunner = { schema: parseCommandSchema("multi"), run: async function () { - const first = await this.prompt("first ", (a) => a as string); - const second = await this.prompt("second ", (b) => b as string); + const first = await this.prompt( + parseCommandSchema("first "), + (a) => a as string, + ); + const second = await this.prompt( + parseCommandSchema("second "), + (b) => b as string, + ); return [first, second]; }, }; @@ -475,13 +484,16 @@ describe("prompt", () => { const chooseRunner: CommandRunner = { schema: parseCommandSchema("choose"), run: async function () { - const result = await this.prompt("select ", (card) => { - const cardStr = card as string; - if (!["Ace", "King", "Queen"].includes(cardStr)) { - throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`; - } - return cardStr; - }); + const result = await this.prompt( + parseCommandSchema("select "), + (card) => { + const cardStr = card as string; + if (!["Ace", "King", "Queen"].includes(cardStr)) { + throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`; + } + return cardStr; + }, + ); return result; }, }; @@ -533,7 +545,10 @@ describe("prompt", () => { schema: parseCommandSchema("choose"), run: async function () { try { - await this.prompt("select ", (cmd) => cmd.params[0] as string); + await this.prompt( + parseCommandSchema("select "), + (cmd) => cmd.params[0] as string, + ); return "unexpected success"; } catch (e) { return (e as Error).message; diff --git a/tests/utils/middleware.test.ts b/tests/utils/middleware.test.ts index d775d80..fcf0357 100644 --- a/tests/utils/middleware.test.ts +++ b/tests/utils/middleware.test.ts @@ -1,436 +1,445 @@ -import { describe, it, expect } from 'vitest'; -import { createMiddlewareChain, type MiddlewareChain } from '@/utils/middleware'; +import { describe, it, expect } from "vitest"; +import { + createMiddlewareChain, + type MiddlewareChain, +} from "@/utils/middleware"; -describe('createMiddlewareChain', () => { - describe('basic execution', () => { - it('should return context when no middlewares and no fallback', async () => { - const chain = createMiddlewareChain<{ value: number }>(); - const result = await chain.execute({ value: 42 }); +describe("createMiddlewareChain", () => { + describe("basic execution", () => { + it("should return context when no middlewares and no fallback", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + const result = await chain.execute({ value: 42 }); - expect(result).toEqual({ value: 42 }); - }); - - it('should call fallback when no middlewares', async () => { - const chain = createMiddlewareChain<{ value: number }, string>( - async ctx => `value is ${ctx.value}` - ); - const result = await chain.execute({ value: 42 }); - - expect(result).toBe('value is 42'); - }); - - it('should pass context to fallback', async () => { - const chain = createMiddlewareChain<{ a: number; b: number }, number>( - async ctx => ctx.a + ctx.b - ); - const result = await chain.execute({ a: 3, b: 7 }); - - expect(result).toBe(10); - }); + expect(result).toEqual({ value: 42 }); }); - describe('single middleware', () => { - it('should execute a single middleware', async () => { - const chain = createMiddlewareChain<{ count: number }>(); - chain.use(async (ctx, next) => { - ctx.count *= 2; - return next(); - }); + it("should call fallback when no middlewares", async () => { + const chain = createMiddlewareChain<{ value: number }, string>( + async (ctx) => `value is ${ctx.value}`, + ); + const result = await chain.execute({ value: 42 }); - const result = await chain.execute({ count: 5 }); - - expect(result.count).toBe(10); - }); - - it('should allow middleware to modify return value', async () => { - const chain = createMiddlewareChain<{ value: number }, number>( - async ctx => ctx.value - ); - chain.use(async (ctx, next) => { - const result = await next(); - return result * 2; - }); - - const result = await chain.execute({ value: 21 }); - - expect(result).toBe(42); - }); - - it('should allow middleware to short-circuit without calling next', async () => { - const chain = createMiddlewareChain<{ value: number }>(); - chain.use(async (_ctx, _next) => { - return { value: 999 }; - }); - - const result = await chain.execute({ value: 1 }); - - expect(result.value).toBe(999); - }); + expect(result).toBe("value is 42"); }); - describe('multiple middlewares', () => { - it('should execute middlewares in order', async () => { - const order: number[] = []; - const chain = createMiddlewareChain<{ value: number }>(); + it("should pass context to fallback", async () => { + const chain = createMiddlewareChain<{ a: number; b: number }, number>( + async (ctx) => ctx.a + ctx.b, + ); + const result = await chain.execute({ a: 3, b: 7 }); - chain.use(async (_ctx, next) => { - order.push(1); - const result = await next(); - order.push(4); - return result; - }); - chain.use(async (_ctx, next) => { - order.push(2); - const result = await next(); - order.push(3); - return result; - }); + expect(result).toBe(10); + }); + }); - await chain.execute({ value: 0 }); + describe("single middleware", () => { + it("should execute a single middleware", async () => { + const chain = createMiddlewareChain<{ count: number }>(); + chain.use(async (ctx, next) => { + ctx.count *= 2; + return next(); + }); - expect(order).toEqual([1, 2, 3, 4]); - }); + const result = await chain.execute({ count: 5 }); - it('should accumulate modifications through the chain', async () => { - const chain = createMiddlewareChain<{ value: number }>(); - - chain.use(async (ctx, next) => { - ctx.value += 1; - return next(); - }); - chain.use(async (ctx, next) => { - ctx.value *= 2; - return next(); - }); - chain.use(async (ctx, next) => { - ctx.value += 3; - return next(); - }); - - const result = await chain.execute({ value: 0 }); - - expect(result.value).toBe(5); - }); - - it('should allow middleware to modify result on the way back', async () => { - const chain = createMiddlewareChain<{ base: number }, number>( - async ctx => ctx.base - ); - - chain.use(async (_ctx, next) => { - const result = await next(); - return result + 10; - }); - chain.use(async (_ctx, next) => { - const result = await next(); - return result * 2; - }); - - const result = await chain.execute({ base: 5 }); - - expect(result).toBe(20); - }); - - it('should allow middleware to short-circuit in the middle', async () => { - const executed: number[] = []; - const chain = createMiddlewareChain<{ value: number }>(); - - chain.use(async (_ctx, next) => { - executed.push(1); - return next(); - }); - chain.use(async () => { - executed.push(2); - return { value: -1 }; - }); - chain.use(async (_ctx, next) => { - executed.push(3); - return next(); - }); - - const result = await chain.execute({ value: 100 }); - - expect(result.value).toBe(-1); - expect(executed).toEqual([1, 2]); - }); + expect(result.count).toBe(10); }); - describe('nested next calls', () => { - it('should advance index on each next call, skipping remaining middlewares', async () => { - const chain = createMiddlewareChain<{ counter: number }>(); + it("should allow middleware to modify return value", async () => { + const chain = createMiddlewareChain<{ value: number }, number>( + async (ctx) => ctx.value, + ); + chain.use(async (ctx, next) => { + const result = await next(); + return result * 2; + }); - chain.use(async (_ctx, next) => { - await next(); - await next(); - }); + const result = await chain.execute({ value: 21 }); - const result = await chain.execute({ counter: 0 }); - - expect(result).toBeUndefined(); - }); - - it('should allow middleware to call next conditionally', async () => { - const chain = createMiddlewareChain<{ skip: boolean; value: number }>(); - - chain.use(async (ctx, next) => { - if (ctx.skip) { - return { value: -1 }; - } - return next(); - }); - chain.use(async (ctx, next) => { - ctx.value += 10; - return next(); - }); - - const resultA = await chain.execute({ skip: true, value: 0 }); - const resultB = await chain.execute({ skip: false, value: 0 }); - - expect(resultA.value).toBe(-1); - expect(resultB.value).toBe(10); - }); - - it('should handle middleware that awaits next multiple times with a fallback', async () => { - const log: string[] = []; - const chain = createMiddlewareChain<{ value: number }, string[]>( - async _ctx => log - ); - - chain.use(async (_ctx, next) => { - log.push('before'); - await next(); - log.push('after-first'); - await next(); - log.push('after-second'); - return log; - }); - chain.use(async (_ctx, next) => { - log.push('mw2'); - return next(); - }); - - const result = await chain.execute({ value: 0 }); - - expect(result).toEqual(['before', 'mw2', 'after-first', 'after-second']); - }); - - it('should return fallback result on second next call when no more middlewares remain', async () => { - const chain = createMiddlewareChain<{ value: number }, number>( - async ctx => ctx.value * 10 - ); - - chain.use(async (_ctx, next) => { - await next(); - return await next(); - }); - chain.use(async (_ctx, next) => { - return next(); - }); - - const result = await chain.execute({ value: 5 }); - - expect(result).toBe(50); - }); - - it('should return fallback result on second next call when no more middlewares remain', async () => { - const chain = createMiddlewareChain<{ value: number }, number>( - async ctx => ctx.value * 10 - ); - - chain.use(async (_ctx, next) => { - await next(); - return await next(); - }); - chain.use(async (_ctx, next) => { - return next(); - }); - - const result = await chain.execute({ value: 5 }); - - expect(result).toBe(50); - }); + expect(result).toBe(42); }); - describe('async behavior', () => { - it('should handle async middlewares', async () => { - const chain = createMiddlewareChain<{ value: number }>(); + it("should allow middleware to short-circuit without calling next", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + chain.use(async (_ctx, _next) => { + return { value: 999 }; + }); - chain.use(async (ctx, next) => { - await new Promise(resolve => setTimeout(resolve, 10)); - ctx.value += 1; - return next(); - }); - chain.use(async (ctx, next) => { - await new Promise(resolve => setTimeout(resolve, 10)); - ctx.value += 2; - return next(); - }); + const result = await chain.execute({ value: 1 }); - const result = await chain.execute({ value: 0 }); + expect(result.value).toBe(999); + }); + }); - expect(result.value).toBe(3); - }); + describe("multiple middlewares", () => { + it("should execute middlewares in order", async () => { + const order: number[] = []; + const chain = createMiddlewareChain<{ value: number }>(); - it('should handle async fallback', async () => { - const chain = createMiddlewareChain<{ value: number }, number>( - async ctx => { - await new Promise(resolve => setTimeout(resolve, 10)); - return ctx.value * 10; - } - ); + chain.use(async (_ctx, next) => { + order.push(1); + const result = await next(); + order.push(4); + return result; + }); + chain.use(async (_ctx, next) => { + order.push(2); + const result = await next(); + order.push(3); + return result; + }); - const result = await chain.execute({ value: 5 }); + await chain.execute({ value: 0 }); - expect(result).toBe(50); - }); + expect(order).toEqual([1, 2, 3, 4]); }); - describe('error handling', () => { - it('should propagate errors from middleware', async () => { - const chain = createMiddlewareChain<{ value: number }>(); + it("should accumulate modifications through the chain", async () => { + const chain = createMiddlewareChain<{ value: number }>(); - chain.use(async () => { - throw new Error('middleware error'); - }); + chain.use(async (ctx, next) => { + ctx.value += 1; + return next(); + }); + chain.use(async (ctx, next) => { + ctx.value *= 2; + return next(); + }); + chain.use(async (ctx, next) => { + ctx.value += 3; + return next(); + }); - await expect(chain.execute({ value: 1 })).rejects.toThrow('middleware error'); - }); + const result = await chain.execute({ value: 0 }); - it('should propagate errors from fallback', async () => { - const chain = createMiddlewareChain<{ value: number }, number>( - async () => { - throw new Error('fallback error'); - } - ); - - await expect(chain.execute({ value: 1 })).rejects.toThrow('fallback error'); - }); - - it('should allow middleware to catch errors from downstream', async () => { - const chain = createMiddlewareChain<{ value: number }>(); - - chain.use(async (_ctx, next) => { - try { - return await next(); - } catch { - return { value: -1 }; - } - }); - chain.use(async () => { - throw new Error('downstream error'); - }); - - const result = await chain.execute({ value: 1 }); - - expect(result.value).toBe(-1); - }); + expect(result.value).toBe(5); }); - describe('return type override', () => { - it('should support different TReturn type than TContext', async () => { - const chain = createMiddlewareChain<{ name: string }, string>( - async ctx => `Hello, ${ctx.name}!` - ); + it("should allow middleware to modify result on the way back", async () => { + const chain = createMiddlewareChain<{ base: number }, number>( + async (ctx) => ctx.base, + ); - const result = await chain.execute({ name: 'World' }); + chain.use(async (_ctx, next) => { + const result = await next(); + return result + 10; + }); + chain.use(async (_ctx, next) => { + const result = await next(); + return result * 2; + }); - expect(result).toBe('Hello, World!'); - }); + const result = await chain.execute({ base: 5 }); - it('should allow middleware to transform return type', async () => { - const chain = createMiddlewareChain<{ items: number[] }, number>( - async ctx => ctx.items.reduce((a, b) => a + b, 0) - ); - - chain.use(async (_ctx, next) => { - const sum = await next(); - return sum * 2; - }); - - const result = await chain.execute({ items: [1, 2, 3] }); - - expect(result).toBe(12); - }); + expect(result).toBe(20); }); - describe('reusability', () => { - it('should reset index on each execute call', async () => { - const chain = createMiddlewareChain<{ count: number }>(); + it("should allow middleware to short-circuit in the middle", async () => { + const executed: number[] = []; + const chain = createMiddlewareChain<{ value: number }>(); - chain.use(async (ctx, next) => { - ctx.count += 1; - return next(); - }); + chain.use(async (_ctx, next) => { + executed.push(1); + return next(); + }); + chain.use(async () => { + executed.push(2); + return { value: -1 }; + }); + chain.use(async (_ctx, next) => { + executed.push(3); + return next(); + }); - const resultA = await chain.execute({ count: 0 }); - const resultB = await chain.execute({ count: 0 }); + const result = await chain.execute({ value: 100 }); - expect(resultA.count).toBe(1); - expect(resultB.count).toBe(1); - }); + expect(result.value).toBe(-1); + expect(executed).toEqual([1, 2]); + }); + }); - it('should share middlewares across execute calls', async () => { - const chain = createMiddlewareChain<{ log: string[] }>(); + describe("nested next calls", () => { + it("should advance index on each next call, skipping remaining middlewares", async () => { + const chain = createMiddlewareChain<{ counter: number }>(); - chain.use(async (ctx, next) => { - ctx.log.push('always'); - return next(); - }); + chain.use(async (_ctx, next) => { + await next(); + await next(); + return undefined as unknown as { counter: number }; + }); - await chain.execute({ log: [] }); - await chain.execute({ log: [] }); + const result = await chain.execute({ counter: 0 }); - expect(chain).toBeDefined(); - }); + expect(result).toBeUndefined(); }); - describe('edge cases', () => { - it('should handle empty context object', async () => { - const chain = createMiddlewareChain>(); - const result = await chain.execute({}); + it("should allow middleware to call next conditionally", async () => { + const chain = createMiddlewareChain<{ skip: boolean; value: number }>(); - expect(result).toEqual({}); - }); + chain.use(async (ctx, next) => { + if (ctx.skip) { + return { skip: ctx.skip, value: -1 }; + } + return next(); + }); + chain.use(async (ctx, next) => { + ctx.value += 10; + return next(); + }); - it('should handle middleware that returns a completely different object', async () => { - const chain = createMiddlewareChain<{ x: number }, { y: string }>( - async () => ({ y: 'default' }) - ); + const resultA = await chain.execute({ skip: true, value: 0 }); + const resultB = await chain.execute({ skip: false, value: 0 }); - chain.use(async (_ctx, next) => { - return next(); - }); - - const result = await chain.execute({ x: 42 }); - - expect(result).toEqual({ y: 'default' }); - }); - - it('should handle middleware that mutates context without returning', async () => { - const chain = createMiddlewareChain<{ value: number }>( - async ctx => ctx - ); - - chain.use(async (ctx, next) => { - ctx.value = 100; - return next(); - }); - - const result = await chain.execute({ value: 0 }); - - expect(result.value).toBe(100); - }); - - it('should return undefined when middleware does not call next or return', async () => { - const chain = createMiddlewareChain<{ value: number }>(); - - chain.use(async (ctx) => { - ctx.value = 100; - }); - - const result = await chain.execute({ value: 0 }); - - expect(result).toBeUndefined(); - }); + expect(resultA.value).toBe(-1); + expect(resultB.value).toBe(10); }); + + it("should handle middleware that awaits next multiple times with a fallback", async () => { + const log: string[] = []; + const chain = createMiddlewareChain<{ value: number }, string[]>( + async (_ctx) => log, + ); + + chain.use(async (_ctx, next) => { + log.push("before"); + await next(); + log.push("after-first"); + await next(); + log.push("after-second"); + return log; + }); + chain.use(async (_ctx, next) => { + log.push("mw2"); + return next(); + }); + + const result = await chain.execute({ value: 0 }); + + expect(result).toEqual(["before", "mw2", "after-first", "after-second"]); + }); + + it("should return fallback result on second next call when no more middlewares remain", async () => { + const chain = createMiddlewareChain<{ value: number }, number>( + async (ctx) => ctx.value * 10, + ); + + chain.use(async (_ctx, next) => { + await next(); + return await next(); + }); + chain.use(async (_ctx, next) => { + return next(); + }); + + const result = await chain.execute({ value: 5 }); + + expect(result).toBe(50); + }); + + it("should return fallback result on second next call when no more middlewares remain", async () => { + const chain = createMiddlewareChain<{ value: number }, number>( + async (ctx) => ctx.value * 10, + ); + + chain.use(async (_ctx, next) => { + await next(); + return await next(); + }); + chain.use(async (_ctx, next) => { + return next(); + }); + + const result = await chain.execute({ value: 5 }); + + expect(result).toBe(50); + }); + }); + + describe("async behavior", () => { + it("should handle async middlewares", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + + chain.use(async (ctx, next) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + ctx.value += 1; + return next(); + }); + chain.use(async (ctx, next) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + ctx.value += 2; + return next(); + }); + + const result = await chain.execute({ value: 0 }); + + expect(result.value).toBe(3); + }); + + it("should handle async fallback", async () => { + const chain = createMiddlewareChain<{ value: number }, number>( + async (ctx) => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return ctx.value * 10; + }, + ); + + const result = await chain.execute({ value: 5 }); + + expect(result).toBe(50); + }); + }); + + describe("error handling", () => { + it("should propagate errors from middleware", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + + chain.use(async () => { + throw new Error("middleware error"); + }); + + await expect(chain.execute({ value: 1 })).rejects.toThrow( + "middleware error", + ); + }); + + it("should propagate errors from fallback", async () => { + const chain = createMiddlewareChain<{ value: number }, number>( + async () => { + throw new Error("fallback error"); + }, + ); + + await expect(chain.execute({ value: 1 })).rejects.toThrow( + "fallback error", + ); + }); + + it("should allow middleware to catch errors from downstream", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + + chain.use(async (_ctx, next) => { + try { + return await next(); + } catch { + return { value: -1 }; + } + }); + chain.use(async () => { + throw new Error("downstream error"); + }); + + const result = await chain.execute({ value: 1 }); + + expect(result.value).toBe(-1); + }); + }); + + describe("return type override", () => { + it("should support different TReturn type than TContext", async () => { + const chain = createMiddlewareChain<{ name: string }, string>( + async (ctx) => `Hello, ${ctx.name}!`, + ); + + const result = await chain.execute({ name: "World" }); + + expect(result).toBe("Hello, World!"); + }); + + it("should allow middleware to transform return type", async () => { + const chain = createMiddlewareChain<{ items: number[] }, number>( + async (ctx) => ctx.items.reduce((a, b) => a + b, 0), + ); + + chain.use(async (_ctx, next) => { + const sum = await next(); + return sum * 2; + }); + + const result = await chain.execute({ items: [1, 2, 3] }); + + expect(result).toBe(12); + }); + }); + + describe("reusability", () => { + it("should reset index on each execute call", async () => { + const chain = createMiddlewareChain<{ count: number }>(); + + chain.use(async (ctx, next) => { + ctx.count += 1; + return next(); + }); + + const resultA = await chain.execute({ count: 0 }); + const resultB = await chain.execute({ count: 0 }); + + expect(resultA.count).toBe(1); + expect(resultB.count).toBe(1); + }); + + it("should share middlewares across execute calls", async () => { + const chain = createMiddlewareChain<{ log: string[] }>(); + + chain.use(async (ctx, next) => { + ctx.log.push("always"); + return next(); + }); + + await chain.execute({ log: [] }); + await chain.execute({ log: [] }); + + expect(chain).toBeDefined(); + }); + }); + + describe("edge cases", () => { + it("should handle empty context object", async () => { + const chain = createMiddlewareChain>(); + const result = await chain.execute({}); + + expect(result).toEqual({}); + }); + + it("should handle middleware that returns a completely different object", async () => { + const chain = createMiddlewareChain<{ x: number }, { y: string }>( + async () => ({ y: "default" }), + ); + + chain.use(async (_ctx, next) => { + return next(); + }); + + const result = await chain.execute({ x: 42 }); + + expect(result).toEqual({ y: "default" }); + }); + + it("should handle middleware that mutates context without returning", async () => { + const chain = createMiddlewareChain<{ value: number }>( + async (ctx) => ctx, + ); + + chain.use(async (ctx, next) => { + ctx.value = 100; + return next(); + }); + + const result = await chain.execute({ value: 0 }); + + expect(result.value).toBe(100); + }); + + it("should return undefined when middleware does not call next or return", async () => { + const chain = createMiddlewareChain<{ value: number }>(); + + chain.use(async (ctx) => { + ctx.value = 100; + return undefined as unknown as { value: number }; + }); + + const result = await chain.execute({ value: 0 }); + + expect(result).toBeUndefined(); + }); + }); });