From c4e380c140b7bdfdfbc24ee7c6c310b3807fbfa4 Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:06:57 -0500 Subject: [PATCH] test: add tests for services in CoreModule --- .../src/services/AnomalyService.test.ts | 133 ++++++++++++ .../services/ClientOperationService.test.ts | 125 ++++++++++++ .../src/services/CommandService.test.ts | 120 +++++++++++ .../src/services/CommentService.test.ts | 193 ++++++++++++++++++ .../ConfigurableCountingService.test.ts | 80 ++++++++ .../src/services/ContextInitService.test.ts | 87 ++++++++ .../services/DetailProviderService.test.ts | 107 ++++++++++ src/backend/src/services/EventService.test.ts | 100 +++++++++ .../src/services/FeatureFlagService.test.ts | 84 ++++++++ .../src/services/HelloWorldService.test.ts | 42 ++++ .../src/services/HostnameService.test.ts | 47 +++++ src/backend/src/services/LockService.test.ts | 81 ++++++++ .../src/services/MemoryStorageService.test.ts | 94 +++++++++ .../src/services/NotificationService.test.ts | 164 +++++++++++++++ .../src/services/PuterVersionService.test.ts | 70 +++++++ .../src/services/RegistryService.test.ts | 98 +++++++++ .../src/services/ScriptService.test.ts | 182 +++++++++++++++++ .../src/services/ShutdownService.test.ts | 80 ++++++++ .../services/SystemValidationService.test.ts | 149 ++++++++++++++ src/backend/src/services/TraceService.test.ts | 64 ++++++ 20 files changed, 2100 insertions(+) create mode 100644 src/backend/src/services/AnomalyService.test.ts create mode 100644 src/backend/src/services/ClientOperationService.test.ts create mode 100644 src/backend/src/services/CommandService.test.ts create mode 100644 src/backend/src/services/CommentService.test.ts create mode 100644 src/backend/src/services/ConfigurableCountingService.test.ts create mode 100644 src/backend/src/services/ContextInitService.test.ts create mode 100644 src/backend/src/services/DetailProviderService.test.ts create mode 100644 src/backend/src/services/EventService.test.ts create mode 100644 src/backend/src/services/FeatureFlagService.test.ts create mode 100644 src/backend/src/services/HelloWorldService.test.ts create mode 100644 src/backend/src/services/HostnameService.test.ts create mode 100644 src/backend/src/services/LockService.test.ts create mode 100644 src/backend/src/services/MemoryStorageService.test.ts create mode 100644 src/backend/src/services/NotificationService.test.ts create mode 100644 src/backend/src/services/PuterVersionService.test.ts create mode 100644 src/backend/src/services/RegistryService.test.ts create mode 100644 src/backend/src/services/ScriptService.test.ts create mode 100644 src/backend/src/services/ShutdownService.test.ts create mode 100644 src/backend/src/services/SystemValidationService.test.ts create mode 100644 src/backend/src/services/TraceService.test.ts diff --git a/src/backend/src/services/AnomalyService.test.ts b/src/backend/src/services/AnomalyService.test.ts new file mode 100644 index 000000000..ddcbbb489 --- /dev/null +++ b/src/backend/src/services/AnomalyService.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { AnomalyService, DENY_SERVICE_INSTRUCTION } from './AnomalyService'; + +describe('AnomalyService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'anomaly': AnomalyService, + }, + initLevelString: 'init', + }); + + const anomalyService = testKernel.services!.get('anomaly') as any; + + it('should be instantiated', () => { + expect(anomalyService).toBeInstanceOf(AnomalyService); + }); + + it('should have types object', () => { + expect(anomalyService.types).toBeDefined(); + expect(typeof anomalyService.types).toBe('object'); + }); + + it('should register a type with handler', () => { + const handler = vi.fn(); + anomalyService.register('test-type', { handler }); + + expect(anomalyService.types['test-type']).toBeDefined(); + expect(anomalyService.types['test-type'].handler).toBe(handler); + }); + + it('should register a type with threshold', () => { + anomalyService.register('threshold-type', { high: 100 }); + + expect(anomalyService.types['threshold-type']).toBeDefined(); + expect(anomalyService.types['threshold-type'].handler).toBeDefined(); + expect(typeof anomalyService.types['threshold-type'].handler).toBe('function'); + }); + + it('should call handler when noting anomaly', async () => { + const handler = vi.fn().mockReturnValue('result'); + anomalyService.register('callable-type', { handler }); + + const data = { test: 'data' }; + const result = await anomalyService.note('callable-type', data); + + expect(handler).toHaveBeenCalledWith(data); + expect(result).toBe('result'); + }); + + it('should return undefined for unregistered type', async () => { + const result = await anomalyService.note('non-existent-type', {}); + + expect(result).toBeUndefined(); + }); + + it('should trigger threshold handler when value exceeds high', async () => { + anomalyService.register('high-threshold', { high: 50 }); + + const result = await anomalyService.note('high-threshold', { value: 75 }); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Set); + expect(result.has(DENY_SERVICE_INSTRUCTION)).toBe(true); + }); + + it('should not trigger threshold handler when value is below high', async () => { + anomalyService.register('low-threshold', { high: 100 }); + + const result = await anomalyService.note('low-threshold', { value: 50 }); + + expect(result).toBeUndefined(); + }); + + it('should handle multiple type registrations', () => { + anomalyService.register('type1', { handler: () => {} }); + anomalyService.register('type2', { high: 100 }); + anomalyService.register('type3', { handler: () => {} }); + + expect(anomalyService.types['type1']).toBeDefined(); + expect(anomalyService.types['type2']).toBeDefined(); + expect(anomalyService.types['type3']).toBeDefined(); + }); + + it('should store config in type instance', () => { + const config = { high: 200, custom: 'value' }; + anomalyService.register('config-type', config); + + expect(anomalyService.types['config-type'].config).toBe(config); + }); + + it('should handle exact threshold value', async () => { + anomalyService.register('exact-threshold', { high: 100 }); + + const result = await anomalyService.note('exact-threshold', { value: 100 }); + + // Threshold uses > not >=, so equal should not trigger + expect(result).toBeUndefined(); + }); + + it('should handle value just over threshold', async () => { + anomalyService.register('just-over', { high: 100 }); + + const result = await anomalyService.note('just-over', { value: 100.1 }); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Set); + expect(result.has(DENY_SERVICE_INSTRUCTION)).toBe(true); + }); + + it('should allow custom handler to return any value', async () => { + const customResult = { custom: 'result', data: [1, 2, 3] }; + anomalyService.register('custom-return', { + handler: () => customResult + }); + + const result = await anomalyService.note('custom-return', {}); + + expect(result).toBe(customResult); + }); +}); + +describe('DENY_SERVICE_INSTRUCTION', () => { + it('should be a symbol', () => { + expect(typeof DENY_SERVICE_INSTRUCTION).toBe('symbol'); + }); + + it('should be unique', () => { + const anotherSymbol = Symbol('DENY_SERVICE_INSTRUCTION'); + expect(DENY_SERVICE_INSTRUCTION).not.toBe(anotherSymbol); + }); +}); + diff --git a/src/backend/src/services/ClientOperationService.test.ts b/src/backend/src/services/ClientOperationService.test.ts new file mode 100644 index 000000000..0c7e7be09 --- /dev/null +++ b/src/backend/src/services/ClientOperationService.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest'; +import { ClientOperationService } from './ClientOperationService'; + +describe('ClientOperationService', async () => { + // ClientOperationService doesn't extend BaseService, so we can't use init + // We need to create it directly + const services = { _instances: {} }; + const clientOperationService = new ClientOperationService({ services }); + + it('should be instantiated', () => { + expect(clientOperationService).toBeDefined(); + expect(clientOperationService.operations_).toBeDefined(); + }); + + it('should have operations array', () => { + expect(clientOperationService.operations_).toBeDefined(); + expect(Array.isArray(clientOperationService.operations_)).toBe(true); + }); + + it('should create operation with default parameters', async () => { + const tracker = await clientOperationService.add_operation({}); + + expect(tracker).toBeDefined(); + expect(tracker.name).toBe('untitled'); + expect(Array.isArray(tracker.tags)).toBe(true); + expect(tracker.tags.length).toBe(0); + expect(tracker.frame).toBe(null); + expect(tracker.metadata).toBeDefined(); + expect(typeof tracker.metadata).toBe('object'); + expect(Array.isArray(tracker.objects)).toBe(true); + }); + + it('should create operation with name', async () => { + const tracker = await clientOperationService.add_operation({ + name: 'test-operation', + }); + + expect(tracker.name).toBe('test-operation'); + }); + + it('should create operation with tags', async () => { + const tags = ['tag1', 'tag2', 'tag3']; + const tracker = await clientOperationService.add_operation({ + tags, + }); + + expect(tracker.tags).toEqual(tags); + }); + + it('should create operation with frame', async () => { + const frame = { type: 'test-frame' }; + const tracker = await clientOperationService.add_operation({ + frame, + }); + + expect(tracker.frame).toBe(frame); + }); + + it('should create operation with metadata', async () => { + const metadata = { key1: 'value1', key2: 'value2' }; + const tracker = await clientOperationService.add_operation({ + metadata, + }); + + expect(tracker.metadata).toEqual(metadata); + }); + + it('should create operation with objects', async () => { + const objects = [{ id: 1 }, { id: 2 }]; + const tracker = await clientOperationService.add_operation({ + objects, + }); + + expect(tracker.objects).toEqual(objects); + }); + + it('should create operation with all parameters', async () => { + const params = { + name: 'full-operation', + tags: ['full', 'test'], + frame: { type: 'frame' }, + metadata: { meta: 'data' }, + objects: [{ obj: 1 }], + }; + + const tracker = await clientOperationService.add_operation(params); + + expect(tracker.name).toBe(params.name); + expect(tracker.tags).toEqual(params.tags); + expect(tracker.frame).toBe(params.frame); + expect(tracker.metadata).toEqual(params.metadata); + expect(tracker.objects).toEqual(params.objects); + }); + + it('should create multiple operations', async () => { + const tracker1 = await clientOperationService.add_operation({ name: 'op1' }); + const tracker2 = await clientOperationService.add_operation({ name: 'op2' }); + const tracker3 = await clientOperationService.add_operation({ name: 'op3' }); + + expect(tracker1.name).toBe('op1'); + expect(tracker2.name).toBe('op2'); + expect(tracker3.name).toBe('op3'); + }); + + it('should have ckey method', () => { + expect(clientOperationService.ckey).toBeDefined(); + expect(typeof clientOperationService.ckey).toBe('function'); + }); + + it('should generate context key with ckey', () => { + const key = clientOperationService.ckey('test-key'); + + expect(key).toBeDefined(); + expect(typeof key).toBe('string'); + expect(key).toContain('test-key'); + }); + + it('should generate different keys for different inputs', () => { + const key1 = clientOperationService.ckey('key1'); + const key2 = clientOperationService.ckey('key2'); + + expect(key1).not.toBe(key2); + }); +}); + diff --git a/src/backend/src/services/CommandService.test.ts b/src/backend/src/services/CommandService.test.ts new file mode 100644 index 000000000..3d4c02a39 --- /dev/null +++ b/src/backend/src/services/CommandService.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { CommandService } from './CommandService'; + +describe('CommandService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + commands: CommandService, + }, + initLevelString: 'init', + }); + + const commandService = testKernel.services!.get('commands') as CommandService; + + it('should be instantiated', () => { + expect(commandService).toBeInstanceOf(CommandService); + }); + + it('should have help command registered by default', () => { + expect(commandService.commandNames).toContain('help'); + }); + + it('should register commands', () => { + commandService.registerCommands('test-service', [ + { + id: 'test-cmd', + description: 'A test command', + handler: async () => {}, + }, + ]); + expect(commandService.commandNames).toContain('test-service:test-cmd'); + }); + + it('should execute registered commands', async () => { + let executed = false; + commandService.registerCommands('exec-test', [ + { + id: 'exec-cmd', + description: 'Execute test', + handler: async () => { executed = true; }, + }, + ]); + + const mockLog = { error: vi.fn(), log: vi.fn() }; + await commandService.executeCommand(['exec-test:exec-cmd'], mockLog); + expect(executed).toBe(true); + }); + + it('should pass arguments to command handler', async () => { + let receivedArgs: string[] = []; + commandService.registerCommands('args-test', [ + { + id: 'args-cmd', + description: 'Args test', + handler: async (args) => { receivedArgs = args; }, + }, + ]); + + const mockLog = { error: vi.fn(), log: vi.fn() }; + await commandService.executeCommand(['args-test:args-cmd', 'arg1', 'arg2'], mockLog); + expect(receivedArgs).toEqual(['arg1', 'arg2']); + }); + + it('should handle unknown commands', async () => { + const mockLog = { error: vi.fn(), log: vi.fn() }; + await commandService.executeCommand(['unknown-command'], mockLog); + expect(mockLog.error).toHaveBeenCalledWith('unknown command: unknown-command'); + }); + + it('should execute raw commands', async () => { + let executed = false; + commandService.registerCommands('raw-test', [ + { + id: 'raw-cmd', + description: 'Raw test', + handler: async () => { executed = true; }, + }, + ]); + + const mockLog = { error: vi.fn(), log: vi.fn() }; + await commandService.executeRawCommand('raw-test:raw-cmd', mockLog); + expect(executed).toBe(true); + }); + + it('should get command by id', () => { + commandService.registerCommands('get-test', [ + { + id: 'get-cmd', + description: 'Get test', + handler: async () => {}, + }, + ]); + + const cmd = commandService.getCommand('get-test:get-cmd'); + expect(cmd).toBeDefined(); + expect(cmd?.id).toBe('get-test:get-cmd'); + }); + + it('should execute help command', async () => { + const mockLog = { error: vi.fn(), log: vi.fn() }; + await commandService.executeCommand(['help'], mockLog); + expect(mockLog.log).toHaveBeenCalledWith('available commands:'); + }); + + it('should support command completers', () => { + commandService.registerCommands('complete-test', [ + { + id: 'complete-cmd', + description: 'Complete test', + handler: async () => {}, + completer: (args) => ['option1', 'option2'], + }, + ]); + + const cmd = commandService.getCommand('complete-test:complete-cmd'); + const completions = cmd?.completeArgument([]); + expect(completions).toEqual(['option1', 'option2']); + }); +}); + diff --git a/src/backend/src/services/CommentService.test.ts b/src/backend/src/services/CommentService.test.ts new file mode 100644 index 000000000..5f6a9bec5 --- /dev/null +++ b/src/backend/src/services/CommentService.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import * as config from '../config'; +import { CommentService } from './CommentService'; + +describe('CommentService', async () => { + config.load_config({ + 'services': { + 'database': { + path: ':memory:', + }, + }, + }); + + const testKernel = await createTestKernel({ + serviceMap: { + 'comment': CommentService, + }, + initLevelString: 'init', + testCore: true, + }); + + const commentService = testKernel.services!.get('comment') as any; + + it('should be instantiated', () => { + expect(commentService).toBeInstanceOf(CommentService); + }); + + it('should have db connection after init', () => { + expect(commentService.db).toBeDefined(); + }); + + it('should have uuidv4 module', () => { + expect(commentService.modules).toBeDefined(); + expect(commentService.modules.uuidv4).toBeDefined(); + expect(typeof commentService.modules.uuidv4).toBe('function'); + }); + + it('should have create_comment_ method', () => { + expect(commentService.create_comment_).toBeDefined(); + expect(typeof commentService.create_comment_).toBe('function'); + }); + + it('should have attach_comment_to_fsentry method', () => { + expect(commentService.attach_comment_to_fsentry).toBeDefined(); + expect(typeof commentService.attach_comment_to_fsentry).toBe('function'); + }); + + it('should have get_comments_for_fsentry method', () => { + expect(commentService.get_comments_for_fsentry).toBeDefined(); + expect(typeof commentService.get_comments_for_fsentry).toBe('function'); + }); + + it('should generate UUID for comments', () => { + const uuid1 = commentService.modules.uuidv4(); + const uuid2 = commentService.modules.uuidv4(); + + expect(uuid1).toBeDefined(); + expect(uuid2).toBeDefined(); + expect(typeof uuid1).toBe('string'); + expect(typeof uuid2).toBe('string'); + expect(uuid1).not.toBe(uuid2); + }); + + it('should validate UUID format', () => { + const uuid = commentService.modules.uuidv4(); + + // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + expect(uuid).toMatch(uuidRegex); + }); + + it('should create comment with text', async () => { + const mockReq = { + body: { text: 'Test comment text' }, + user: { id: 1 }, + }; + const mockRes = {}; + + // Mock database write + const originalWrite = commentService.db.write.bind(commentService.db); + commentService.db.write = vi.fn().mockResolvedValue({ insertId: 123 }); + + try { + const result = await commentService.create_comment_({ + req: mockReq, + res: mockRes + }); + + expect(result).toBeDefined(); + expect(result.id).toBe(123); + expect(result.uid).toBeDefined(); + expect(typeof result.uid).toBe('string'); + + // Verify database write was called with correct parameters + expect(commentService.db.write).toHaveBeenCalledWith( + expect.stringContaining('INSERT INTO `user_comments`'), + expect.arrayContaining([ + expect.any(String), // UUID + 1, // user_id + '{}', // metadata + 'Test comment text', + ]) + ); + } finally { + commentService.db.write = originalWrite; + } + }); + + it('should attach comment to fsentry', async () => { + const mockNode = { + get: vi.fn().mockResolvedValue(456), // mysql-id + }; + const comment = { + id: 123, + uid: 'comment-uuid', + }; + + const originalWrite = commentService.db.write.bind(commentService.db); + commentService.db.write = vi.fn().mockResolvedValue({}); + + try { + await commentService.attach_comment_to_fsentry({ + node: mockNode, + comment: comment, + }); + + expect(commentService.db.write).toHaveBeenCalledWith( + expect.stringContaining('INSERT INTO `user_fsentry_comments`'), + expect.arrayContaining([123, 456]) + ); + + expect(mockNode.get).toHaveBeenCalledWith('mysql-id'); + } finally { + commentService.db.write = originalWrite; + } + }); + + it('should call database to get comments for fsentry', async () => { + const mockNode = { + get: vi.fn().mockResolvedValue(789), + }; + + const originalRead = commentService.db.read.bind(commentService.db); + commentService.db.read = vi.fn().mockResolvedValue([]); + + try { + // Note: This test only verifies the database call structure + // Full integration tests would require proper user service setup + await commentService.get_comments_for_fsentry({ + node: mockNode, + }); + + expect(commentService.db.read).toHaveBeenCalledWith( + expect.stringContaining('SELECT * FROM `user_comments`'), + expect.arrayContaining([789]) + ); + + expect(mockNode.get).toHaveBeenCalledWith('mysql-id'); + } finally { + commentService.db.read = originalRead; + } + }); + + it('should handle multiple comment attachments', async () => { + const mockNode = { + get: vi.fn().mockResolvedValue(999), + }; + + const comments = [ + { id: 1, uid: 'uuid-1' }, + { id: 2, uid: 'uuid-2' }, + { id: 3, uid: 'uuid-3' }, + ]; + + const originalWrite = commentService.db.write.bind(commentService.db); + commentService.db.write = vi.fn().mockResolvedValue({}); + + try { + for (const comment of comments) { + await commentService.attach_comment_to_fsentry({ + node: mockNode, + comment, + }); + } + + expect(commentService.db.write).toHaveBeenCalledTimes(3); + } finally { + commentService.db.write = originalWrite; + } + }); +}); + diff --git a/src/backend/src/services/ConfigurableCountingService.test.ts b/src/backend/src/services/ConfigurableCountingService.test.ts new file mode 100644 index 000000000..9926b9011 --- /dev/null +++ b/src/backend/src/services/ConfigurableCountingService.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import * as config from '../config'; +import { ConfigurableCountingService } from './ConfigurableCountingService'; + +describe('ConfigurableCountingService', async () => { + config.load_config({ + 'services': { + 'database': { + path: ':memory:', + }, + }, + }); + + const testKernel = await createTestKernel({ + serviceMap: { + 'counting': ConfigurableCountingService, + }, + initLevelString: 'init', + testCore: true, + }); + + const countingService = testKernel.services!.get('counting') as ConfigurableCountingService; + + it('should be instantiated', () => { + expect(countingService).toBeInstanceOf(ConfigurableCountingService); + }); + + it('should have counting types defined', () => { + expect(ConfigurableCountingService.counting_types).toBeDefined(); + expect(ConfigurableCountingService.counting_types.gpt).toBeDefined(); + expect(ConfigurableCountingService.counting_types.dalle).toBeDefined(); + }); + + it('should have sql columns defined', () => { + expect(ConfigurableCountingService.sql_columns).toBeDefined(); + expect(ConfigurableCountingService.sql_columns.uint).toBeDefined(); + expect(ConfigurableCountingService.sql_columns.uint.length).toBe(3); + }); + + it('should validate GPT counting type structure', () => { + const gptType = ConfigurableCountingService.counting_types.gpt; + expect(gptType.category).toBeDefined(); + expect(gptType.values).toBeDefined(); + expect(gptType.category.length).toBeGreaterThan(0); + expect(gptType.values.length).toBeGreaterThan(0); + }); + + it('should validate DALL-E counting type structure', () => { + const dalleType = ConfigurableCountingService.counting_types.dalle; + expect(dalleType.category).toBeDefined(); + expect(dalleType.category.length).toBeGreaterThan(0); + expect(dalleType.category.some(c => c.name === 'model')).toBe(true); + expect(dalleType.category.some(c => c.name === 'quality')).toBe(true); + expect(dalleType.category.some(c => c.name === 'resolution')).toBe(true); + }); + + it('should have gpt token value definitions', () => { + const gptType = ConfigurableCountingService.counting_types.gpt; + expect(gptType.values.some(v => v.name === 'input_tokens')).toBe(true); + expect(gptType.values.some(v => v.name === 'output_tokens')).toBe(true); + expect(gptType.values.every(v => v.type === 'uint')).toBe(true); + }); + + it('should have available sql columns for uint type', () => { + const columns = ConfigurableCountingService.sql_columns.uint; + expect(columns).toBeDefined(); + expect(Array.isArray(columns)).toBe(true); + expect(columns.length).toBe(3); + expect(columns.every(col => typeof col === 'string')).toBe(true); + }); + + it('should have model category for gpt', () => { + const gptType = ConfigurableCountingService.counting_types.gpt; + const modelCategory = gptType.category.find(c => c.name === 'model'); + expect(modelCategory).toBeDefined(); + expect(modelCategory!.type).toBe('string'); + }); +}); + diff --git a/src/backend/src/services/ContextInitService.test.ts b/src/backend/src/services/ContextInitService.test.ts new file mode 100644 index 000000000..fb3acf836 --- /dev/null +++ b/src/backend/src/services/ContextInitService.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { ContextInitService } from './ContextInitService'; + +describe('ContextInitService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'context-init': ContextInitService, + }, + initLevelString: 'init', + }); + + const contextInitService = testKernel.services!.get('context-init') as any; + + it('should be instantiated', () => { + expect(contextInitService).toBeInstanceOf(ContextInitService); + }); + + it('should have middleware instance', () => { + expect(contextInitService.mw).toBeDefined(); + expect(contextInitService.mw.value_initializers_).toBeDefined(); + expect(Array.isArray(contextInitService.mw.value_initializers_)).toBe(true); + }); + + it('should register a value initializer', () => { + const initialLength = contextInitService.mw.value_initializers_.length; + + contextInitService.register_value('test-key', 'test-value'); + + expect(contextInitService.mw.value_initializers_.length).toBe(initialLength + 1); + }); + + it('should store key-value pair in initializer', () => { + const service = testKernel.services!.get('context-init') as any; + + service.register_value('stored-key', 'stored-value'); + + const lastInitializer = service.mw.value_initializers_[service.mw.value_initializers_.length - 1]; + expect(lastInitializer.key).toBe('stored-key'); + expect(lastInitializer.value).toBe('stored-value'); + }); + + it('should register async factory', () => { + const service = testKernel.services!.get('context-init') as any; + const initialLength = service.mw.value_initializers_.length; + + const factory = async () => 'async-value'; + service.register_async_factory('async-key', factory); + + expect(service.mw.value_initializers_.length).toBe(initialLength + 1); + }); + + it('should store async factory in initializer', () => { + const service = testKernel.services!.get('context-init') as any; + + const factory = async () => 'factory-result'; + service.register_async_factory('factory-key', factory); + + const lastInitializer = service.mw.value_initializers_[service.mw.value_initializers_.length - 1]; + expect(lastInitializer.key).toBe('factory-key'); + expect(lastInitializer.async_factory).toBe(factory); + }); + + it('should handle multiple value registrations', () => { + const service = testKernel.services!.get('context-init') as any; + + service.register_value('key1', 'value1'); + service.register_value('key2', 'value2'); + service.register_value('key3', 'value3'); + + const keys = service.mw.value_initializers_.map((init: any) => init.key); + expect(keys).toContain('key1'); + expect(keys).toContain('key2'); + expect(keys).toContain('key3'); + }); + + it('should have install method on middleware', () => { + expect(contextInitService.mw.install).toBeDefined(); + expect(typeof contextInitService.mw.install).toBe('function'); + }); + + it('should have run method on middleware', () => { + expect(contextInitService.mw.run).toBeDefined(); + expect(typeof contextInitService.mw.run).toBe('function'); + }); +}); + diff --git a/src/backend/src/services/DetailProviderService.test.ts b/src/backend/src/services/DetailProviderService.test.ts new file mode 100644 index 000000000..32ae245db --- /dev/null +++ b/src/backend/src/services/DetailProviderService.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { DetailProviderService } from './DetailProviderService'; + +describe('DetailProviderService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'detail-provider': DetailProviderService, + }, + initLevelString: 'init', + }); + + const detailProviderService = testKernel.services!.get('detail-provider') as any; + + it('should be instantiated', () => { + expect(detailProviderService).toBeInstanceOf(DetailProviderService); + }); + + it('should have empty providers array initially', () => { + expect(detailProviderService.providers_).toBeDefined(); + expect(Array.isArray(detailProviderService.providers_)).toBe(true); + }); + + it('should register a provider', () => { + const initialLength = detailProviderService.providers_.length; + const provider = async (context: any, out: any) => { + out.test = 'value'; + }; + + detailProviderService.register_provider(provider); + + expect(detailProviderService.providers_.length).toBe(initialLength + 1); + }); + + it('should get details with single provider', async () => { + const service = testKernel.services!.get('detail-provider') as any; + + service.register_provider(async (context: any, out: any) => { + out.name = context.input; + }); + + const result = await service.get_details({ input: 'test-name' }); + + expect(result.name).toBe('test-name'); + }); + + it('should get details with multiple providers', async () => { + const service = testKernel.services!.get('detail-provider') as any; + + service.register_provider(async (context: any, out: any) => { + out.field1 = 'value1'; + }); + + service.register_provider(async (context: any, out: any) => { + out.field2 = 'value2'; + }); + + const result = await service.get_details({}); + + expect(result.field1).toBe('value1'); + expect(result.field2).toBe('value2'); + }); + + it('should allow providers to modify existing output', async () => { + const service = testKernel.services!.get('detail-provider') as any; + + service.register_provider(async (context: any, out: any) => { + out.counter = 1; + }); + + service.register_provider(async (context: any, out: any) => { + out.counter = out.counter + 1; + }); + + const result = await service.get_details({}); + + expect(result.counter).toBe(2); + }); + + it('should use provided output object', async () => { + const service = testKernel.services!.get('detail-provider') as any; + + service.register_provider(async (context: any, out: any) => { + out.added = true; + }); + + const existingOut = { existing: 'value' }; + const result = await service.get_details({}, existingOut); + + expect(result.existing).toBe('value'); + expect(result.added).toBe(true); + }); + + it('should handle async providers', async () => { + const service = testKernel.services!.get('detail-provider') as any; + + service.register_provider(async (context: any, out: any) => { + await new Promise(resolve => setTimeout(resolve, 10)); + out.async = true; + }); + + const result = await service.get_details({}); + + expect(result.async).toBe(true); + }); +}); + diff --git a/src/backend/src/services/EventService.test.ts b/src/backend/src/services/EventService.test.ts new file mode 100644 index 000000000..37b2e6182 --- /dev/null +++ b/src/backend/src/services/EventService.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { EventService } from './EventService'; + +describe('EventService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'event-test': EventService, + }, + initLevelString: 'init', + }); + + const eventService = testKernel.services!.get('event-test') as EventService; + + it('should be instantiated', () => { + expect(eventService).toBeInstanceOf(EventService); + }); + + it('should emit and receive events', async () => { + let received = false; + eventService.on('test.event', () => { + received = true; + }); + + await eventService.emit('test.event', {}); + expect(received).toBe(true); + }); + + it('should pass data to event listeners', async () => { + let receivedData: any = null; + eventService.on('data.event', (key, data) => { + receivedData = data; + }); + + await eventService.emit('data.event', { value: 42 }); + expect(receivedData).toEqual({ value: 42 }); + }); + + it('should support wildcard listeners', async () => { + const received: string[] = []; + eventService.on('wild.*', (key) => { + received.push(key); + }); + + await eventService.emit('wild.test1', {}); + await eventService.emit('wild.test2', {}); + + expect(received).toContain('wild.test1'); + expect(received).toContain('wild.test2'); + }); + + it('should support multiple listeners on same event', async () => { + let count = 0; + eventService.on('multi.event', () => { count++; }); + eventService.on('multi.event', () => { count++; }); + + await eventService.emit('multi.event', {}); + expect(count).toBe(2); + }); + + it('should detach listeners', async () => { + let count = 0; + const det = eventService.on('detach.event', () => { count++; }); + + await eventService.emit('detach.event', {}); + expect(count).toBe(1); + + det.detach(); + await eventService.emit('detach.event', {}); + expect(count).toBe(1); // Should still be 1 + }); + + it('should support global listeners', async () => { + let globalReceived = false; + eventService.on_all(() => { + globalReceived = true; + }); + + await eventService.emit('any.event', {}); + expect(globalReceived).toBe(true); + }); + + it('should create scoped event bus', () => { + const scoped = eventService.get_scoped('test.scope'); + expect(scoped).toBeDefined(); + expect(scoped.scope).toBe('test.scope'); + }); + + it('should emit events through scoped bus', async () => { + let received = false; + eventService.on('scope.test.event', () => { + received = true; + }); + + const scoped = eventService.get_scoped('scope.test'); + await scoped.emit('event', {}); + expect(received).toBe(true); + }); +}); + diff --git a/src/backend/src/services/FeatureFlagService.test.ts b/src/backend/src/services/FeatureFlagService.test.ts new file mode 100644 index 000000000..42f0ca0ca --- /dev/null +++ b/src/backend/src/services/FeatureFlagService.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { FeatureFlagService } from './FeatureFlagService'; + +describe('FeatureFlagService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'feature-flag': FeatureFlagService, + }, + initLevelString: 'init', + testCore: true, + }); + + const featureFlagService = testKernel.services!.get('feature-flag') as FeatureFlagService; + + it('should be instantiated', () => { + expect(featureFlagService).toBeInstanceOf(FeatureFlagService); + }); + + it('should register feature flags', () => { + featureFlagService.register('test-flag', true); + expect(featureFlagService.known_flags.has('test-flag')).toBe(true); + }); + + it('should register config flags', () => { + featureFlagService.register('config-flag', { $: 'config-flag', value: true }); + expect(featureFlagService.known_flags.get('config-flag')).toEqual({ $: 'config-flag', value: true }); + }); + + it('should check config flags', async () => { + featureFlagService.register('enabled-flag', { $: 'config-flag', value: true }); + const result = await featureFlagService.check('enabled-flag'); + expect(result).toBe(true); + }); + + it('should check disabled config flags', async () => { + featureFlagService.register('disabled-flag', { $: 'config-flag', value: false }); + const result = await featureFlagService.check('disabled-flag'); + expect(result).toBe(false); + }); + + it('should register function flags', () => { + featureFlagService.register('fn-flag', { + $: 'function-flag', + fn: async () => true, + }); + expect(featureFlagService.known_flags.has('fn-flag')).toBe(true); + }); + + it('should check function flags', async () => { + featureFlagService.register('dynamic-flag', { + $: 'function-flag', + fn: async ({ actor }) => actor?.type?.user?.username === 'test', + }); + + const result = await featureFlagService.check({ actor: { type: { user: { username: 'test' } } } }, 'dynamic-flag'); + expect(result).toBe(true); + }); + + it('should support function flags with different conditions', async () => { + featureFlagService.register('conditional-flag', { + $: 'function-flag', + fn: async ({ actor }) => actor?.type?.user?.username !== 'test', + }); + + const result = await featureFlagService.check({ actor: { type: { user: { username: 'other' } } } }, 'conditional-flag'); + expect(result).toBe(true); + }); + + it('should manage multiple flags', () => { + featureFlagService.register('multi-flag-1', { $: 'config-flag', value: true }); + featureFlagService.register('multi-flag-2', { $: 'config-flag', value: false }); + featureFlagService.register('multi-flag-3', { + $: 'function-flag', + fn: async () => true, + }); + + expect(featureFlagService.known_flags.has('multi-flag-1')).toBe(true); + expect(featureFlagService.known_flags.has('multi-flag-2')).toBe(true); + expect(featureFlagService.known_flags.has('multi-flag-3')).toBe(true); + expect(featureFlagService.known_flags.size).toBeGreaterThanOrEqual(3); + }); +}); + diff --git a/src/backend/src/services/HelloWorldService.test.ts b/src/backend/src/services/HelloWorldService.test.ts new file mode 100644 index 000000000..405705f97 --- /dev/null +++ b/src/backend/src/services/HelloWorldService.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { HelloWorldService } from './HelloWorldService'; + +describe('HelloWorldService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'hello-world': HelloWorldService, + }, + initLevelString: 'init', + }); + + const helloWorldService = testKernel.services!.get('hello-world') as any; + + it('should be instantiated', () => { + expect(helloWorldService).toBeInstanceOf(HelloWorldService); + }); + + it('should return version', () => { + const version = helloWorldService.as('version').get_version(); + expect(version).toBe('v1.0.0'); + }); + + it('should greet without subject', async () => { + const greeting = await helloWorldService.as('hello-world').greet({}); + expect(greeting).toBe('Hello, World!'); + }); + + it('should greet with subject', async () => { + const greeting = await helloWorldService.as('hello-world').greet({ subject: 'Alice' }); + expect(greeting).toBe('Hello, Alice!'); + }); + + it('should greet with different subjects', async () => { + const greeting1 = await helloWorldService.as('hello-world').greet({ subject: 'Bob' }); + const greeting2 = await helloWorldService.as('hello-world').greet({ subject: 'Charlie' }); + + expect(greeting1).toBe('Hello, Bob!'); + expect(greeting2).toBe('Hello, Charlie!'); + }); +}); + diff --git a/src/backend/src/services/HostnameService.test.ts b/src/backend/src/services/HostnameService.test.ts new file mode 100644 index 000000000..82c6c977b --- /dev/null +++ b/src/backend/src/services/HostnameService.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { HostnameService } from './HostnameService'; + +describe('HostnameService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + hostname: HostnameService, + }, + initLevelString: 'init', + }); + + const hostnameService = testKernel.services!.get('hostname') as HostnameService; + + it('should be instantiated', () => { + expect(hostnameService).toBeInstanceOf(HostnameService); + }); + + it('should have entries object', () => { + expect(hostnameService.entries).toBeDefined(); + expect(typeof hostnameService.entries).toBe('object'); + }); + + it('should have entries as empty object by default', () => { + expect(hostnameService.entries).toBeDefined(); + expect(typeof hostnameService.entries).toBe('object'); + }); + + it('should have get_broadcast_addresses method', () => { + expect(typeof hostnameService.get_broadcast_addresses).toBe('function'); + }); + + it('should allow manual entry registration', () => { + hostnameService.entries['manual.test.com'] = { scope: 'test' }; + expect(hostnameService.entries['manual.test.com']).toBeDefined(); + expect(hostnameService.entries['manual.test.com'].scope).toBe('test'); + }); + + it('should maintain multiple entries', () => { + hostnameService.entries['first.test.com'] = { scope: 'web' }; + hostnameService.entries['second.test.com'] = { scope: 'api' }; + + expect(hostnameService.entries['first.test.com'].scope).toBe('web'); + expect(hostnameService.entries['second.test.com'].scope).toBe('api'); + }); +}); + diff --git a/src/backend/src/services/LockService.test.ts b/src/backend/src/services/LockService.test.ts new file mode 100644 index 000000000..34dc8f4bf --- /dev/null +++ b/src/backend/src/services/LockService.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { LockService } from './LockService'; + +describe('LockService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + lock: LockService, + }, + initLevelString: 'init', + testCore: true, + }); + + const lockService = testKernel.services!.get('lock') as LockService; + + it('should be instantiated', () => { + expect(lockService).toBeInstanceOf(LockService); + }); + + it('should acquire and release a lock', async () => { + let executed = false; + await lockService.lock('test-lock', async () => { + executed = true; + }); + expect(executed).toBe(true); + }); + + it('should execute callback within lock', async () => { + const result = await lockService.lock('test-lock-2', async () => { + return 'success'; + }); + expect(result).toBe('success'); + }); + + it('should handle multiple sequential locks', async () => { + const results: number[] = []; + + await lockService.lock('seq-lock', async () => { + results.push(1); + }); + + await lockService.lock('seq-lock', async () => { + results.push(2); + }); + + expect(results).toEqual([1, 2]); + }); + + it('should handle locks with options', async () => { + let executed = false; + await lockService.lock('opt-lock', { timeout: 5000 }, async () => { + executed = true; + }); + expect(executed).toBe(true); + }); + + it('should support array of lock names', async () => { + let executed = false; + await lockService.lock(['lock-a', 'lock-b'], async () => { + executed = true; + }); + expect(executed).toBe(true); + }); + + it('should maintain lock state', async () => { + await lockService.lock('state-lock', async () => { + expect(lockService.locks['state-lock']).toBeDefined(); + }); + // Lock should still exist after release + expect(lockService.locks['state-lock']).toBeDefined(); + }); + + it('should handle errors within lock callback', async () => { + await expect( + lockService.lock('error-lock', async () => { + throw new Error('Test error'); + }) + ).rejects.toThrow('Test error'); + }); +}); + diff --git a/src/backend/src/services/MemoryStorageService.test.ts b/src/backend/src/services/MemoryStorageService.test.ts new file mode 100644 index 000000000..0f8db83c0 --- /dev/null +++ b/src/backend/src/services/MemoryStorageService.test.ts @@ -0,0 +1,94 @@ +import { Readable } from 'stream'; +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import MemoryStorageService from './MemoryStorageService'; + +describe('MemoryStorageService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'memory-storage': MemoryStorageService, + }, + initLevelString: 'construct', + }); + + const memoryStorage = testKernel.services!.get('memory-storage') as MemoryStorageService; + + it('should be instantiated', () => { + expect(memoryStorage).toBeInstanceOf(MemoryStorageService); + }); + + it('should create read stream from memory file', async () => { + const mockFile = { + content: Buffer.from('test content'), + }; + + const stream = await memoryStorage.create_read_stream('test-uuid', { + memory_file: mockFile, + }); + + expect(stream).toBeInstanceOf(Readable); + }); + + it('should read content from stream', async () => { + const testContent = 'Hello, World!'; + const mockFile = { + content: Buffer.from(testContent), + }; + + const stream = await memoryStorage.create_read_stream('test-uuid', { + memory_file: mockFile, + }) as Readable; + + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + const result = Buffer.concat(chunks).toString(); + expect(result).toBe(testContent); + }); + + it('should throw error when memory_file is not provided', async () => { + await expect( + memoryStorage.create_read_stream('test-uuid', {}) + ).rejects.toThrow('MemoryStorageService.create_read_stream: memory_file is required'); + }); + + it('should handle empty content', async () => { + const mockFile = { + content: Buffer.from(''), + }; + + const stream = await memoryStorage.create_read_stream('test-uuid', { + memory_file: mockFile, + }) as Readable; + + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + const result = Buffer.concat(chunks).toString(); + expect(result).toBe(''); + }); + + it('should handle binary content', async () => { + const binaryData = Buffer.from([0x00, 0x01, 0x02, 0xFF]); + const mockFile = { + content: binaryData, + }; + + const stream = await memoryStorage.create_read_stream('test-uuid', { + memory_file: mockFile, + }) as Readable; + + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + const result = Buffer.concat(chunks); + expect(result).toEqual(binaryData); + }); +}); + diff --git a/src/backend/src/services/NotificationService.test.ts b/src/backend/src/services/NotificationService.test.ts new file mode 100644 index 000000000..d6c3d04b2 --- /dev/null +++ b/src/backend/src/services/NotificationService.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import * as config from '../config'; +import { NotificationService, UserIDNotifSelector, UsernameNotifSelector } from './NotificationService'; +import { ScriptService } from './ScriptService'; + +describe('NotificationService', async () => { + config.load_config({ + 'services': { + 'database': { + path: ':memory:', + }, + }, + }); + + const testKernel = await createTestKernel({ + serviceMap: { + 'script': ScriptService, + 'notification': NotificationService, + }, + initLevelString: 'init', + testCore: true, + }); + + const notificationService = testKernel.services!.get('notification') as any; + + it('should be instantiated', () => { + expect(notificationService).toBeInstanceOf(NotificationService); + }); + + it('should have db connection after init', () => { + expect(notificationService.db).toBeDefined(); + }); + + it('should have notifs_pending_write object', () => { + expect(notificationService.notifs_pending_write).toBeDefined(); + expect(typeof notificationService.notifs_pending_write).toBe('object'); + }); + + it('should have merged_on_user_connected_ object', () => { + expect(notificationService.merged_on_user_connected_).toBeDefined(); + expect(typeof notificationService.merged_on_user_connected_).toBe('object'); + }); + + it('should have on_user_connected method', () => { + expect(notificationService.on_user_connected).toBeDefined(); + expect(typeof notificationService.on_user_connected).toBe('function'); + }); + + it('should have do_on_user_connected method', () => { + expect(notificationService.do_on_user_connected).toBeDefined(); + expect(typeof notificationService.do_on_user_connected).toBe('function'); + }); + + it('should have on_sent_to_user method', () => { + expect(notificationService.on_sent_to_user).toBeDefined(); + expect(typeof notificationService.on_sent_to_user).toBe('function'); + }); + + it('should have notify method', () => { + expect(notificationService.notify).toBeDefined(); + expect(typeof notificationService.notify).toBe('function'); + }); + + it('should schedule do_on_user_connected on user connected', async () => { + vi.useFakeTimers(); + + const user = { uuid: 'test-uuid-123', id: 1 }; + + await notificationService.on_user_connected({ user }); + + expect(notificationService.merged_on_user_connected_[user.uuid]).toBeDefined(); + + vi.useRealTimers(); + }); + + it('should clear previous timeout on repeated user connected', async () => { + vi.useFakeTimers(); + + const user = { uuid: 'test-uuid-456', id: 2 }; + + await notificationService.on_user_connected({ user }); + const firstTimeout = notificationService.merged_on_user_connected_[user.uuid]; + + await notificationService.on_user_connected({ user }); + const secondTimeout = notificationService.merged_on_user_connected_[user.uuid]; + + expect(firstTimeout).toBeDefined(); + expect(secondTimeout).toBeDefined(); + // The timeout should have been replaced + + vi.useRealTimers(); + }); + + it('should handle notify with user ID selector', async () => { + const userId = 123; + const selector = UserIDNotifSelector(userId); + + const result = await selector(notificationService); + + expect(result).toEqual([userId]); + }); +}); + +describe('UsernameNotifSelector', () => { + it('should create a selector function', () => { + const selector = UsernameNotifSelector('testuser'); + + expect(selector).toBeDefined(); + expect(typeof selector).toBe('function'); + }); + + it('should return function that fetches user by username', async () => { + const mockGetUserService = { + get_user: vi.fn().mockResolvedValue({ id: 42, username: 'testuser' }), + }; + + const mockService = { + services: { + get: vi.fn().mockReturnValue(mockGetUserService), + }, + }; + + const selector = UsernameNotifSelector('testuser'); + const result = await selector(mockService as any); + + expect(mockService.services.get).toHaveBeenCalledWith('get-user'); + expect(mockGetUserService.get_user).toHaveBeenCalledWith({ username: 'testuser' }); + expect(result).toEqual([42]); + }); +}); + +describe('UserIDNotifSelector', () => { + it('should create a selector function', () => { + const selector = UserIDNotifSelector(123); + + expect(selector).toBeDefined(); + expect(typeof selector).toBe('function'); + }); + + it('should return array with user ID', async () => { + const userId = 456; + const selector = UserIDNotifSelector(userId); + + const result = await selector(null as any); + + expect(result).toEqual([userId]); + }); + + it('should work with different user IDs', async () => { + const selector1 = UserIDNotifSelector(100); + const selector2 = UserIDNotifSelector(200); + const selector3 = UserIDNotifSelector(300); + + const result1 = await selector1(null as any); + const result2 = await selector2(null as any); + const result3 = await selector3(null as any); + + expect(result1).toEqual([100]); + expect(result2).toEqual([200]); + expect(result3).toEqual([300]); + }); +}); + diff --git a/src/backend/src/services/PuterVersionService.test.ts b/src/backend/src/services/PuterVersionService.test.ts new file mode 100644 index 000000000..0ac4ade0a --- /dev/null +++ b/src/backend/src/services/PuterVersionService.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { PuterVersionService } from './PuterVersionService'; + +describe('PuterVersionService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'puter-version': PuterVersionService, + }, + initLevelString: 'init', + }); + + const versionService = testKernel.services!.get('puter-version') as any; + + it('should be instantiated', () => { + expect(versionService).toBeInstanceOf(PuterVersionService); + }); + + it('should have boot_time set after init', () => { + expect(versionService.boot_time).toBeDefined(); + expect(typeof versionService.boot_time).toBe('number'); + expect(versionService.boot_time).toBeGreaterThan(0); + }); + + it('should return version info', () => { + const versionInfo = versionService.get_version(); + + expect(versionInfo).toBeDefined(); + expect(versionInfo).toHaveProperty('version'); + expect(versionInfo).toHaveProperty('environment'); + expect(versionInfo).toHaveProperty('location'); + expect(versionInfo).toHaveProperty('deploy_timestamp'); + }); + + it('should have valid version string', () => { + const versionInfo = versionService.get_version(); + + expect(typeof versionInfo.version).toBe('string'); + expect(versionInfo.version).toBeTruthy(); + }); + + it('should have deploy_timestamp matching boot_time', () => { + const versionInfo = versionService.get_version(); + + expect(versionInfo.deploy_timestamp).toBe(versionService.boot_time); + }); + + it('should have environment from config', () => { + const versionInfo = versionService.get_version(); + + // Environment might be undefined in test context + expect(versionInfo).toHaveProperty('environment'); + }); + + it('should have location from config', () => { + const versionInfo = versionService.get_version(); + + // Location might be undefined in test context + expect(versionInfo).toHaveProperty('location'); + }); + + it('should return consistent version info on multiple calls', () => { + const versionInfo1 = versionService.get_version(); + const versionInfo2 = versionService.get_version(); + + expect(versionInfo1.version).toBe(versionInfo2.version); + expect(versionInfo1.deploy_timestamp).toBe(versionInfo2.deploy_timestamp); + }); +}); + diff --git a/src/backend/src/services/RegistryService.test.ts b/src/backend/src/services/RegistryService.test.ts new file mode 100644 index 000000000..68ae2e24f --- /dev/null +++ b/src/backend/src/services/RegistryService.test.ts @@ -0,0 +1,98 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { RegistryService } from './RegistryService'; + +describe('RegistryService', async () => { + // Initialize globalThis.kv for testing + beforeAll(() => { + if (!globalThis.kv) { + globalThis.kv = new Map(); + globalThis.kv.set = function(key, value) { + return Map.prototype.set.call(this, key, value); + }; + globalThis.kv.get = function(key) { + return Map.prototype.get.call(this, key); + }; + globalThis.kv.exists = function(key) { + return this.has(key); + }; + globalThis.kv.del = function(key) { + return this.delete(key); + }; + globalThis.kv.keys = function(pattern) { + const prefix = pattern.replace('*', ''); + return Array.from(this.keys()).filter(k => k.startsWith(prefix)); + }; + } + }); + + const testKernel = await createTestKernel({ + serviceMap: { + registry: RegistryService, + }, + initLevelString: 'init', + }); + + const registryService = testKernel.services!.get('registry') as RegistryService; + + it('should be instantiated', () => { + expect(registryService).toBeInstanceOf(RegistryService); + }); + + it('should register a collection', () => { + const collection = registryService.register_collection('test-collection'); + expect(collection).toBeDefined(); + }); + + it('should retrieve registered collection', () => { + registryService.register_collection('retrieve-collection'); + const collection = registryService.get('retrieve-collection'); + expect(collection).toBeDefined(); + }); + + it('should throw error when registering duplicate collection', () => { + registryService.register_collection('duplicate-collection'); + expect(() => { + registryService.register_collection('duplicate-collection'); + }).toThrow('collection duplicate-collection already exists'); + }); + + it('should throw error when getting non-existent collection', () => { + expect(() => { + registryService.get('non-existent-collection'); + }).toThrow('collection non-existent-collection does not exist'); + }); + + it('should allow setting values in collection', () => { + const collection = registryService.register_collection('value-collection'); + collection.set('key1', 'value1'); + expect(collection.get('key1')).toBe('value1'); + }); + + it('should allow checking existence in collection', () => { + const collection = registryService.register_collection('exists-collection'); + collection.set('existing-key', 'value'); + expect(collection.exists('existing-key')).toBe(true); + expect(collection.exists('non-existing-key')).toBe(false); + }); + + it('should allow deleting from collection', () => { + const collection = registryService.register_collection('delete-collection'); + collection.set('delete-key', 'value'); + expect(collection.exists('delete-key')).toBe(true); + collection.del('delete-key'); + expect(collection.exists('delete-key')).toBe(false); + }); + + it('should support multiple independent collections', () => { + const collection1 = registryService.register_collection('coll1'); + const collection2 = registryService.register_collection('coll2'); + + collection1.set('key', 'value1'); + collection2.set('key', 'value2'); + + expect(collection1.get('key')).toBe('value1'); + expect(collection2.get('key')).toBe('value2'); + }); +}); + diff --git a/src/backend/src/services/ScriptService.test.ts b/src/backend/src/services/ScriptService.test.ts new file mode 100644 index 000000000..fd316aba2 --- /dev/null +++ b/src/backend/src/services/ScriptService.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { BackendScript, ScriptService } from './ScriptService'; + +describe('ScriptService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'script': ScriptService, + }, + initLevelString: 'construct', + }); + + const scriptService = testKernel.services!.get('script') as any; + + it('should be instantiated', () => { + expect(scriptService).toBeInstanceOf(ScriptService); + }); + + it('should have empty scripts array initially', () => { + expect(scriptService.scripts).toBeDefined(); + expect(Array.isArray(scriptService.scripts)).toBe(true); + }); + + it('should register a script', () => { + const initialLength = scriptService.scripts.length; + const scriptFn = async (ctx: any, args: any[]) => { + return 'result'; + }; + + scriptService.register('test-script', scriptFn); + + expect(scriptService.scripts.length).toBe(initialLength + 1); + }); + + it('should create BackendScript instance on registration', () => { + const service = testKernel.services!.get('script') as any; + const scriptFn = async (ctx: any, args: any[]) => {}; + + service.register('backend-script', scriptFn); + + const lastScript = service.scripts[service.scripts.length - 1]; + expect(lastScript).toBeInstanceOf(BackendScript); + expect(lastScript.name).toBe('backend-script'); + }); + + it('should store script function', () => { + const service = testKernel.services!.get('script') as any; + const scriptFn = async (ctx: any, args: any[]) => 'my-result'; + + service.register('fn-script', scriptFn); + + const lastScript = service.scripts[service.scripts.length - 1]; + expect(lastScript.fn).toBe(scriptFn); + }); + + it('should execute registered script', async () => { + const service = testKernel.services!.get('script') as any; + let executed = false; + + const scriptFn = async (ctx: any, args: any[]) => { + executed = true; + return 'executed'; + }; + + service.register('exec-script', scriptFn); + const script = service.scripts[service.scripts.length - 1]; + + const result = await script.run({}, []); + + expect(executed).toBe(true); + expect(result).toBe('executed'); + }); + + it('should pass context to script', async () => { + const service = testKernel.services!.get('script') as any; + let receivedCtx: any = null; + + const scriptFn = async (ctx: any, args: any[]) => { + receivedCtx = ctx; + }; + + service.register('ctx-script', scriptFn); + const script = service.scripts[service.scripts.length - 1]; + + const testCtx = { test: 'context' }; + await script.run(testCtx, []); + + expect(receivedCtx).toBe(testCtx); + }); + + it('should pass arguments to script', async () => { + const service = testKernel.services!.get('script') as any; + let receivedArgs: any[] = []; + + const scriptFn = async (ctx: any, args: any[]) => { + receivedArgs = args; + }; + + service.register('args-script', scriptFn); + const script = service.scripts[service.scripts.length - 1]; + + const testArgs = ['arg1', 'arg2', 'arg3']; + await script.run({}, testArgs); + + expect(receivedArgs).toEqual(testArgs); + }); + + it('should handle multiple script registrations', () => { + const service = testKernel.services!.get('script') as any; + + service.register('script1', async () => {}); + service.register('script2', async () => {}); + service.register('script3', async () => {}); + + const scriptNames = service.scripts.map((s: any) => s.name); + expect(scriptNames).toContain('script1'); + expect(scriptNames).toContain('script2'); + expect(scriptNames).toContain('script3'); + }); + + it('should allow scripts to return values', async () => { + const service = testKernel.services!.get('script') as any; + + service.register('return-script', async (ctx: any, args: any[]) => { + return { success: true, data: args[0] }; + }); + + const script = service.scripts[service.scripts.length - 1]; + const result = await script.run({}, ['test-data']); + + expect(result).toEqual({ success: true, data: 'test-data' }); + }); +}); + +describe('BackendScript', () => { + it('should create script with name and function', () => { + const fn = async () => {}; + const script = new BackendScript('test', fn); + + expect(script.name).toBe('test'); + expect(script.fn).toBe(fn); + }); + + it('should execute script function', async () => { + let executed = false; + const fn = async () => { executed = true; }; + const script = new BackendScript('exec', fn); + + await script.run({}, []); + + expect(executed).toBe(true); + }); + + it('should pass parameters to function', async () => { + let receivedCtx: any = null; + let receivedArgs: any = null; + + const fn = async (ctx: any, args: any) => { + receivedCtx = ctx; + receivedArgs = args; + }; + + const script = new BackendScript('params', fn); + const ctx = { test: true }; + const args = ['a', 'b']; + + await script.run(ctx, args); + + expect(receivedCtx).toBe(ctx); + expect(receivedArgs).toBe(args); + }); + + it('should return function result', async () => { + const fn = async () => 'result-value'; + const script = new BackendScript('return', fn); + + const result = await script.run({}, []); + + expect(result).toBe('result-value'); + }); +}); + diff --git a/src/backend/src/services/ShutdownService.test.ts b/src/backend/src/services/ShutdownService.test.ts new file mode 100644 index 000000000..1854adb7a --- /dev/null +++ b/src/backend/src/services/ShutdownService.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { ShutdownService } from './ShutdownService'; + +describe('ShutdownService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + shutdown: ShutdownService, + }, + initLevelString: 'construct', + }); + + const shutdownService = testKernel.services!.get('shutdown') as ShutdownService; + + // Mock the logger for the service + shutdownService.log = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + + it('should be instantiated', () => { + expect(shutdownService).toBeInstanceOf(ShutdownService); + }); + + it('should have shutdown method', () => { + expect(typeof shutdownService.shutdown).toBe('function'); + }); + + it('should call process.exit when shutdown is called', () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any); + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((() => {}) as any); + + shutdownService.shutdown({ reason: 'test shutdown', code: 0 }); + + expect(exitSpy).toHaveBeenCalledWith(0); + expect(stdoutSpy).toHaveBeenCalled(); + + exitSpy.mockRestore(); + stdoutSpy.mockRestore(); + }); + + it('should use default exit code when not provided', () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any); + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((() => {}) as any); + + shutdownService.shutdown({ reason: 'test' }); + + expect(exitSpy).toHaveBeenCalledWith(0); + + exitSpy.mockRestore(); + stdoutSpy.mockRestore(); + }); + + it('should use custom exit code when provided', () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any); + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((() => {}) as any); + + shutdownService.shutdown({ reason: 'error', code: 1 }); + + expect(exitSpy).toHaveBeenCalledWith(1); + + exitSpy.mockRestore(); + stdoutSpy.mockRestore(); + }); + + it('should work without any parameters', () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any); + const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((() => {}) as any); + + shutdownService.shutdown(); + + expect(exitSpy).toHaveBeenCalledWith(0); + + exitSpy.mockRestore(); + stdoutSpy.mockRestore(); + }); +}); + diff --git a/src/backend/src/services/SystemValidationService.test.ts b/src/backend/src/services/SystemValidationService.test.ts new file mode 100644 index 000000000..f1fab6074 --- /dev/null +++ b/src/backend/src/services/SystemValidationService.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { SystemValidationService } from './SystemValidationService'; + +describe('SystemValidationService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + 'system-validation': SystemValidationService, + }, + initLevelString: 'init', + }); + + const systemValidationService = testKernel.services!.get('system-validation') as any; + + it('should be instantiated', () => { + expect(systemValidationService).toBeInstanceOf(SystemValidationService); + }); + + it('should have mark_invalid method', () => { + expect(systemValidationService.mark_invalid).toBeDefined(); + expect(typeof systemValidationService.mark_invalid).toBe('function'); + }); + + it('should handle mark_invalid in dev environment', async () => { + // Set up dev environment + const originalEnv = systemValidationService.global_config?.env; + if (systemValidationService.global_config) { + systemValidationService.global_config.env = 'dev'; + } + + // Mock the error service + const mockReport = vi.fn(); + systemValidationService.errors = { + report: mockReport, + }; + + // Mock dev-console service + const mockTurnOn = vi.fn(); + const mockAddWidget = vi.fn(); + const mockDevConsole = { + turn_on_the_warning_lights: mockTurnOn, + add_widget: mockAddWidget, + }; + + const originalGet = testKernel.services.get.bind(testKernel.services); + testKernel.services.get = vi.fn((name: string) => { + if (name === 'dev-console') return mockDevConsole; + return originalGet(name); + }) as any; + + try { + await systemValidationService.mark_invalid('test message', new Error('test error')); + + // Verify error was reported + expect(mockReport).toHaveBeenCalledWith('INVALID SYSTEM STATE', expect.objectContaining({ + message: 'test message', + trace: true, + alarm: true, + })); + + // Verify dev console was called + expect(mockTurnOn).toHaveBeenCalled(); + expect(mockAddWidget).toHaveBeenCalled(); + } finally { + // Restore original environment + if (systemValidationService.global_config) { + systemValidationService.global_config.env = originalEnv; + } + testKernel.services.get = originalGet as any; + } + }); + + it('should create source error if not provided', async () => { + const originalEnv = systemValidationService.global_config?.env; + if (systemValidationService.global_config) { + systemValidationService.global_config.env = 'dev'; + } + + const mockReport = vi.fn(); + systemValidationService.errors = { + report: mockReport, + }; + + const mockDevConsole = { + turn_on_the_warning_lights: vi.fn(), + add_widget: vi.fn(), + }; + + const originalGet = testKernel.services.get.bind(testKernel.services); + testKernel.services.get = vi.fn((name: string) => { + if (name === 'dev-console') return mockDevConsole; + return originalGet(name); + }) as any; + + try { + await systemValidationService.mark_invalid('test without source'); + + expect(mockReport).toHaveBeenCalledWith('INVALID SYSTEM STATE', expect.objectContaining({ + source: expect.any(Error), + })); + } finally { + if (systemValidationService.global_config) { + systemValidationService.global_config.env = originalEnv; + } + testKernel.services.get = originalGet as any; + } + }); + + it('should report with correct parameters', async () => { + const originalEnv = systemValidationService.global_config?.env; + if (systemValidationService.global_config) { + systemValidationService.global_config.env = 'dev'; + } + + const mockReport = vi.fn(); + systemValidationService.errors = { + report: mockReport, + }; + + const mockDevConsole = { + turn_on_the_warning_lights: vi.fn(), + add_widget: vi.fn(), + }; + + const originalGet = testKernel.services.get.bind(testKernel.services); + testKernel.services.get = vi.fn((name: string) => { + if (name === 'dev-console') return mockDevConsole; + return originalGet(name); + }) as any; + + try { + const testError = new Error('specific error'); + await systemValidationService.mark_invalid('specific message', testError); + + expect(mockReport).toHaveBeenCalledWith('INVALID SYSTEM STATE', { + source: testError, + message: 'specific message', + trace: true, + alarm: true, + }); + } finally { + if (systemValidationService.global_config) { + systemValidationService.global_config.env = originalEnv; + } + testKernel.services.get = originalGet as any; + } + }); +}); + diff --git a/src/backend/src/services/TraceService.test.ts b/src/backend/src/services/TraceService.test.ts new file mode 100644 index 000000000..df71aa835 --- /dev/null +++ b/src/backend/src/services/TraceService.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; +import { createTestKernel } from '../../tools/test.mjs'; +import { TraceService } from './TraceService'; + +describe('TraceService', async () => { + const testKernel = await createTestKernel({ + serviceMap: { + trace: TraceService, + }, + initLevelString: 'construct', + }); + + const traceService = testKernel.services!.get('trace') as TraceService; + + it('should be instantiated', () => { + expect(traceService).toBeInstanceOf(TraceService); + }); + + it('should have a tracer', () => { + expect(traceService.tracer).toBeDefined(); + }); + + it('should create spans with spanify', async () => { + const result = await traceService.spanify('test-span', async ({ span }) => { + expect(span).toBeDefined(); + return 'test-result'; + }); + expect(result).toBe('test-result'); + }); + + it('should execute callback within span', async () => { + let executed = false; + await traceService.spanify('exec-span', async () => { + executed = true; + }); + expect(executed).toBe(true); + }); + + it('should handle errors in spanify', async () => { + await expect( + traceService.spanify('error-span', async () => { + throw new Error('Test span error'); + }) + ).rejects.toThrow('Test span error'); + }); + + it('should support options in spanify', async () => { + const result = await traceService.spanify('options-span', async ({ span }) => { + return 'with-options'; + }, { + attributes: { 'test.attribute': 'value' }, + }); + expect(result).toBe('with-options'); + }); + + it('should return values from span callback', async () => { + const obj = { value: 42 }; + const result = await traceService.spanify('return-span', async () => { + return obj; + }); + expect(result).toEqual(obj); + }); +}); +