import { expect, test } from "vitest" import { Either, gen, genAsync, genAwait, genSync, runAsyncGenerator, runSyncGenerator, } from "#Source/result/index.ts" const throwCleanupError = (): never => { throw new Error("cleanup") } test("runSyncGenerator returns the first yielded Left, the final Right, and wraps sync failures", () => { let internalFinallyCalled = false const success = runSyncGenerator( (function* () { const value = yield* Either.right(1) return Either.right(value + 1) })(), ) const failure = runSyncGenerator( (function* () { try { yield* Either.right(1) yield* Either.left("stop") return Either.right(999) } finally { internalFinallyCalled = true } })(), ) expect(success.isRight()).toBe(true) expect(success.getRight()).toBe(2) expect(failure.isLeft()).toBe(true) expect(failure.getLeft()).toBe("stop") expect(internalFinallyCalled).toBe(true) expect(() => runSyncGenerator( (function* () { throw new Error("boom") })(), ), ).toThrow("Generator threw an exception") expect(() => runSyncGenerator( (function* () { yield "invalid" as unknown as Either return Either.right(1) })(), ), ).toThrow("Generator yielded a non-Either value") expect(() => runSyncGenerator( (function* () { try { yield Either.left("stop") return Either.right(1) } finally { throwCleanupError() } })(), ), ).toThrow("Generator threw an exception during cleanup") }) test("runAsyncGenerator returns the first yielded Left, the final Right, and wraps async failures", async () => { let internalFinallyCalled = false const success = await runAsyncGenerator( (async function* () { const value = yield* Either.right(1) return await Promise.resolve(Either.right(value + 1)) })(), ) const failure = await runAsyncGenerator( (async function* () { try { yield* Either.right(1) yield* Either.left("stop") return await Promise.resolve(Either.right(999)) } finally { internalFinallyCalled = true } })(), ) expect(success.isRight()).toBe(true) expect(success.getRight()).toBe(2) expect(failure.isLeft()).toBe(true) expect(failure.getLeft()).toBe("stop") expect(internalFinallyCalled).toBe(true) await expect( runAsyncGenerator( (async function* () { await Promise.resolve() throw new Error("boom") })(), ), ).rejects.toThrow("Generator threw an exception") await expect( runAsyncGenerator( (async function* () { await Promise.resolve() yield "invalid" as unknown as Either return Either.right(1) })(), ), ).rejects.toThrow("Generator yielded a non-Either value") await expect( runAsyncGenerator( (async function* () { try { await Promise.resolve() yield Either.left("stop") return Either.right(1) } finally { throwCleanupError() } })(), ), ).rejects.toThrow("Generator threw an exception during cleanup") }) test("genSync composes Either values with sync generators and keeps yielded Left unions", () => { const getA = (): Either<"a", number> => Either.right(1) const getB = (value: number): Either<"b", number> => Either.right(value + 1) const getC = (value: number): Either<"c", number> => Either.right(value + 1) const context = { offset: 4, *build(this: { offset: number }) { const value = yield* Either.right<"ctx", number>(this.offset) return Either.right(value + 1) }, } const success = genSync(function* () { const a = yield* getA() const b = yield* getB(a) const c = yield* getC(b) return Either.right(c) }) const thisBound = genSync(context.build, context) const failure = genSync(function* () { yield* getA() yield* Either.left<"stop", number>("stop") return Either.right(999) }) const expected: Either<"a" | "b" | "c", number> = success const actual: typeof success = expected const thisExpected: Either<"ctx", number> = thisBound const thisActual: typeof thisBound = thisExpected expect(actual).toBe(success) expect(thisActual).toBe(thisBound) expect(success.isRight()).toBe(true) expect(success.getRight()).toBe(3) expect(thisBound.isRight()).toBe(true) expect(thisBound.getRight()).toBe(5) expect(failure.isLeft()).toBe(true) expect(failure.getLeft()).toBe("stop") }) test("genAsync composes Either values with async generators and keeps yielded Left unions", async () => { const getA = (): Either<"a", number> => Either.right(1) const getB = (value: number): Either<"b", number> => Either.right(value + 1) const getC = (value: number): Either<"c", number> => Either.right(value + 1) const context = { offset: 5, async *build(this: { offset: number }) { const value = yield* Either.right<"ctx", number>(this.offset) return await Promise.resolve(Either.right(value + 1)) }, } const success = genAsync(async function* () { const a = yield* getA() const b = yield* getB(a) const c = yield* getC(b) return await Promise.resolve(Either.right(c)) }) const thisBound = genAsync(context.build, context) const failure = genAsync(async function* () { yield* getA() yield* Either.left<"stop", number>("stop") return await Promise.resolve(Either.right(999)) }) const expected: Promise> = success const actual: typeof success = expected const thisExpected: Promise> = thisBound const thisActual: typeof thisBound = thisExpected const resolvedSuccess = await success const resolvedThisBound = await thisBound const resolvedFailure = await failure expect(actual).toBe(success) expect(thisActual).toBe(thisBound) expect(resolvedSuccess.isRight()).toBe(true) expect(resolvedSuccess.getRight()).toBe(3) expect(resolvedThisBound.isRight()).toBe(true) expect(resolvedThisBound.getRight()).toBe(6) expect(resolvedFailure.isLeft()).toBe(true) expect(resolvedFailure.getLeft()).toBe("stop") }) test("gen dispatches sync and async generators while preserving inferred Left unions", async () => { const getA = (): Either<"a", number> => Either.right(1) const getB = (value: number): Either<"b", number> => Either.right(value + 1) const syncContext = { offset: 7, *build(this: { offset: number }) { const value = yield* Either.right<"ctx-sync", number>(this.offset) return Either.right(value + 1) }, } const asyncContext = { offset: 8, async *build(this: { offset: number }) { const value = yield* Either.right<"ctx-async", number>(this.offset) return await Promise.resolve(Either.right(value + 1)) }, } const sync = gen(function* () { const a = yield* getA() const b = yield* getB(a) return Either.right(b) }) const syncWithThis = gen(syncContext.build, syncContext) const asyncResult = gen(async function* () { const a = yield* getA() const b = yield* getB(a) return await Promise.resolve(Either.right(b)) }) const asyncWithThis = gen(asyncContext.build, asyncContext) const syncExpected: Either<"a" | "b", number> = sync const syncActual: typeof sync = syncExpected const syncWithThisExpected: Either<"ctx-sync", number> = syncWithThis const syncWithThisActual: typeof syncWithThis = syncWithThisExpected const asyncExpected: Promise> = asyncResult const asyncActual: typeof asyncResult = asyncExpected const asyncWithThisExpected: Promise> = asyncWithThis const asyncWithThisActual: typeof asyncWithThis = asyncWithThisExpected const resolvedAsyncResult = await asyncResult const resolvedAsyncWithThis = await asyncWithThis expect(syncActual).toBe(sync) expect(syncWithThisActual).toBe(syncWithThis) expect(asyncActual).toBe(asyncResult) expect(asyncWithThisActual).toBe(asyncWithThis) expect(sync.isRight()).toBe(true) expect(sync.getRight()).toBe(2) expect(syncWithThis.isRight()).toBe(true) expect(syncWithThis.getRight()).toBe(8) expect(asyncResult).toBeInstanceOf(Promise) expect(asyncWithThis).toBeInstanceOf(Promise) expect(resolvedAsyncResult.isRight()).toBe(true) expect(resolvedAsyncResult.getRight()).toBe(2) expect(resolvedAsyncWithThis.isRight()).toBe(true) expect(resolvedAsyncWithThis.getRight()).toBe(9) }) test("genAwait composes promised Either values inside async generators", async () => { const fetchUser = async (id: string): Promise> => { return await Promise.resolve(Either.right({ id })) } const fetchMissingProfile = async ( _id: string, ): Promise> => { return await Promise.resolve(Either.left("profile-missing")) } const enrichUserData = (user: { id: string }): Either<"enrich-failed", { id: string; kind: "enriched" }> => { return Either.right({ id: user.id, kind: "enriched" }) } const success = gen(async function* () { const validated = yield* Either.right<"invalid", { id: string }>({ id: "123" }) const user = yield* genAwait(fetchUser(validated.id)) const enriched = yield* enrichUserData(user) return Either.right(enriched) }) const failure = gen(async function* () { const validated = yield* Either.right<"invalid", { id: string }>({ id: "123" }) yield* genAwait(fetchMissingProfile(validated.id)) return Either.right("unreachable") }) const successExpected: Promise< Either<"invalid" | "not-found" | "enrich-failed", { id: string; kind: "enriched" }> > = success const successActual: typeof success = successExpected const failureExpected: Promise> = failure const failureActual: typeof failure = failureExpected const resolvedSuccess = await success const resolvedFailure = await failure expect(successActual).toBe(success) expect(failureActual).toBe(failure) expect(resolvedSuccess.isRight()).toBe(true) expect(resolvedSuccess.getRight()).toEqual({ id: "123", kind: "enriched" }) expect(resolvedFailure.isLeft()).toBe(true) expect(resolvedFailure.getLeft()).toBe("profile-missing") })