From 17b4263a152b4c7d9b8615288ad574092073bee2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Jul 2025 07:56:39 +0000 Subject: [PATCH] Add comprehensive unit tests for server-shared package Co-authored-by: natemkelley --- packages/server-shared/TEST_SUMMARY.md | 156 +++++ packages/server-shared/package.json | 8 +- .../src/chats/chat-errors.types.test.ts | 242 ++++++++ .../src/chats/chat-message.types.test.ts | 587 ++++++++++++++++++ .../src/chats/chat.types.test.ts | 309 +++++++++ .../server-shared/src/chats/index.test.ts | 217 +++++++ .../src/currency/currency.types.test.ts | 249 ++++++++ .../server-shared/src/currency/index.test.ts | 68 ++ packages/server-shared/src/index.test.ts | 89 +++ packages/server-shared/vitest.config.ts | 3 + pnpm-lock.yaml | 7 + 11 files changed, 1934 insertions(+), 1 deletion(-) create mode 100644 packages/server-shared/TEST_SUMMARY.md create mode 100644 packages/server-shared/src/chats/chat-errors.types.test.ts create mode 100644 packages/server-shared/src/chats/chat-message.types.test.ts create mode 100644 packages/server-shared/src/chats/chat.types.test.ts create mode 100644 packages/server-shared/src/chats/index.test.ts create mode 100644 packages/server-shared/src/currency/currency.types.test.ts create mode 100644 packages/server-shared/src/currency/index.test.ts create mode 100644 packages/server-shared/src/index.test.ts create mode 100644 packages/server-shared/vitest.config.ts diff --git a/packages/server-shared/TEST_SUMMARY.md b/packages/server-shared/TEST_SUMMARY.md new file mode 100644 index 000000000..a187aa7f3 --- /dev/null +++ b/packages/server-shared/TEST_SUMMARY.md @@ -0,0 +1,156 @@ +# Server-Shared Package - Comprehensive Unit Tests Summary + +## Overview +I have successfully created comprehensive unit tests for **every single file** in the `packages/server-shared` folder. All tests are now passing with **115 total tests** across **7 test files**. + +## Test Coverage Summary + +### ๐ŸŽฏ Files Tested +- โœ… `src/index.ts` - Main entry point exports +- โœ… `src/chats/index.ts` - Chat module exports +- โœ… `src/chats/chat.types.ts` - Chat type schemas +- โœ… `src/chats/chat-errors.types.ts` - Error handling types +- โœ… `src/chats/chat-message.types.ts` - Message type schemas +- โœ… `src/currency/index.ts` - Currency module exports +- โœ… `src/currency/currency.types.ts` - Currency type schemas + +### ๐Ÿ“Š Test Statistics +- **Test Files**: 7 passed (7) +- **Total Tests**: 115 passed (115) +- **Duration**: 428ms +- **Pass Rate**: 100% โœ… + +## Detailed Test Coverage + +### 1. Chat Types (`chat.types.test.ts`) - 27 tests +**Comprehensive testing of chat-related Zod schemas:** +- โœ… `AssetPermissionRoleSchema` validation (enum values: viewer, editor, owner) +- โœ… `BusterShareIndividualSchema` validation (email, role, optional name) +- โœ… `ChatWithMessagesSchema` validation (complex nested objects) +- โœ… `ChatCreateRequestSchema` validation (request validation with refinements) +- โœ… `ChatCreateHandlerRequestSchema` validation (internal handler requests) + +**Key Test Cases:** +- Valid enum values and rejection of invalid ones +- Email validation and required field validation +- Complex nested object validation with optional fields +- UUID format validation +- Schema refinement rules (asset_id requires asset_type) +- Type inference verification + +### 2. Chat Errors (`chat-errors.types.test.ts`) - 22 tests +**Complete testing of error handling system:** +- โœ… `ChatErrorCode` constant validation (10 error codes) +- โœ… `ChatErrorResponseSchema` validation +- โœ… `ChatError` class functionality + +**Key Test Cases:** +- All error code constants exist and have correct values +- Error response schema validation with and without details +- ChatError class constructor, methods, and inheritance +- Error serialization with `toResponse()` method +- Custom status codes and error details handling +- Schema compatibility between ChatError output and response schema + +### 3. Chat Messages (`chat-message.types.test.ts`) - 28 tests +**Extensive testing of complex discriminated union schemas:** +- โœ… `ResponseMessageSchema` (discriminated union: text vs file) +- โœ… `ReasoningMessageSchema` (discriminated union: text vs files vs pills) +- โœ… `ChatMessageSchema` (complete message structure) + +**Key Test Cases:** +- Text response messages with optional fields +- File response messages with all file types (metric, dashboard, reasoning) +- File metadata and version handling +- Text reasoning messages with status validation +- Files reasoning messages with nested file objects +- Pills reasoning messages with pill containers and types +- Complex nested chat message validation +- Discriminated union type validation +- Status enum validation (loading, completed, failed) +- All file and pill type enums + +### 4. Currency Types (`currency.types.test.ts`) - 14 tests +**Thorough testing of simple schema:** +- โœ… `CurrencySchema` validation and edge cases + +**Key Test Cases:** +- Valid currency objects (code, description, flag) +- Multiple real-world currency examples +- Empty string handling +- Long descriptions and special characters +- Unicode character support (Arabic, Chinese, complex emojis) +- Missing field validation +- Non-string value rejection +- Extra property handling (schema stripping) +- SafeParse functionality for error handling + +### 5. Index Files (`index.test.ts`, `chats/index.test.ts`, `currency/index.test.ts`) - 24 tests +**Export verification and integration testing:** +- โœ… Main package exports work correctly +- โœ… Chat module exports work correctly +- โœ… Currency module exports work correctly + +**Key Test Cases:** +- All expected schemas are exported and functional +- Schema validation works through exports +- Type inference works correctly +- Export isolation (currency not in main index) +- Integration testing of exported functionality + +## ๐Ÿ”ง Testing Infrastructure Setup + +### Dependencies Added +- โœ… Added `vitest` and `@buster/vitest-config` to devDependencies +- โœ… Added test scripts to package.json (`test`, `test:watch`) +- โœ… Created `vitest.config.ts` using workspace base configuration + +### Test File Organization +- โœ… Co-located tests with source files (`.test.ts` alongside `.ts`) +- โœ… Follows project testing conventions +- โœ… Uses vitest framework as specified in project rules + +## ๐Ÿงช Test Quality Features + +### Comprehensive Validation Testing +- **Schema Defaults**: Tested optional fields and default behavior +- **Edge Cases**: Empty strings, null values, undefined, special characters +- **Type Safety**: Verified TypeScript type inference works correctly +- **Error Scenarios**: Invalid inputs, missing fields, wrong types +- **Real-World Data**: Used realistic examples (currencies, UUIDs, etc.) + +### Advanced Schema Testing +- **Discriminated Unions**: Thoroughly tested complex type discrimination +- **Nested Objects**: Deep validation of complex object structures +- **Array Validation**: Tested array fields with various lengths and contents +- **Enum Validation**: Tested all enum values and rejection of invalid ones +- **Custom Validation**: Tested Zod refinements and custom rules + +### Integration Testing +- **Export Verification**: Ensured all modules export correctly +- **Cross-Module**: Tested schemas work across module boundaries +- **Type Inference**: Verified TypeScript types work as expected + +## ๐Ÿš€ Benefits Achieved + +1. **100% File Coverage**: Every TypeScript file in the package has corresponding tests +2. **Schema Validation**: Comprehensive testing of all Zod schemas ensures data integrity +3. **Type Safety**: Verified TypeScript integration and type inference +4. **Error Handling**: Complete coverage of error scenarios and edge cases +5. **Real-World Testing**: Used realistic data examples and use cases +6. **Maintenance**: Tests will catch breaking changes and regressions +7. **Documentation**: Tests serve as living examples of how to use the schemas + +## ๐ŸŽฏ Conclusion + +The `packages/server-shared` package now has **comprehensive, battle-tested unit tests** covering every schema, type, and function. With **115 passing tests**, this provides excellent confidence in the reliability and correctness of the shared type definitions used across the Buster application. + +The tests follow best practices including: +- โœ… Co-located test files +- โœ… Descriptive test names and organization +- โœ… Edge case coverage +- โœ… Type inference verification +- โœ… Real-world example usage +- โœ… Error scenario testing + +**All tests pass successfully!** ๐ŸŽ‰ \ No newline at end of file diff --git a/packages/server-shared/package.json b/packages/server-shared/package.json index 5f293d309..d43eeabda 100644 --- a/packages/server-shared/package.json +++ b/packages/server-shared/package.json @@ -8,7 +8,9 @@ "build": "tsc --build", "dev": "tsc --watch", "lint": "biome check", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "exports": { ".": { @@ -27,5 +29,9 @@ "dependencies": { "@buster/typescript-config": "workspace:*", "zod": "catalog:" + }, + "devDependencies": { + "@buster/vitest-config": "workspace:*", + "vitest": "catalog:" } } diff --git a/packages/server-shared/src/chats/chat-errors.types.test.ts b/packages/server-shared/src/chats/chat-errors.types.test.ts new file mode 100644 index 000000000..d57aaaa30 --- /dev/null +++ b/packages/server-shared/src/chats/chat-errors.types.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it } from 'vitest'; +import { + ChatError, + ChatErrorCode, + type ChatErrorResponse, + ChatErrorResponseSchema, +} from './chat-errors.types'; + +describe('ChatErrorCode', () => { + it('should have all expected error codes', () => { + const expectedCodes = [ + 'INVALID_REQUEST', + 'MISSING_ORGANIZATION', + 'UNAUTHORIZED', + 'PERMISSION_DENIED', + 'CHAT_NOT_FOUND', + 'ASSET_NOT_FOUND', + 'USER_NOT_FOUND', + 'DATABASE_ERROR', + 'TRIGGER_ERROR', + 'INTERNAL_ERROR', + ]; + + for (const code of expectedCodes) { + expect(ChatErrorCode[code as keyof typeof ChatErrorCode]).toBe(code); + } + }); + + it('should have validation error codes', () => { + expect(ChatErrorCode.INVALID_REQUEST).toBe('INVALID_REQUEST'); + expect(ChatErrorCode.MISSING_ORGANIZATION).toBe('MISSING_ORGANIZATION'); + }); + + it('should have permission error codes', () => { + expect(ChatErrorCode.UNAUTHORIZED).toBe('UNAUTHORIZED'); + expect(ChatErrorCode.PERMISSION_DENIED).toBe('PERMISSION_DENIED'); + }); + + it('should have resource error codes', () => { + expect(ChatErrorCode.CHAT_NOT_FOUND).toBe('CHAT_NOT_FOUND'); + expect(ChatErrorCode.ASSET_NOT_FOUND).toBe('ASSET_NOT_FOUND'); + expect(ChatErrorCode.USER_NOT_FOUND).toBe('USER_NOT_FOUND'); + }); + + it('should have service error codes', () => { + expect(ChatErrorCode.DATABASE_ERROR).toBe('DATABASE_ERROR'); + expect(ChatErrorCode.TRIGGER_ERROR).toBe('TRIGGER_ERROR'); + expect(ChatErrorCode.INTERNAL_ERROR).toBe('INTERNAL_ERROR'); + }); + + it('should be readonly (const assertion)', () => { + // TypeScript should prevent this at compile time, but we can test the values + expect(typeof ChatErrorCode).toBe('object'); + expect(Object.isFrozen(ChatErrorCode)).toBe(false); // const assertion doesn't freeze + }); +}); + +describe('ChatErrorResponseSchema', () => { + it('should validate valid error response', () => { + const validResponse = { + code: 'INVALID_REQUEST', + message: 'The request is invalid', + }; + + const result = ChatErrorResponseSchema.parse(validResponse); + expect(result).toEqual(validResponse); + }); + + it('should validate error response with details', () => { + const responseWithDetails = { + code: 'PERMISSION_DENIED', + message: 'You do not have permission to access this resource', + details: { + resource: 'chat', + resourceId: '123', + requiredPermission: 'read', + }, + }; + + const result = ChatErrorResponseSchema.parse(responseWithDetails); + expect(result).toEqual(responseWithDetails); + }); + + it('should validate error response with complex details', () => { + const responseWithComplexDetails = { + code: 'DATABASE_ERROR', + message: 'Database operation failed', + details: { + query: 'SELECT * FROM chats', + error: { message: 'Connection timeout' }, + retries: 3, + timestamp: new Date().toISOString(), + }, + }; + + const result = ChatErrorResponseSchema.parse(responseWithComplexDetails); + expect(result.details).toEqual(responseWithComplexDetails.details); + }); + + it('should handle optional details field', () => { + const responseWithoutDetails = { + code: 'INTERNAL_ERROR', + message: 'An internal error occurred', + }; + + const result = ChatErrorResponseSchema.parse(responseWithoutDetails); + expect(result.details).toBeUndefined(); + }); + + it('should require code and message fields', () => { + expect(() => ChatErrorResponseSchema.parse({})).toThrow(); + expect(() => ChatErrorResponseSchema.parse({ code: 'INVALID_REQUEST' })).toThrow(); + expect(() => ChatErrorResponseSchema.parse({ message: 'Error message' })).toThrow(); + }); + + it('should reject non-string code and message', () => { + expect(() => + ChatErrorResponseSchema.parse({ + code: 123, + message: 'Error message', + }), + ).toThrow(); + + expect(() => + ChatErrorResponseSchema.parse({ + code: 'INVALID_REQUEST', + message: 123, + }), + ).toThrow(); + }); + + it('should have correct type inference', () => { + const response: ChatErrorResponse = { + code: 'UNAUTHORIZED', + message: 'Authentication required', + details: { endpoint: '/api/chat' }, + }; + expect(response.code).toBe('UNAUTHORIZED'); + }); +}); + +describe('ChatError', () => { + it('should create error with required parameters', () => { + const error = new ChatError('INVALID_REQUEST', 'The request is invalid'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(ChatError); + expect(error.name).toBe('ChatError'); + expect(error.code).toBe('INVALID_REQUEST'); + expect(error.message).toBe('The request is invalid'); + expect(error.statusCode).toBe(500); // default + expect(error.details).toBeUndefined(); + }); + + it('should create error with custom status code', () => { + const error = new ChatError('UNAUTHORIZED', 'Authentication required', 401); + + expect(error.code).toBe('UNAUTHORIZED'); + expect(error.message).toBe('Authentication required'); + expect(error.statusCode).toBe(401); + }); + + it('should create error with details', () => { + const details = { userId: '123', resource: 'chat' }; + const error = new ChatError('PERMISSION_DENIED', 'Access denied', 403, details); + + expect(error.code).toBe('PERMISSION_DENIED'); + expect(error.message).toBe('Access denied'); + expect(error.statusCode).toBe(403); + expect(error.details).toEqual(details); + }); + + it('should support all valid status codes', () => { + const statusCodes = [400, 401, 403, 404, 409, 500] as const; + + for (const statusCode of statusCodes) { + const error = new ChatError('INTERNAL_ERROR', 'Test error', statusCode); + expect(error.statusCode).toBe(statusCode); + } + }); + + it('should convert to response format', () => { + const error = new ChatError('DATABASE_ERROR', 'Database connection failed', 500, { + query: 'SELECT * FROM users', + }); + + const response = error.toResponse(); + + expect(response).toEqual({ + code: 'DATABASE_ERROR', + message: 'Database connection failed', + details: { query: 'SELECT * FROM users' }, + }); + }); + + it('should convert to response format without details', () => { + const error = new ChatError('CHAT_NOT_FOUND', 'Chat not found', 404); + + const response = error.toResponse(); + + expect(response).toEqual({ + code: 'CHAT_NOT_FOUND', + message: 'Chat not found', + }); + expect(response.details).toBeUndefined(); + }); + + it('should maintain error stack trace', () => { + const error = new ChatError('INTERNAL_ERROR', 'Test error'); + expect(error.stack).toBeDefined(); + expect(error.stack).toContain('ChatError'); + }); + + it('should be throwable and catchable', () => { + expect(() => { + throw new ChatError('INVALID_REQUEST', 'Test error', 400); + }).toThrow(ChatError); + + try { + throw new ChatError('UNAUTHORIZED', 'Auth error', 401); + } catch (error) { + expect(error).toBeInstanceOf(ChatError); + expect((error as ChatError).code).toBe('UNAUTHORIZED'); + expect((error as ChatError).statusCode).toBe(401); + } + }); + + it('should validate response format against schema', () => { + const error = new ChatError('TRIGGER_ERROR', 'External service error', 500, { + service: 'trigger', + endpoint: '/api/chat/create', + }); + + const response = error.toResponse(); + + // The response should be valid according to our schema + expect(() => ChatErrorResponseSchema.parse(response)).not.toThrow(); + + const validatedResponse = ChatErrorResponseSchema.parse(response); + expect(validatedResponse).toEqual(response); + }); +}); diff --git a/packages/server-shared/src/chats/chat-message.types.test.ts b/packages/server-shared/src/chats/chat-message.types.test.ts new file mode 100644 index 000000000..cb0a81da0 --- /dev/null +++ b/packages/server-shared/src/chats/chat-message.types.test.ts @@ -0,0 +1,587 @@ +import { describe, expect, it } from 'vitest'; +import { + type ChatMessage, + type ChatMessageReasoningMessage, + type ChatMessageReasoningMessage_Files, + type ChatMessageReasoningMessage_Pills, + type ChatMessageReasoningMessage_Text, + type ChatMessageResponseMessage, + type ChatMessageResponseMessage_File, + type ChatMessageResponseMessage_Text, + ChatMessageSchema, + ReasoningMessageSchema, + ResponseMessageSchema, +} from './chat-message.types'; + +describe('ResponseMessageSchema', () => { + describe('Text Response Message', () => { + it('should validate text response message', () => { + const textMessage = { + id: 'msg-1', + type: 'text' as const, + message: 'This is a text response', + }; + + const result = ResponseMessageSchema.parse(textMessage); + expect(result).toEqual(textMessage); + expect(result.type).toBe('text'); + }); + + it('should validate text message with optional is_final_message', () => { + const textMessage = { + id: 'msg-1', + type: 'text' as const, + message: 'Final response', + is_final_message: true, + }; + + const result = ResponseMessageSchema.parse(textMessage); + expect(result.is_final_message).toBe(true); + }); + + it('should handle missing optional fields', () => { + const textMessage = { + id: 'msg-1', + type: 'text' as const, + message: 'Response without optional fields', + }; + + const result = ResponseMessageSchema.parse(textMessage); + expect(result.is_final_message).toBeUndefined(); + }); + }); + + describe('File Response Message', () => { + it('should validate file response message', () => { + const fileMessage = { + id: 'file-1', + type: 'file' as const, + file_type: 'metric' as const, + file_name: 'sales_metrics.yaml', + version_number: 1, + }; + + const result = ResponseMessageSchema.parse(fileMessage); + expect(result).toEqual(fileMessage); + expect(result.type).toBe('file'); + }); + + it('should validate all file types', () => { + const fileTypes = ['metric', 'dashboard', 'reasoning'] as const; + + for (const fileType of fileTypes) { + const fileMessage = { + id: `file-${fileType}`, + type: 'file' as const, + file_type: fileType, + file_name: `test_${fileType}.yaml`, + version_number: 1, + }; + + const result = ResponseMessageSchema.parse(fileMessage); + expect(result.file_type).toBe(fileType); + } + }); + + it('should validate file message with optional fields', () => { + const fileMessage = { + id: 'file-1', + type: 'file' as const, + file_type: 'dashboard' as const, + file_name: 'dashboard.yaml', + version_number: 2, + filter_version_id: 'filter-123', + metadata: [ + { + status: 'completed' as const, + message: 'File generated successfully', + timestamp: Date.now(), + }, + ], + }; + + const result = ResponseMessageSchema.parse(fileMessage); + expect(result.filter_version_id).toBe('filter-123'); + expect(result.metadata).toHaveLength(1); + }); + + it('should handle null filter_version_id', () => { + const fileMessage = { + id: 'file-1', + type: 'file' as const, + file_type: 'metric' as const, + file_name: 'metric.yaml', + version_number: 1, + filter_version_id: null, + }; + + const result = ResponseMessageSchema.parse(fileMessage); + expect(result.filter_version_id).toBeNull(); + }); + }); + + it('should reject invalid discriminated union types', () => { + const invalidMessage = { + id: 'invalid-1', + type: 'invalid_type', + message: 'This should fail', + }; + + expect(() => ResponseMessageSchema.parse(invalidMessage)).toThrow(); + }); +}); + +describe('ReasoningMessageSchema', () => { + describe('Text Reasoning Message', () => { + it('should validate text reasoning message', () => { + const textReasoning = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Analyzing Data', + status: 'loading' as const, + }; + + const result = ReasoningMessageSchema.parse(textReasoning); + expect(result).toEqual({ ...textReasoning, finished_reasoning: undefined }); + expect(result.type).toBe('text'); + }); + + it('should validate text reasoning with all optional fields', () => { + const textReasoning = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Data Analysis Complete', + secondary_title: 'Summary of findings', + message: 'Analysis completed successfully', + message_chunk: 'Processing chunk 1/5', + status: 'completed' as const, + finished_reasoning: true, + }; + + const result = ReasoningMessageSchema.parse(textReasoning); + expect(result.secondary_title).toBe('Summary of findings'); + expect(result.finished_reasoning).toBe(true); + }); + + it('should validate all status values', () => { + const statusValues = ['loading', 'completed', 'failed'] as const; + + for (const status of statusValues) { + const textReasoning = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Test', + status, + }; + + const result = ReasoningMessageSchema.parse(textReasoning); + expect(result.status).toBe(status); + } + }); + + it('should handle null message fields', () => { + const textReasoning = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Test', + message: null, + message_chunk: null, + status: 'loading' as const, + }; + + const result = ReasoningMessageSchema.parse(textReasoning); + expect(result.message).toBeNull(); + expect(result.message_chunk).toBeNull(); + }); + }); + + describe('Files Reasoning Message', () => { + it('should validate files reasoning message', () => { + const filesReasoning = { + id: 'reasoning-files-1', + type: 'files' as const, + title: 'Generated Files', + status: 'completed' as const, + file_ids: ['file-1', 'file-2'], + files: { + 'file-1': { + id: 'file-1', + file_type: 'metric' as const, + file_name: 'metrics.yaml', + status: 'completed' as const, + file: { text: 'metric content' }, + }, + 'file-2': { + id: 'file-2', + file_type: 'dashboard' as const, + file_name: 'dashboard.yaml', + status: 'loading' as const, + file: {}, + }, + }, + }; + + const result = ReasoningMessageSchema.parse(filesReasoning); + expect(result.type).toBe('files'); + expect(result.file_ids).toHaveLength(2); + expect(result.files['file-1'].file_type).toBe('metric'); + }); + + it('should validate all reasoning file types', () => { + const fileTypes = ['metric', 'dashboard', 'reasoning', 'agent-action', 'todo'] as const; + + for (const fileType of fileTypes) { + const filesReasoning = { + id: 'reasoning-files-1', + type: 'files' as const, + title: 'Test Files', + status: 'completed' as const, + file_ids: [`file-${fileType}`], + files: { + [`file-${fileType}`]: { + id: `file-${fileType}`, + file_type: fileType, + file_name: `test.${fileType}`, + status: 'completed' as const, + file: { text: 'content' }, + }, + }, + }; + + const result = ReasoningMessageSchema.parse(filesReasoning); + expect(result.files[`file-${fileType}`].file_type).toBe(fileType); + } + }); + + it('should validate file with modifications', () => { + const filesReasoning = { + id: 'reasoning-files-1', + type: 'files' as const, + title: 'Modified Files', + status: 'completed' as const, + file_ids: ['file-1'], + files: { + 'file-1': { + id: 'file-1', + file_type: 'metric' as const, + file_name: 'modified_metrics.yaml', + status: 'completed' as const, + file: { + text: 'updated content', + modified: [ + [0, 10], + [20, 30], + ], + }, + }, + }, + }; + + const result = ReasoningMessageSchema.parse(filesReasoning); + expect(result.files['file-1'].file.modified).toEqual([ + [0, 10], + [20, 30], + ]); + }); + }); + + describe('Pills Reasoning Message', () => { + it('should validate pills reasoning message', () => { + const pillsReasoning = { + id: 'reasoning-pills-1', + type: 'pills' as const, + title: 'Related Items', + status: 'completed' as const, + pill_containers: [ + { + title: 'Metrics', + pills: [ + { + text: 'Revenue', + type: 'metric' as const, + id: 'metric-1', + }, + { + text: 'Users', + type: 'metric' as const, + id: 'metric-2', + }, + ], + }, + ], + }; + + const result = ReasoningMessageSchema.parse(pillsReasoning); + expect(result.type).toBe('pills'); + expect(result.pill_containers).toHaveLength(1); + expect(result.pill_containers[0].pills).toHaveLength(2); + }); + + it('should validate all pill types', () => { + const pillTypes = [ + 'metric', + 'dashboard', + 'collection', + 'dataset', + 'term', + 'topic', + 'value', + 'empty', + ] as const; + + for (const pillType of pillTypes) { + const pillsReasoning = { + id: 'reasoning-pills-1', + type: 'pills' as const, + title: 'Test Pills', + status: 'completed' as const, + pill_containers: [ + { + title: 'Test Container', + pills: [ + { + text: `Test ${pillType}`, + type: pillType, + id: `${pillType}-1`, + }, + ], + }, + ], + }; + + const result = ReasoningMessageSchema.parse(pillsReasoning); + expect(result.pill_containers[0].pills[0].type).toBe(pillType); + } + }); + + it('should validate multiple pill containers', () => { + const pillsReasoning = { + id: 'reasoning-pills-1', + type: 'pills' as const, + title: 'Multiple Containers', + status: 'completed' as const, + pill_containers: [ + { + title: 'Metrics', + pills: [{ text: 'Revenue', type: 'metric' as const, id: 'metric-1' }], + }, + { + title: 'Dashboards', + pills: [{ text: 'Sales Dashboard', type: 'dashboard' as const, id: 'dash-1' }], + }, + ], + }; + + const result = ReasoningMessageSchema.parse(pillsReasoning); + expect(result.pill_containers).toHaveLength(2); + expect(result.pill_containers[0].title).toBe('Metrics'); + expect(result.pill_containers[1].title).toBe('Dashboards'); + }); + }); + + it('should validate finished_reasoning field across all types', () => { + const textWithFinished = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Test', + status: 'completed' as const, + finished_reasoning: true, + }; + + const result = ReasoningMessageSchema.parse(textWithFinished); + expect(result.finished_reasoning).toBe(true); + }); +}); + +describe('ChatMessageSchema', () => { + const baseChatMessage = { + id: '123e4567-e89b-12d3-a456-426614174000', + request_message: { + request: 'Show me revenue metrics', + sender_id: 'user-123', + sender_name: 'John Doe', + sender_avatar: 'https://example.com/avatar.jpg', + }, + response_messages: { + 'resp-1': { + id: 'resp-1', + type: 'text' as const, + message: 'Here are your revenue metrics', + }, + }, + response_message_ids: ['resp-1'], + reasoning_message_ids: ['reason-1'], + reasoning_messages: { + 'reason-1': { + id: 'reason-1', + type: 'text' as const, + title: 'Analyzing request', + status: 'completed' as const, + }, + }, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + final_reasoning_message: 'reason-1', + feedback: null, + is_completed: true, + }; + + it('should validate complete chat message', () => { + const result = ChatMessageSchema.parse(baseChatMessage); + expect(result).toEqual(baseChatMessage); + }); + + it('should handle null request_message', () => { + const messageWithNullRequest = { + ...baseChatMessage, + request_message: null, + }; + + const result = ChatMessageSchema.parse(messageWithNullRequest); + expect(result.request_message).toBeNull(); + }); + + it('should handle optional sender_avatar', () => { + const messageWithoutAvatar = { + ...baseChatMessage, + request_message: { + request: 'Test request', + sender_id: 'user-123', + sender_name: 'John Doe', + }, + }; + + const result = ChatMessageSchema.parse(messageWithoutAvatar); + expect(result.request_message?.sender_avatar).toBeUndefined(); + }); + + it('should handle null sender_avatar', () => { + const messageWithNullAvatar = { + ...baseChatMessage, + request_message: { + request: 'Test request', + sender_id: 'user-123', + sender_name: 'John Doe', + sender_avatar: null, + }, + }; + + const result = ChatMessageSchema.parse(messageWithNullAvatar); + expect(result.request_message?.sender_avatar).toBeNull(); + }); + + it('should validate negative feedback', () => { + const messageWithFeedback = { + ...baseChatMessage, + feedback: 'negative' as const, + }; + + const result = ChatMessageSchema.parse(messageWithFeedback); + expect(result.feedback).toBe('negative'); + }); + + it('should validate complex nested structures', () => { + const complexMessage = { + ...baseChatMessage, + response_messages: { + 'text-1': { + id: 'text-1', + type: 'text' as const, + message: 'Analysis complete', + is_final_message: true, + }, + 'file-1': { + id: 'file-1', + type: 'file' as const, + file_type: 'metric' as const, + file_name: 'revenue_metrics.yaml', + version_number: 1, + metadata: [ + { + status: 'completed' as const, + message: 'File generated', + timestamp: Date.now(), + }, + ], + }, + }, + response_message_ids: ['text-1', 'file-1'], + reasoning_messages: { + 'reason-1': { + id: 'reason-1', + type: 'text' as const, + title: 'Processing request', + status: 'completed' as const, + }, + 'reason-2': { + id: 'reason-2', + type: 'files' as const, + title: 'Generated files', + status: 'completed' as const, + file_ids: ['gen-file-1'], + files: { + 'gen-file-1': { + id: 'gen-file-1', + file_type: 'metric' as const, + file_name: 'temp_metric.yaml', + status: 'completed' as const, + file: { text: 'temp content' }, + }, + }, + }, + }, + reasoning_message_ids: ['reason-1', 'reason-2'], + }; + + const result = ChatMessageSchema.parse(complexMessage); + expect(result.response_message_ids).toHaveLength(2); + expect(result.reasoning_message_ids).toHaveLength(2); + expect(result.response_messages['file-1'].type).toBe('file'); + expect(result.reasoning_messages['reason-2'].type).toBe('files'); + }); + + it('should require UUID format for id', () => { + const invalidMessage = { + ...baseChatMessage, + id: 'invalid-uuid', + }; + + expect(() => ChatMessageSchema.parse(invalidMessage)).toThrow(); + }); + + it('should validate empty collections', () => { + const messageWithEmptyCollections = { + ...baseChatMessage, + response_messages: {}, + response_message_ids: [], + reasoning_messages: {}, + reasoning_message_ids: [], + }; + + const result = ChatMessageSchema.parse(messageWithEmptyCollections); + expect(result.response_message_ids).toEqual([]); + expect(result.reasoning_message_ids).toEqual([]); + }); + + it('should have correct type inference', () => { + const message: ChatMessage = baseChatMessage; + expect(message.id).toBe(baseChatMessage.id); + + // Test discriminated union type inference + const textResponse: ChatMessageResponseMessage_Text = { + id: 'text-1', + type: 'text', + message: 'Text response', + }; + expect(textResponse.type).toBe('text'); + + const fileResponse: ChatMessageResponseMessage_File = { + id: 'file-1', + type: 'file', + file_type: 'metric', + file_name: 'test.yaml', + version_number: 1, + }; + expect(fileResponse.type).toBe('file'); + }); +}); diff --git a/packages/server-shared/src/chats/chat.types.test.ts b/packages/server-shared/src/chats/chat.types.test.ts new file mode 100644 index 000000000..c73c3b53c --- /dev/null +++ b/packages/server-shared/src/chats/chat.types.test.ts @@ -0,0 +1,309 @@ +import { describe, expect, it } from 'vitest'; +import { + type AssetPermissionRole, + AssetPermissionRoleSchema, + type BusterShareIndividual, + BusterShareIndividualSchema, + type ChatCreateHandlerRequest, + ChatCreateHandlerRequestSchema, + type ChatCreateRequest, + ChatCreateRequestSchema, + type ChatWithMessages, + ChatWithMessagesSchema, +} from './chat.types'; + +describe('AssetPermissionRoleSchema', () => { + it('should validate correct permission roles', () => { + expect(AssetPermissionRoleSchema.parse('viewer')).toBe('viewer'); + expect(AssetPermissionRoleSchema.parse('editor')).toBe('editor'); + expect(AssetPermissionRoleSchema.parse('owner')).toBe('owner'); + }); + + it('should reject invalid permission roles', () => { + expect(() => AssetPermissionRoleSchema.parse('admin')).toThrow(); + expect(() => AssetPermissionRoleSchema.parse('invalid')).toThrow(); + expect(() => AssetPermissionRoleSchema.parse('')).toThrow(); + expect(() => AssetPermissionRoleSchema.parse(null)).toThrow(); + }); + + it('should have correct type inference', () => { + const role: AssetPermissionRole = 'viewer'; + expect(role).toBe('viewer'); + }); +}); + +describe('BusterShareIndividualSchema', () => { + it('should validate valid individual permission objects', () => { + const validIndividual = { + email: 'test@example.com', + role: 'viewer' as const, + name: 'Test User', + }; + + const result = BusterShareIndividualSchema.parse(validIndividual); + expect(result).toEqual(validIndividual); + }); + + it('should validate without optional name field', () => { + const validIndividual = { + email: 'test@example.com', + role: 'editor' as const, + }; + + const result = BusterShareIndividualSchema.parse(validIndividual); + expect(result).toEqual(validIndividual); + }); + + it('should reject invalid email addresses', () => { + const invalidIndividual = { + email: 'invalid-email', + role: 'viewer' as const, + }; + + expect(() => BusterShareIndividualSchema.parse(invalidIndividual)).toThrow(); + }); + + it('should reject invalid roles', () => { + const invalidIndividual = { + email: 'test@example.com', + role: 'invalid', + }; + + expect(() => BusterShareIndividualSchema.parse(invalidIndividual)).toThrow(); + }); + + it('should require email and role fields', () => { + expect(() => BusterShareIndividualSchema.parse({})).toThrow(); + expect(() => BusterShareIndividualSchema.parse({ email: 'test@example.com' })).toThrow(); + expect(() => BusterShareIndividualSchema.parse({ role: 'viewer' })).toThrow(); + }); + + it('should have correct type inference', () => { + const individual: BusterShareIndividual = { + email: 'test@example.com', + role: 'owner', + name: 'Test User', + }; + expect(individual.email).toBe('test@example.com'); + }); +}); + +describe('ChatWithMessagesSchema', () => { + const baseChatData = { + id: '123e4567-e89b-12d3-a456-426614174000', + title: 'Test Chat', + is_favorited: false, + message_ids: ['msg1', 'msg2'], + messages: { + msg1: { + id: '123e4567-e89b-12d3-a456-426614174001', + request_message: null, + response_messages: {}, + response_message_ids: [], + reasoning_message_ids: [], + reasoning_messages: {}, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + final_reasoning_message: null, + feedback: null, + is_completed: true, + }, + }, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + created_by: 'test-user', + created_by_id: 'user123', + created_by_name: 'Test User', + created_by_avatar: 'https://example.com/avatar.png', + publicly_accessible: false, + }; + + it('should validate valid chat with messages', () => { + const result = ChatWithMessagesSchema.parse(baseChatData); + expect(result).toEqual(baseChatData); + }); + + it('should handle optional fields correctly', () => { + const chatWithOptionals = { + ...baseChatData, + individual_permissions: [ + { + email: 'user@example.com', + role: 'viewer' as const, + name: 'Shared User', + }, + ], + public_expiry_date: '2024-12-31T23:59:59Z', + public_enabled_by: 'admin-user', + public_password: 'secret123', + permission: 'editor' as const, + }; + + const result = ChatWithMessagesSchema.parse(chatWithOptionals); + expect(result.individual_permissions).toHaveLength(1); + expect(result.public_expiry_date).toBe('2024-12-31T23:59:59Z'); + }); + + it('should handle null avatar', () => { + const chatWithNullAvatar = { + ...baseChatData, + created_by_avatar: null, + }; + + const result = ChatWithMessagesSchema.parse(chatWithNullAvatar); + expect(result.created_by_avatar).toBeNull(); + }); + + it('should reject invalid UUID formats', () => { + const invalidChat = { + ...baseChatData, + id: 'invalid-uuid', + }; + + expect(() => ChatWithMessagesSchema.parse(invalidChat)).toThrow(); + }); + + it('should validate nested individual permissions', () => { + const chatWithPermissions = { + ...baseChatData, + individual_permissions: [ + { + email: 'user1@example.com', + role: 'viewer' as const, + }, + { + email: 'user2@example.com', + role: 'editor' as const, + name: 'User Two', + }, + ], + }; + + const result = ChatWithMessagesSchema.parse(chatWithPermissions); + expect(result.individual_permissions).toHaveLength(2); + expect(result.individual_permissions![0].email).toBe('user1@example.com'); + }); + + it('should have correct type inference', () => { + const chat: ChatWithMessages = baseChatData; + expect(chat.id).toBe(baseChatData.id); + }); +}); + +describe('ChatCreateRequestSchema', () => { + it('should validate minimal request', () => { + const minimalRequest = {}; + const result = ChatCreateRequestSchema.parse(minimalRequest); + expect(result).toEqual({}); + }); + + it('should validate complete request with new asset fields', () => { + const request = { + prompt: 'Test prompt', + chat_id: '123e4567-e89b-12d3-a456-426614174000', + message_id: '123e4567-e89b-12d3-a456-426614174001', + asset_id: '123e4567-e89b-12d3-a456-426614174002', + asset_type: 'metric_file' as const, + }; + + const result = ChatCreateRequestSchema.parse(request); + expect(result).toEqual(request); + }); + + it('should validate with legacy fields', () => { + const legacyRequest = { + prompt: 'Test prompt', + metric_id: '123e4567-e89b-12d3-a456-426614174000', + dashboard_id: '123e4567-e89b-12d3-a456-426614174001', + }; + + const result = ChatCreateRequestSchema.parse(legacyRequest); + expect(result).toEqual(legacyRequest); + }); + + it('should require asset_type when asset_id is provided', () => { + const invalidRequest = { + asset_id: '123e4567-e89b-12d3-a456-426614174000', + // Missing asset_type + }; + + expect(() => ChatCreateRequestSchema.parse(invalidRequest)).toThrow(); + }); + + it('should allow asset_type without asset_id', () => { + const validRequest = { + asset_type: 'dashboard_file' as const, + // No asset_id + }; + + const result = ChatCreateRequestSchema.parse(validRequest); + expect(result.asset_type).toBe('dashboard_file'); + }); + + it('should reject invalid UUID formats', () => { + const invalidRequest = { + chat_id: 'invalid-uuid', + }; + + expect(() => ChatCreateRequestSchema.parse(invalidRequest)).toThrow(); + }); + + it('should reject invalid asset types', () => { + const invalidRequest = { + asset_id: '123e4567-e89b-12d3-a456-426614174000', + asset_type: 'invalid_type', + }; + + expect(() => ChatCreateRequestSchema.parse(invalidRequest)).toThrow(); + }); + + it('should have correct type inference', () => { + const request: ChatCreateRequest = { + prompt: 'Test', + asset_type: 'metric_file', + }; + expect(request.prompt).toBe('Test'); + }); +}); + +describe('ChatCreateHandlerRequestSchema', () => { + it('should validate handler request without legacy fields', () => { + const handlerRequest = { + prompt: 'Test prompt', + chat_id: '123e4567-e89b-12d3-a456-426614174000', + message_id: '123e4567-e89b-12d3-a456-426614174001', + asset_id: '123e4567-e89b-12d3-a456-426614174002', + asset_type: 'dashboard_file' as const, + }; + + const result = ChatCreateHandlerRequestSchema.parse(handlerRequest); + expect(result).toEqual(handlerRequest); + }); + + it('should validate minimal handler request', () => { + const minimalRequest = {}; + const result = ChatCreateHandlerRequestSchema.parse(minimalRequest); + expect(result).toEqual({}); + }); + + it('should reject legacy fields (metric_id, dashboard_id)', () => { + const requestWithLegacy = { + prompt: 'Test prompt', + metric_id: '123e4567-e89b-12d3-a456-426614174000', + }; + + // This should still parse since extra fields are typically ignored in zod objects + // unless .strict() is used, but let's verify the schema behavior + const result = ChatCreateHandlerRequestSchema.parse(requestWithLegacy); + expect(result.prompt).toBe('Test prompt'); + expect((result as any).metric_id).toBeUndefined(); + }); + + it('should have correct type inference', () => { + const request: ChatCreateHandlerRequest = { + prompt: 'Test', + asset_type: 'metric_file', + }; + expect(request.prompt).toBe('Test'); + }); +}); diff --git a/packages/server-shared/src/chats/index.test.ts b/packages/server-shared/src/chats/index.test.ts new file mode 100644 index 000000000..2b8e87913 --- /dev/null +++ b/packages/server-shared/src/chats/index.test.ts @@ -0,0 +1,217 @@ +import { describe, expect, it } from 'vitest'; +import { + type AssetPermissionRole, + AssetPermissionRoleSchema, + type BusterShareIndividual, + BusterShareIndividualSchema, + type ChatCreateHandlerRequest, + ChatCreateHandlerRequestSchema, + type ChatCreateRequest, + ChatCreateRequestSchema, + ChatError, + ChatErrorCode, + type ChatErrorResponse, + ChatErrorResponseSchema, + type ChatMessage, + type ChatMessageReasoningMessage, + type ChatMessageResponseMessage, + ChatMessageSchema, + type ChatWithMessages, + ChatWithMessagesSchema, + ReasoningMessageSchema, + ResponseMessageSchema, +} from './index'; + +describe('chats index exports', () => { + it('should export all chat.types schemas', () => { + expect(AssetPermissionRoleSchema).toBeDefined(); + expect(BusterShareIndividualSchema).toBeDefined(); + expect(ChatWithMessagesSchema).toBeDefined(); + expect(ChatCreateRequestSchema).toBeDefined(); + expect(ChatCreateHandlerRequestSchema).toBeDefined(); + }); + + it('should export all chat-errors.types', () => { + expect(ChatErrorCode).toBeDefined(); + expect(ChatErrorResponseSchema).toBeDefined(); + expect(ChatError).toBeDefined(); + }); + + it('should export all chat-message.types schemas', () => { + expect(ChatMessageSchema).toBeDefined(); + expect(ResponseMessageSchema).toBeDefined(); + expect(ReasoningMessageSchema).toBeDefined(); + }); + + it('should have working type inference', () => { + // Test that types are properly exported + const role: AssetPermissionRole = 'viewer'; + expect(role).toBe('viewer'); + + const individual: BusterShareIndividual = { + email: 'test@example.com', + role: 'editor', + }; + expect(individual.email).toBe('test@example.com'); + }); + + it('should validate schemas work from chats index', () => { + // Test AssetPermissionRoleSchema + expect(AssetPermissionRoleSchema.parse('viewer')).toBe('viewer'); + expect(() => AssetPermissionRoleSchema.parse('invalid')).toThrow(); + + // Test BusterShareIndividualSchema + const validIndividual = { + email: 'test@example.com', + role: 'owner' as const, + name: 'Test User', + }; + expect(() => BusterShareIndividualSchema.parse(validIndividual)).not.toThrow(); + + // Test ChatErrorResponseSchema + const validErrorResponse = { + code: 'INVALID_REQUEST', + message: 'Test error message', + }; + expect(() => ChatErrorResponseSchema.parse(validErrorResponse)).not.toThrow(); + }); + + it('should have working ChatError class', () => { + const error = new ChatError(ChatErrorCode.PERMISSION_DENIED, 'Access denied', 403, { + resource: 'chat', + }); + + expect(error).toBeInstanceOf(Error); + expect(error.code).toBe('PERMISSION_DENIED'); + expect(error.statusCode).toBe(403); + expect(error.details).toEqual({ resource: 'chat' }); + + const response = error.toResponse(); + expect(response.code).toBe('PERMISSION_DENIED'); + expect(response.message).toBe('Access denied'); + expect(response.details).toEqual({ resource: 'chat' }); + }); + + it('should validate response message discriminated union', () => { + const textMessage = { + id: 'text-1', + type: 'text' as const, + message: 'Hello world', + }; + + const fileMessage = { + id: 'file-1', + type: 'file' as const, + file_type: 'metric' as const, + file_name: 'test.yaml', + version_number: 1, + }; + + expect(() => ResponseMessageSchema.parse(textMessage)).not.toThrow(); + expect(() => ResponseMessageSchema.parse(fileMessage)).not.toThrow(); + }); + + it('should validate reasoning message discriminated union', () => { + const textReasoning = { + id: 'reason-1', + type: 'text' as const, + title: 'Processing', + status: 'loading' as const, + }; + + const filesReasoning = { + id: 'reason-2', + type: 'files' as const, + title: 'Generated Files', + status: 'completed' as const, + file_ids: ['file-1'], + files: { + 'file-1': { + id: 'file-1', + file_type: 'metric' as const, + file_name: 'test.yaml', + status: 'completed' as const, + file: { text: 'content' }, + }, + }, + }; + + const pillsReasoning = { + id: 'reason-3', + type: 'pills' as const, + title: 'Related Items', + status: 'completed' as const, + pill_containers: [ + { + title: 'Metrics', + pills: [ + { + text: 'Revenue', + type: 'metric' as const, + id: 'metric-1', + }, + ], + }, + ], + }; + + expect(() => ReasoningMessageSchema.parse(textReasoning)).not.toThrow(); + expect(() => ReasoningMessageSchema.parse(filesReasoning)).not.toThrow(); + expect(() => ReasoningMessageSchema.parse(pillsReasoning)).not.toThrow(); + }); + + it('should validate complete chat message', () => { + const chatMessage = { + id: '123e4567-e89b-12d3-a456-426614174000', + request_message: { + request: 'Show me metrics', + sender_id: 'user-123', + sender_name: 'John Doe', + }, + response_messages: { + 'resp-1': { + id: 'resp-1', + type: 'text' as const, + message: 'Here are your metrics', + }, + }, + response_message_ids: ['resp-1'], + reasoning_message_ids: ['reason-1'], + reasoning_messages: { + 'reason-1': { + id: 'reason-1', + type: 'text' as const, + title: 'Processing request', + status: 'completed' as const, + }, + }, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + final_reasoning_message: null, + feedback: null, + is_completed: true, + }; + + expect(() => ChatMessageSchema.parse(chatMessage)).not.toThrow(); + }); + + it('should validate chat creation requests', () => { + const minimalRequest = {}; + expect(() => ChatCreateRequestSchema.parse(minimalRequest)).not.toThrow(); + + const fullRequest = { + prompt: 'Create a dashboard', + chat_id: '123e4567-e89b-12d3-a456-426614174000', + message_id: '123e4567-e89b-12d3-a456-426614174001', + asset_id: '123e4567-e89b-12d3-a456-426614174002', + asset_type: 'dashboard_file' as const, + }; + expect(() => ChatCreateRequestSchema.parse(fullRequest)).not.toThrow(); + + const handlerRequest = { + prompt: 'Create a metric', + asset_type: 'metric_file' as const, + }; + expect(() => ChatCreateHandlerRequestSchema.parse(handlerRequest)).not.toThrow(); + }); +}); diff --git a/packages/server-shared/src/currency/currency.types.test.ts b/packages/server-shared/src/currency/currency.types.test.ts new file mode 100644 index 000000000..3fb27b88e --- /dev/null +++ b/packages/server-shared/src/currency/currency.types.test.ts @@ -0,0 +1,249 @@ +import { describe, expect, it } from 'vitest'; +import { type Currency, CurrencySchema } from './currency.types'; + +describe('CurrencySchema', () => { + it('should validate valid currency object', () => { + const validCurrency = { + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + + const result = CurrencySchema.parse(validCurrency); + expect(result).toEqual(validCurrency); + }); + + it('should validate different currency codes', () => { + const currencies = [ + { code: 'EUR', description: 'Euro', flag: '๐Ÿ‡ช๐Ÿ‡บ' }, + { code: 'GBP', description: 'British Pound Sterling', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + { code: 'JPY', description: 'Japanese Yen', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + { code: 'CAD', description: 'Canadian Dollar', flag: '๐Ÿ‡จ๐Ÿ‡ฆ' }, + { code: 'AUD', description: 'Australian Dollar', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, + ]; + + for (const currency of currencies) { + const result = CurrencySchema.parse(currency); + expect(result).toEqual(currency); + expect(result.code).toBe(currency.code); + } + }); + + it('should accept empty strings for fields', () => { + const currencyWithEmptyStrings = { + code: '', + description: '', + flag: '', + }; + + const result = CurrencySchema.parse(currencyWithEmptyStrings); + expect(result).toEqual(currencyWithEmptyStrings); + }); + + it('should accept long descriptions', () => { + const currencyWithLongDescription = { + code: 'BTC', + description: + 'Bitcoin - A decentralized digital currency that can be transferred on the peer-to-peer bitcoin network', + flag: 'โ‚ฟ', + }; + + const result = CurrencySchema.parse(currencyWithLongDescription); + expect(result.description).toBe(currencyWithLongDescription.description); + }); + + it('should accept special characters in flag', () => { + const currencyWithSpecialFlag = { + code: 'XAU', + description: 'Gold (troy ounce)', + flag: '๐Ÿฅ‡', + }; + + const result = CurrencySchema.parse(currencyWithSpecialFlag); + expect(result.flag).toBe('๐Ÿฅ‡'); + }); + + it('should reject missing required fields', () => { + expect(() => CurrencySchema.parse({})).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + description: 'United States Dollar', + // missing flag + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + // missing description + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + // missing code + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + }); + + it('should reject non-string values', () => { + expect(() => + CurrencySchema.parse({ + code: 123, + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + description: null, + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + description: 'United States Dollar', + flag: true, + }), + ).toThrow(); + }); + + it('should reject undefined values', () => { + expect(() => + CurrencySchema.parse({ + code: undefined, + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + description: undefined, + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }), + ).toThrow(); + + expect(() => + CurrencySchema.parse({ + code: 'USD', + description: 'United States Dollar', + flag: undefined, + }), + ).toThrow(); + }); + + it('should handle extra properties gracefully', () => { + const currencyWithExtra = { + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + extraProperty: 'this should be ignored', + anotherExtra: 123, + }; + + const result = CurrencySchema.parse(currencyWithExtra); + + // Zod objects by default strip unknown properties + expect(result).toEqual({ + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }); + expect((result as any).extraProperty).toBeUndefined(); + }); + + it('should validate whitespace strings', () => { + const currencyWithWhitespace = { + code: ' ', + description: '\t\n', + flag: ' ', + }; + + const result = CurrencySchema.parse(currencyWithWhitespace); + expect(result.code).toBe(' '); + expect(result.description).toBe('\t\n'); + expect(result.flag).toBe(' '); + }); + + it('should validate complex Unicode characters', () => { + const currencyWithUnicode = { + code: 'ู…๏ปผ๏บฐ๏ปฒ', // Arabic text + description: 'ไธญๆ–‡่ดงๅธๆ่ฟฐ', // Chinese text + flag: '๐Ÿดโ€โ˜ ๏ธ', // Complex emoji + }; + + const result = CurrencySchema.parse(currencyWithUnicode); + expect(result).toEqual(currencyWithUnicode); + }); + + it('should have correct type inference', () => { + const currency: Currency = { + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + + expect(currency.code).toBe('USD'); + expect(currency.description).toBe('United States Dollar'); + expect(currency.flag).toBe('๐Ÿ‡บ๐Ÿ‡ธ'); + }); + + it('should work with safeParse for error handling', () => { + const validCurrency = { + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + + const validResult = CurrencySchema.safeParse(validCurrency); + expect(validResult.success).toBe(true); + if (validResult.success) { + expect(validResult.data).toEqual(validCurrency); + } + + const invalidCurrency = { + code: 123, + description: 'Invalid', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + + const invalidResult = CurrencySchema.safeParse(invalidCurrency); + expect(invalidResult.success).toBe(false); + if (!invalidResult.success) { + expect(invalidResult.error).toBeDefined(); + expect(invalidResult.error.issues).toHaveLength(1); + expect(invalidResult.error.issues[0].path).toEqual(['code']); + } + }); + + it('should validate real-world currency examples', () => { + const realCurrencies = [ + { code: 'USD', description: 'United States Dollar', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'EUR', description: 'Euro', flag: '๐Ÿ‡ช๐Ÿ‡บ' }, + { code: 'JPY', description: 'Japanese Yen', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + { code: 'GBP', description: 'British Pound Sterling', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + { code: 'CHF', description: 'Swiss Franc', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, + { code: 'CNY', description: 'Chinese Yuan', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + { code: 'INR', description: 'Indian Rupee', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + { code: 'BRL', description: 'Brazilian Real', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, + { code: 'KRW', description: 'South Korean Won', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + { code: 'MXN', description: 'Mexican Peso', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, + ]; + + for (const currency of realCurrencies) { + const result = CurrencySchema.parse(currency); + expect(result).toEqual(currency); + } + }); +}); diff --git a/packages/server-shared/src/currency/index.test.ts b/packages/server-shared/src/currency/index.test.ts new file mode 100644 index 000000000..ea9222cda --- /dev/null +++ b/packages/server-shared/src/currency/index.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { type Currency, CurrencySchema } from './index'; + +describe('currency index exports', () => { + it('should export CurrencySchema', () => { + expect(CurrencySchema).toBeDefined(); + expect(typeof CurrencySchema.parse).toBe('function'); + }); + + it('should have working Currency type', () => { + // Test type inference + const currency: Currency = { + code: 'USD', + description: 'United States Dollar', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + expect(currency.code).toBe('USD'); + }); + + it('should validate currency through index export', () => { + const validCurrency = { + code: 'EUR', + description: 'Euro', + flag: '๐Ÿ‡ช๐Ÿ‡บ', + }; + + const result = CurrencySchema.parse(validCurrency); + expect(result).toEqual(validCurrency); + }); + + it('should reject invalid currency through index export', () => { + const invalidCurrency = { + code: 123, + description: 'Invalid', + flag: '๐Ÿ‡บ๐Ÿ‡ธ', + }; + + expect(() => CurrencySchema.parse(invalidCurrency)).toThrow(); + }); + + it('should work with safeParse from index', () => { + const validCurrency = { + code: 'GBP', + description: 'British Pound Sterling', + flag: '๐Ÿ‡ฌ๐Ÿ‡ง', + }; + + const result = CurrencySchema.safeParse(validCurrency); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(validCurrency); + } + }); + + it('should handle multiple currency validations from index', () => { + const currencies = [ + { code: 'USD', description: 'United States Dollar', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'EUR', description: 'Euro', flag: '๐Ÿ‡ช๐Ÿ‡บ' }, + { code: 'JPY', description: 'Japanese Yen', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + ]; + + for (const currency of currencies) { + expect(() => CurrencySchema.parse(currency)).not.toThrow(); + const result = CurrencySchema.parse(currency); + expect(result.code).toBe(currency.code); + } + }); +}); diff --git a/packages/server-shared/src/index.test.ts b/packages/server-shared/src/index.test.ts new file mode 100644 index 000000000..40c898fc6 --- /dev/null +++ b/packages/server-shared/src/index.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { + AssetPermissionRoleSchema, + BusterShareIndividualSchema, + ChatCreateHandlerRequestSchema, + ChatCreateRequestSchema, + ChatError, + ChatErrorCode, + ChatErrorResponseSchema, + ChatMessageSchema, + ChatWithMessagesSchema, + ReasoningMessageSchema, + ResponseMessageSchema, +} from './index'; + +describe('server-shared main index exports', () => { + it('should export all chat types', () => { + // Check that chat-related exports are available + expect(AssetPermissionRoleSchema).toBeDefined(); + expect(BusterShareIndividualSchema).toBeDefined(); + expect(ChatWithMessagesSchema).toBeDefined(); + expect(ChatCreateRequestSchema).toBeDefined(); + expect(ChatCreateHandlerRequestSchema).toBeDefined(); + }); + + it('should export chat error types', () => { + expect(ChatErrorCode).toBeDefined(); + expect(ChatErrorResponseSchema).toBeDefined(); + expect(ChatError).toBeDefined(); + }); + + it('should export chat message types', () => { + expect(ChatMessageSchema).toBeDefined(); + expect(ResponseMessageSchema).toBeDefined(); + expect(ReasoningMessageSchema).toBeDefined(); + }); + + it('should have working schema validation from exports', () => { + expect(AssetPermissionRoleSchema.parse('viewer')).toBe('viewer'); + expect(AssetPermissionRoleSchema.parse('editor')).toBe('editor'); + expect(AssetPermissionRoleSchema.parse('owner')).toBe('owner'); + }); + + it('should have working ChatError class from exports', () => { + const error = new ChatError(ChatErrorCode.INVALID_REQUEST, 'Test error'); + expect(error).toBeInstanceOf(Error); + expect(error.code).toBe('INVALID_REQUEST'); + expect(error.message).toBe('Test error'); + }); + + it('should validate chat message schema works', () => { + const validMessage = { + id: '123e4567-e89b-12d3-a456-426614174000', + request_message: null, + response_messages: {}, + response_message_ids: [], + reasoning_message_ids: [], + reasoning_messages: {}, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + final_reasoning_message: null, + feedback: null, + is_completed: true, + }; + + expect(() => ChatMessageSchema.parse(validMessage)).not.toThrow(); + }); + + it('should validate response message schema works', () => { + const textMessage = { + id: 'msg-1', + type: 'text' as const, + message: 'Test response', + }; + + expect(() => ResponseMessageSchema.parse(textMessage)).not.toThrow(); + }); + + it('should validate reasoning message schema works', () => { + const reasoningMessage = { + id: 'reasoning-1', + type: 'text' as const, + title: 'Test reasoning', + status: 'completed' as const, + }; + + expect(() => ReasoningMessageSchema.parse(reasoningMessage)).not.toThrow(); + }); +}); diff --git a/packages/server-shared/vitest.config.ts b/packages/server-shared/vitest.config.ts new file mode 100644 index 000000000..d86b4007a --- /dev/null +++ b/packages/server-shared/vitest.config.ts @@ -0,0 +1,3 @@ +import { baseConfig } from '@buster/vitest-config'; + +export default baseConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf6b11dbf..53bf4756d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -763,6 +763,13 @@ importers: zod: specifier: 'catalog:' version: 3.25.67 + devDependencies: + '@buster/vitest-config': + specifier: workspace:* + version: link:../vitest-config + vitest: + specifier: 'catalog:' + version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@20.19.2)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.2(@types/node@20.19.2)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) packages/slack: dependencies: