import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, } from 'bun:test' import * as F from '../src' // Mock Bun environment beforeAll(() => { Bun.env.TEST_PORT = '3000' Bun.env.EMPTY_VAR = '' }) afterEach(() => { delete Bun.env.MISSING_VAR }) describe('Option Module', () => { test('creates Some and None', () => { expect(F.some(5)).toEqual({_tag: 'Some', value: 5}) expect(F.none).toEqual({_tag: 'None'}) }) test('fromNullable handles null/undefined', () => { expect(F.fromNullable(null)).toEqual(F.none) expect(F.fromNullable(undefined)).toEqual(F.none) expect(F.fromNullable(10)).toEqual(F.some(10)) expect(F.fromNullable(0)).toEqual(F.some(0)) // Edge: falsy but not null expect(F.fromNullable('')).toEqual(F.some('')) // Edge: empty string }) test('map transforms values', () => { const double = (n: number) => n * 2 expect(F.map(double)(F.some(5))).toEqual(F.some(10)) expect(F.map(double)(F.none)).toEqual(F.none) expect(F.map(() => null)(F.some(5))).toEqual(F.some(null)) // Edge: mapping to null }) test('chain handles nested options', () => { const safeInverse = (n: number): F.Option => n === 0 ? F.none : F.some(1 / n) expect(F.chain(safeInverse)(F.some(2))).toEqual(F.some(0.5)) expect(F.chain(safeInverse)(F.some(0))).toEqual(F.none) // Edge: division by zero expect(F.chain(safeInverse)(F.none)).toEqual(F.none) }) test('getOrElse provides defaults', () => { const withDefault = F.getOrElse(0) expect(withDefault(F.some(42))).toBe(42) expect(withDefault(F.none)).toBe(0) expect(withDefault(F.some(null))).toBe(null) // Edge: null value }) test('DoOption composition', () => { const result = F.DoOption.start() .bind('a', F.some(3)) .bind('b', F.some(4)) .return(scope => scope.a * scope.b) expect(result).toEqual(F.some(12)) }) test('DoOption.bindL uses scope for computation', () => { const getUser = (id: number): F.Option => id === 1 ? F.some('Alice') : F.none const result = F.DoOption.start() .bind('id', F.some(1)) .bindL('name', scope => getUser(scope.id)) .return(scope => `User: ${scope.name}`) expect(result).toEqual(F.some('User: Alice')) }) test('DoOption.bindL short-circuits on None', () => { const getUser = (id: number): F.Option => id === 1 ? F.some('Alice') : F.none const result = F.DoOption.start() .bind('id', F.some(2)) // Invalid ID .bindL('name', scope => getUser(scope.id)) .bind('age', F.some(30)) // Shouldn't execute .return(scope => `User: ${scope.name}, Age: ${scope.age}`) expect(result).toEqual(F.none) }) test('DoOption.bindL handles complex dependencies', () => { const getProfile = (user: string): F.Option<{age: number}> => user === 'Alice' ? F.some({age: 30}) : F.none const result = F.DoOption.start() .bind('id', F.some(1)) .bindL('name', scope => scope.id === 1 ? F.some('Alice') : F.none) .bindL('profile', scope => getProfile(scope.name)) .return(scope => `${scope.name} is ${scope.profile.age} years old`) expect(result).toEqual(F.some('Alice is 30 years old')) }) test('DoOption.doL executes scoped effects', () => { const log = (msg: string): F.Option => F.some(undefined) let loggedMessage = '' const result = F.DoOption.start() .bind('id', F.some(1)) .doL(scope => { loggedMessage = `Processing user ${scope.id}` return log(loggedMessage) }) .bind('name', F.some('Alice')) .return(scope => scope.name) expect(result).toEqual(F.some('Alice')) expect(loggedMessage).toBe('Processing user 1') }) test('DoOption.doL short-circuits on None', () => { let effectRun = false const result = F.DoOption.start() .bind('id', F.some(1)) .doL(scope => { effectRun = true return scope.id === 1 ? F.none : F.some(undefined) }) .bind('name', F.some('Alice')) // Shouldn't execute .return(scope => scope.name) expect(result).toEqual(F.none) expect(effectRun).toBe(true) }) test('DoOption.doL with multiple effects', () => { const effects: string[] = [] const result = F.DoOption.start() .bind('id', F.some(1)) .doL(scope => { effects.push(`Starting processing for ${scope.id}`) return F.some(undefined) }) .bindL('name', scope => { effects.push(`Fetching name for ${scope.id}`) return scope.id === 1 ? F.some('Alice') : F.none }) .doL(scope => { effects.push(`Completed processing for ${scope.name}`) return F.some(undefined) }) .return(scope => scope.name) expect(result).toEqual(F.some('Alice')) expect(effects).toEqual([ 'Starting processing for 1', 'Fetching name for 1', 'Completed processing for Alice', ]) }) test('DoOption short-circuits on None', () => { const result = F.DoOption.start() .bind('a', F.some(10)) .bind('b', F.none) .bind('c', F.some(30)) .return(scope => scope.a + scope.b + scope.c) expect(result).toEqual(F.none) }) test('AsyncDoOption handles promises', async () => { const fetchNumber = (n: number): Promise> => Promise.resolve(F.some(n * 2)) const result = await F.AsyncDoOption.start() .bind('a', fetchNumber(10)) .bind('b', fetchNumber(20)) .return(({a, b}) => a + b) expect(result).toEqual(F.some(60)) }) test('AsyncDoOption handles empty promise results', async () => { const emptyFetch = (): Promise> => Promise.resolve(F.none) const result = await F.AsyncDoOption.start() .bind('a', Promise.resolve(F.some(10))) .bind('b', emptyFetch()) .return(({a, b}) => a + b) expect(result).toEqual(F.none) }) test('AsyncDoOption handles promise rejections', async () => { const failingFetch = (): Promise> => Promise.reject(new Error('Failed')) const result = await F.AsyncDoOption.start() .bind('a', Promise.resolve(F.some(10))) .bind('b', failingFetch()) .return(({a, b}) => a + b) expect(result).toEqual(F.none) }) }) describe('Either Module', () => { test('creates Right and Left', () => { expect(F.right(5)).toEqual({_tag: 'Right', right: 5}) expect(F.left('error')).toEqual({_tag: 'Left', left: 'error'}) }) test('eitherMap transforms values', () => { const double = (n: number) => n * 2 expect(F.eitherMap(double)(F.right(5))).toEqual(F.right(10)) expect(F.eitherMap(double)(F.left('error'))).toEqual(F.left('error')) expect(F.eitherMap(() => null)(F.right(5))).toEqual(F.right(null)) // Edge: mapping to null }) test('eitherChain handles sequential operations', () => { const safeDivide = (a: number, b: number): F.Either => b === 0 ? F.left('Division by zero') : F.right(a / b) expect(F.eitherChain((n: number) => safeDivide(100, n))(F.right(2))) .toEqual(F.right(50)) expect(F.eitherChain((n: number) => safeDivide(100, n))(F.left('error'))) .toEqual(F.left('error')) expect(F.eitherChain(() => F.left('forced error'))(F.right(5))).toEqual( F.left('forced error'), ) }) test('fold handles both cases', () => { const handleSuccess = F.fold( (e: string) => `Error: ${e}`, (n: number) => `Result: ${n}`, ) expect(handleSuccess(F.right(42))).toBe('Result: 42') expect(handleSuccess(F.left('failure'))).toBe('Error: failure') expect(handleSuccess(F.right(0))).toBe('Result: 0') // Edge: falsy success value }) test('DoEither composition', () => { const result = F.DoEither.start() .bind('a', F.right(10)) .bind('b', F.right(20)) .return(({a, b}) => a + b) expect(result).toEqual(F.right(30)) }) test('DoEither short-circuits on failure', () => { const result = F.DoEither.start() .bind('a', F.right(10)) .bind('b', F.left('error')) .bind('c', F.right(30)) .return(({a, b, c}) => a + b + c) expect(result).toEqual(F.left('error')) }) test('DoEither handles effects', () => { const result = F.DoEither.start() .bind('a', F.right(10)) .do(F.right(undefined)) // Execute effect .bind('b', F.right(20)) .return(({a, b}) => a + b) expect(result).toEqual(F.right(30)) }) }) describe('List Module', () => { let list: F.List beforeEach(() => { list = F.cons(1, F.cons(2, F.cons(3, F.nil))) }) test('constructs lists properly', () => { expect(list._tag).toBe('Cons') expect(list.head).toBe(1) expect(list.tail._tag).toBe('Cons') expect(list.tail.head).toBe(2) expect(F.nil._tag).toBe('Nil') // Edge: empty list }) test('fromArray and toArray conversions', () => { const arr = [4, 5, 6] const newList = F.fromArray(arr) expect(F.toArray(newList)).toEqual(arr) expect(F.toArray(F.nil)).toEqual([]) // Edge: empty array }) test('listMap transforms elements', () => { const double = (n: number) => n * 2 const doubled = F.listMap(double)(list) expect(F.toArray(doubled)).toEqual([2, 4, 6]) // Edge: empty list expect(F.toArray(F.listMap(double)(F.nil))).toEqual([]) // Edge: mapping to different types const toString = F.listMap((n: number) => n.toString()) expect(F.toArray(toString(list))).toEqual(['1', '2', '3']) }) test('listReduce sums values', () => { const sum = F.listReduce((acc: number, val: number) => acc + val, 0)(list) expect(sum).toBe(6) // Edge: empty list expect(F.listReduce((acc, val) => acc + val, 0)(F.nil)).toBe(0) // Edge: different initial type const asString = F.listReduce((acc: string, val: number) => acc + val, '')( list, ) expect(asString).toBe('123') }) test('handles large lists', async () => { // Edge: performance test for large list const largeArray = Array.from({length: 10000}, (_, i) => i) const largeList = F.fromArray(largeArray) const sum = F.listReduce((acc, n) => acc + n, 0)(largeList) expect(sum).toBe(49995000) }) }) describe('Utility Functions', () => { test('pipe chains functions left-to-right', () => { const result = F.pipe( 2, n => n * 3, n => n + 1, n => `${n}`, ) expect(result).toBe('7') // Edge: single function expect(F.pipe(5, n => n * 2)).toBe(10) }) test('compose chains functions right-to-left', () => { const sum = (a: number) => (b: number) => a + b const func = F.compose( (s: string) => s.toUpperCase(), (n: number) => `Result: ${n}`, sum(2), ) expect(func(3)).toBe('RESULT: 5') // Edge: single function const double = F.compose((n: number) => n * 2) expect(double(5)).toBe(10) }) test('tryCatch captures errors', () => { const success = F.tryCatch(() => 42) const failure = F.tryCatch(() => { throw new Error('Failed') }) const throwsString = F.tryCatch(() => { throw 'String error' }) expect(success).toEqual(F.right(42)) expect(failure._tag).toBe('Left') expect((failure as any).left.message).toBe('Failed') expect(throwsString._tag).toBe('Left') expect((throwsString as any).left.message).toBe('String error') }) test('hasTag creates type guards', () => { const isCons = F.hasTag('Cons') const list = F.cons(1, F.nil) expect(isCons(list)).toBe(true) expect(isCons(F.nil)).toBe(false) expect(isCons(F.some(1))).toBe(false) // Different tagged type }) }) describe('Bun Extensions', () => { const TEST_FILE = 'test.txt' const JSON_FILE = 'data.json' beforeAll(async () => { // Create test files await Bun.write(TEST_FILE, 'Hello\nWorld\n') await Bun.write(JSON_FILE, '{"id": 1, "name": "Test"}') }) afterEach(async () => { // Clean up all test-generated files const filesToClean = [ 'output.txt', 'empty.txt', 'protected.txt', ] for (const file of filesToClean) { try { if (await Bun.file(file).exists()) { await Bun.rm(file) } } catch {} } }) test('readFileSafe reads files', async () => { const result = await F.bun.readFileSafe(TEST_FILE) expect(result).toEqual(F.right('Hello\nWorld\n')) }) test('readFileSafe handles missing files', async () => { const result = await F.bun.readFileSafe('missing.txt') expect(result._tag).toBe('Left') expect((result as any).left.message).toBe('File not found') }) test('readFileSafe handles directory paths', async () => { const result = await F.bun.readFileSafe('./') expect(result._tag).toBe('Left') expect((result as any).left).toBeInstanceOf(Error) }) test('safeJsonParse parses valid JSON', () => { const json = '{"id": 1, "name": "Test"}' const result = F.bun.safeJsonParse<{id: number, name: string}>(json) expect(result).toEqual(F.right({id: 1, name: 'Test'})) }) test('safeJsonParse handles invalid JSON', () => { const invalidJson = '{invalid}' const result = F.bun.safeJsonParse(invalidJson) expect(result._tag).toBe('Left') expect((result as any).left).toBeInstanceOf(Error) }) test('safeJsonParse handles empty strings', () => { const result = F.bun.safeJsonParse('') expect(result._tag).toBe('Left') expect((result as any).left).toBeInstanceOf(Error) }) test('asyncReduce processes lists', async () => { const list = F.fromArray([1, 2, 3]) const asyncSum = async (acc: number, val: number) => acc + val const result = await F.bun.asyncReduce(list, asyncSum, 0) expect(result).toBe(6) }) test('asyncReduce handles empty lists', async () => { const asyncSum = async (acc: number, val: number) => acc + val const result = await F.bun.asyncReduce(F.nil, asyncSum, 10) expect(result).toBe(10) }) test('envOption reads environment variables', () => { // Update: envOption should return some for empty strings const port = F.bun.envOption('TEST_PORT') const missing = F.bun.envOption('MISSING_VAR') const empty = F.bun.envOption('EMPTY_VAR') expect(port).toEqual(F.some('3000')) expect(missing).toEqual(F.none) expect(empty).toEqual(F.some('')) // Empty string should be Some("") }) test('listToFile writes lists to files', async () => { const list = F.fromArray(['Line 1', 'Line 2', 'Line 3']) const result = await F.bun.listToFile(list, 'output.txt') expect(result).toEqual(F.right(undefined)) const content = await Bun.file('output.txt').text() expect(content).toBe('Line 1\nLine 2\nLine 3\n') }) test('listToFile handles empty lists', async () => { // Use unique filename to prevent conflicts const result = await F.bun.listToFile(F.nil, 'empty.txt') expect(result).toEqual(F.right(undefined)) const content = await Bun.file('empty.txt').text() expect(content).toBe('') }) test('listToFile handles write errors', async () => { // Try to write to protected directory const list = F.fromArray(['Test']) const result = await F.bun.listToFile(list, '/root/test.txt') expect(result._tag).toBe('Left') expect((result as any).left).toBeInstanceOf(Error) }) test('createServer handles requests', async () => { const handler = (req: Request): F.Either => { return req.url.endsWith('/hello') ? F.right(new Response('Hello World')) : F.left(new Error('Not found')) } const server = F.bun.createServer(handler) // Test valid request const response1 = await server.fetch('http://localhost/hello') expect(await response1.text()).toBe('Hello World') // Test invalid request const response2 = await server.fetch('http://localhost/not-found') expect(response2.status).toBe(500) expect(await response2.text()).toBe('Server error') server.stop() }) test('createServer handles thrown errors', async () => { // This should be caught by the server's error handler const handler = () => { throw new Error('Unexpected error') } const server = F.bun.createServer(handler as any) const response = await server.fetch('http://localhost/') expect(response.status).toBe(500) expect(await response.text()).toBe('Internal error') server.stop() }) })