Yuki Takei 9 месяцев назад
Родитель
Сommit
3ee4adfa17

+ 457 - 0
apps/app/src/features/openai/interfaces/editor-assistant/llm-response-schemas.spec.ts

@@ -0,0 +1,457 @@
+import {
+  describe, test, expect,
+} from 'vitest';
+
+import {
+  LlmEditorAssistantMessageSchema,
+  LlmEditorAssistantDiffSchema,
+  type LlmEditorAssistantMessage,
+  type LlmEditorAssistantDiff,
+} from './llm-response-schemas';
+
+describe('llm-response-schemas', () => {
+  describe('LlmEditorAssistantMessageSchema', () => {
+    test('should validate valid message objects', () => {
+      const validMessage = {
+        message: 'I have successfully updated the function to include error handling.',
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.message).toBe(validMessage.message);
+      }
+    });
+
+    test('should validate empty message string', () => {
+      const validMessage = {
+        message: '',
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate message with special characters and unicode', () => {
+      const validMessage = {
+        message: 'ファイルを更新しました! 🎉 Special chars: @#$%^&*()',
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate multiline messages', () => {
+      const validMessage = {
+        message: `Line 1: Updated function signature
+Line 2: Added error handling
+Line 3: Fixed indentation`,
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should fail when message field is missing', () => {
+      const invalidMessage = {};
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(invalidMessage);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].code).toBe('invalid_type');
+        expect(result.error.issues[0].path).toEqual(['message']);
+      }
+    });
+
+    test('should fail when message is not a string', () => {
+      const invalidMessage = {
+        message: 123,
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(invalidMessage);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].code).toBe('invalid_type');
+        expect(result.error.issues[0].expected).toBe('string');
+      }
+    });
+
+    test('should fail when message is null', () => {
+      const invalidMessage = {
+        message: null,
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(invalidMessage);
+      expect(result.success).toBe(false);
+    });
+
+    test('should allow extra unexpected fields (non-strict mode)', () => {
+      const validMessage = {
+        message: 'Valid message',
+        extraField: 'unexpected',
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.message).toBe('Valid message');
+        // Extra fields are ignored in non-strict mode
+        expect((result.data as any).extraField).toBeUndefined();
+      }
+    });
+  });
+
+  describe('LlmEditorAssistantDiffSchema', () => {
+    test('should validate complete diff object with all fields', () => {
+      const validDiff = {
+        search: 'function oldName() {\n  return "old";\n}',
+        replace: 'function newName() {\n  return "new";\n}',
+        startLine: 5,
+        endLine: 7,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.search).toBe(validDiff.search);
+        expect(result.data.replace).toBe(validDiff.replace);
+        expect(result.data.startLine).toBe(validDiff.startLine);
+        expect(result.data.endLine).toBe(validDiff.endLine);
+      }
+    });
+
+    test('should validate diff object without optional endLine', () => {
+      const validDiff = {
+        search: 'const value = 42;',
+        replace: 'const value = 100;',
+        startLine: 10,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.endLine).toBeUndefined();
+      }
+    });
+
+    test('should validate diff object with null endLine', () => {
+      const validDiff = {
+        search: 'console.log("test");',
+        replace: 'console.log("updated");',
+        startLine: 1,
+        endLine: null,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.endLine).toBeNull();
+      }
+    });
+
+    test('should validate diff with whitespace and indentation in search/replace', () => {
+      const validDiff = {
+        search: '  if (condition) {\n    doSomething();\n  }',
+        replace: '  if (newCondition) {\n    doSomethingElse();\n  }',
+        startLine: 15,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate diff with empty replace content', () => {
+      const validDiff = {
+        search: 'lineToDelete();',
+        replace: '',
+        startLine: 20,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate diff with unicode and special characters', () => {
+      const validDiff = {
+        search: 'const message = "Hello";',
+        replace: 'const message = "こんにちは 🌍";',
+        startLine: 8,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should fail when search field is missing', () => {
+      const invalidDiff = {
+        replace: 'new content',
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const searchError = result.error.issues.find(issue => issue.path.includes('search'));
+        expect(searchError).toBeDefined();
+        expect(searchError?.code).toBe('invalid_type');
+      }
+    });
+
+    test('should fail when replace field is missing', () => {
+      const invalidDiff = {
+        search: 'old content',
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const replaceError = result.error.issues.find(issue => issue.path.includes('replace'));
+        expect(replaceError).toBeDefined();
+        expect(replaceError?.code).toBe('invalid_type');
+      }
+    });
+
+    test('should fail when startLine field is missing', () => {
+      const invalidDiff = {
+        search: 'old content',
+        replace: 'new content',
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const startLineError = result.error.issues.find(issue => issue.path.includes('startLine'));
+        expect(startLineError).toBeDefined();
+        expect(startLineError?.code).toBe('invalid_type');
+      }
+    });
+
+    test('should fail when search is empty string', () => {
+      const invalidDiff = {
+        search: '',
+        replace: 'new content',
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const searchError = result.error.issues.find(issue => issue.path.includes('search'));
+        expect(searchError?.code).toBe('too_small');
+      }
+    });
+
+    test('should fail when startLine is zero', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 0,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const startLineError = result.error.issues.find(issue => issue.path.includes('startLine'));
+        expect(startLineError?.code).toBe('too_small');
+      }
+    });
+
+    test('should fail when startLine is negative', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: -1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when startLine is not an integer', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 1.5,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const startLineError = result.error.issues.find(issue => issue.path.includes('startLine'));
+        expect(startLineError?.code).toBe('invalid_type');
+      }
+    });
+
+    test('should fail when endLine is zero', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 1,
+        endLine: 0,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const endLineError = result.error.issues.find(issue => issue.path.includes('endLine'));
+        expect(endLineError?.code).toBe('too_small');
+      }
+    });
+
+    test('should fail when endLine is negative', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 1,
+        endLine: -1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when endLine is not an integer', () => {
+      const invalidDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 1,
+        endLine: 2.7,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail with non-string search content', () => {
+      const invalidDiff = {
+        search: 123,
+        replace: 'new content',
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const searchError = result.error.issues.find(issue => issue.path.includes('search'));
+        expect(searchError?.code).toBe('invalid_type');
+        expect(searchError?.expected).toBe('string');
+      }
+    });
+
+    test('should fail with non-string replace content', () => {
+      const invalidDiff = {
+        search: 'old content',
+        replace: { content: 'new' },
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(invalidDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const replaceError = result.error.issues.find(issue => issue.path.includes('replace'));
+        expect(replaceError?.code).toBe('invalid_type');
+        expect(replaceError?.expected).toBe('string');
+      }
+    });
+
+    test('should allow extra unexpected fields (non-strict mode)', () => {
+      const validDiff = {
+        search: 'content',
+        replace: 'new content',
+        startLine: 1,
+        unexpectedField: 'value',
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(validDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.search).toBe('content');
+        expect(result.data.replace).toBe('new content');
+        expect(result.data.startLine).toBe(1);
+        // Extra fields are ignored in non-strict mode
+      }
+    });
+  });
+
+  describe('Type inference', () => {
+    test('LlmEditorAssistantMessage type should match schema', () => {
+      const message: LlmEditorAssistantMessage = {
+        message: 'Test message',
+      };
+
+      const result = LlmEditorAssistantMessageSchema.safeParse(message);
+      expect(result.success).toBe(true);
+    });
+
+    test('LlmEditorAssistantDiff type should match schema', () => {
+      const diff: LlmEditorAssistantDiff = {
+        search: 'old code',
+        replace: 'new code',
+        startLine: 1,
+        endLine: 2,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(diff);
+      expect(result.success).toBe(true);
+    });
+
+    test('LlmEditorAssistantDiff type should work without optional fields', () => {
+      const diff: LlmEditorAssistantDiff = {
+        search: 'old code',
+        replace: 'new code',
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(diff);
+      expect(result.success).toBe(true);
+    });
+  });
+
+  describe('Real-world scenarios', () => {
+    test('should validate typical code replacement scenario', () => {
+      const realWorldDiff = {
+        search: 'function getUserData(id) {\n  return users.find(u => u.id === id);\n}',
+        replace: 'async function getUserData(id) {\n  const user = await userService.findById(id);\n  if (!user) {\n    throw new Error(`User not found: ${id}`);\n  }\n  return user;\n}',
+        startLine: 15,
+        endLine: 17,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(realWorldDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate import statement replacement', () => {
+      const importDiff = {
+        search: "import { Component } from 'react';",
+        replace: "import React, { Component } from 'react';",
+        startLine: 1,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(importDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate comment addition', () => {
+      const commentDiff = {
+        search: 'const result = processData(input);',
+        replace: '// Process the input data and return the result\nconst result = processData(input);',
+        startLine: 42,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(commentDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate line deletion scenario', () => {
+      const deletionDiff = {
+        search: 'console.log("Debug message");',
+        replace: '',
+        startLine: 100,
+      };
+
+      const result = LlmEditorAssistantDiffSchema.safeParse(deletionDiff);
+      expect(result.success).toBe(true);
+    });
+  });
+});