import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { LuaEngine, createLuaEngine, type LuaExecutionContext } from './lua-engine' describe('lua-engine', () => { let engine: LuaEngine beforeEach(() => { engine = createLuaEngine() }) afterEach(() => { engine.destroy() }) describe('createLuaEngine', () => { it('should create a new LuaEngine instance', () => { const newEngine = createLuaEngine() expect(newEngine).toBeInstanceOf(LuaEngine) newEngine.destroy() }) }) describe('execute', () => { describe('basic operations', () => { it.each([ { name: 'return number', code: 'return 42', expected: 42 }, { name: 'return string', code: 'return "hello"', expected: 'hello' }, { name: 'return boolean true', code: 'return true', expected: true }, { name: 'return boolean false', code: 'return false', expected: false }, { name: 'return nil', code: 'return nil', expected: null }, { name: 'arithmetic', code: 'return 10 + 5 * 2', expected: 20 }, { name: 'string concatenation', code: 'return "hello" .. " " .. "world"', expected: 'hello world' }, { name: 'comparison', code: 'return 5 > 3', expected: true }, ])('should execute $name', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.success).toBe(true) expect(result.result).toBe(expected) expect(result.error).toBeUndefined() }) }) describe('table handling', () => { it.each([ { name: 'array-like table returns object with numeric keys', code: 'return {1, 2, 3, 4, 5}', // Note: Lua tables with numeric keys are returned as objects with string keys // This is a known limitation of the current implementation expected: { '1': 1, '2': 2, '3': 3, '4': 4, '5': 5 }, }, { name: 'object-like table', code: 'return {name = "Alice", age = 30}', expected: { name: 'Alice', age: 30 }, }, { name: 'nested table', code: 'return {user = {name = "Bob"}, count = 1}', expected: { user: { name: 'Bob' }, count: 1 }, }, { name: 'mixed table with nested numeric keys', code: 'return {items = {1, 2, 3}, total = 6}', // Nested array-like tables also become objects with numeric string keys expected: { items: { '1': 1, '2': 2, '3': 3 }, total: 6 }, }, { name: 'empty table', code: 'return {}', expected: {}, }, ])('should handle $name', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.success).toBe(true) expect(result.result).toEqual(expected) }) }) describe('context data', () => { it.each([ { name: 'access context.data number', code: 'return context.data.value * 2', context: { data: { value: 21 } }, expected: 42, }, { name: 'access context.data string', code: 'return context.data.name', context: { data: { name: 'test' } }, expected: 'test', }, { name: 'access context.data boolean', code: 'return context.data.active', context: { data: { active: true } }, expected: true, }, { name: 'access nested context.data', code: 'return context.data.user.name', context: { data: { user: { name: 'Alice' } } }, expected: 'Alice', }, { name: 'access context.data array', code: 'return context.data.items[2]', context: { data: { items: [10, 20, 30] } }, expected: 20, }, ])('should $name', async ({ code, context, expected }) => { const result = await engine.execute(code, context) expect(result.success).toBe(true) expect(result.result).toBe(expected) }) it('should access context.user', async () => { const result = await engine.execute( 'return context.user.id', { user: { id: 'user123', name: 'Test' } } ) expect(result.success).toBe(true) expect(result.result).toBe('user123') }) }) describe('functions', () => { it.each([ { name: 'simple function', code: ` function add(a, b) return a + b end return add(3, 4) `, expected: 7, }, { name: 'recursive function', code: ` function factorial(n) if n <= 1 then return 1 end return n * factorial(n - 1) end return factorial(5) `, expected: 120, }, { name: 'higher-order function', code: ` function apply(f, x) return f(x) end function double(n) return n * 2 end return apply(double, 10) `, expected: 20, }, { name: 'closure', code: ` function counter() local count = 0 return function() count = count + 1 return count end end local c = counter() c() c() return c() `, expected: 3, }, ])('should execute $name', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.success).toBe(true) expect(result.result).toBe(expected) }) }) describe('control flow', () => { it.each([ { name: 'if-then-else true branch', code: ` local x = 10 if x > 5 then return "big" else return "small" end `, expected: 'big', }, { name: 'if-then-else false branch', code: ` local x = 3 if x > 5 then return "big" else return "small" end `, expected: 'small', }, { name: 'for loop', code: ` local sum = 0 for i = 1, 5 do sum = sum + i end return sum `, expected: 15, }, { name: 'while loop', code: ` local count = 0 local n = 10 while n > 0 do count = count + 1 n = n - 2 end return count `, expected: 5, }, ])('should execute $name', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.success).toBe(true) expect(result.result).toBe(expected) }) }) describe('logging', () => { it('should capture log() calls', async () => { const result = await engine.execute(` log("message 1") log("message 2") return "done" `) expect(result.success).toBe(true) expect(result.logs).toContain('message 1') expect(result.logs).toContain('message 2') }) it('should capture print() calls', async () => { const result = await engine.execute(` print("printed output") return true `) expect(result.success).toBe(true) expect(result.logs).toContain('printed output') }) it.each([ { name: 'number', code: 'log(42)', expected: '42' }, { name: 'boolean', code: 'log(true)', expected: 'true' }, { name: 'multiple args', code: 'log("a", "b", "c")', expected: 'a b c' }, ])('should log $name correctly', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.logs).toContain(expected) }) }) describe('error handling', () => { it.each([ { name: 'syntax error', code: 'this is not valid lua !!!', errorContains: 'Syntax error', }, { name: 'undefined variable', code: 'return undefinedVar.property', errorContains: 'Runtime error', }, { name: 'type error', code: 'return "string" + 5', errorContains: 'error', }, { name: 'explicit error', code: 'error("intentional error")', errorContains: 'intentional error', }, ])('should handle $name', async ({ code, errorContains }) => { const result = await engine.execute(code) expect(result.success).toBe(false) expect(result.error?.toLowerCase()).toContain(errorContains.toLowerCase()) }) }) describe('multiple return values', () => { it('should handle multiple return values', async () => { const result = await engine.execute('return 1, 2, 3') expect(result.success).toBe(true) expect(result.result).toEqual([1, 2, 3]) }) it('should handle function with multiple returns', async () => { const result = await engine.execute(` function swap(a, b) return b, a end return swap(1, 2) `) expect(result.success).toBe(true) expect(result.result).toEqual([2, 1]) }) }) describe('no return value', () => { it('should handle code with no return', async () => { const result = await engine.execute('local x = 1 + 1') expect(result.success).toBe(true) expect(result.result).toBeNull() }) }) describe('standard library', () => { it.each([ { name: 'math.floor', code: 'return math.floor(3.7)', expected: 3 }, { name: 'math.ceil', code: 'return math.ceil(3.2)', expected: 4 }, { name: 'math.abs', code: 'return math.abs(-5)', expected: 5 }, { name: 'math.max', code: 'return math.max(1, 5, 3)', expected: 5 }, { name: 'math.min', code: 'return math.min(1, 5, 3)', expected: 1 }, { name: 'string.upper', code: 'return string.upper("hello")', expected: 'HELLO' }, { name: 'string.lower', code: 'return string.lower("HELLO")', expected: 'hello' }, { name: 'string.len', code: 'return string.len("test")', expected: 4 }, { name: 'string.sub', code: 'return string.sub("hello", 2, 4)', expected: 'ell' }, { name: 'table.concat', code: 'return table.concat({"a", "b", "c"}, ", ")', expected: 'a, b, c' }, ])('should support $name', async ({ code, expected }) => { const result = await engine.execute(code) expect(result.success).toBe(true) expect(result.result).toBe(expected) }) }) }) describe('destroy', () => { it('should cleanup without error', () => { const testEngine = createLuaEngine() expect(() => testEngine.destroy()).not.toThrow() }) }) })