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

+ 636 - 0
apps/app/src/features/openai/interfaces/editor-assistant/sse-schemas.spec.ts

@@ -0,0 +1,636 @@
+import {
+  describe, test, expect,
+} from 'vitest';
+
+import {
+  SseMessageSchema,
+  SseDetectedDiffSchema,
+  SseFinalizedSchema,
+  hasApplicationResult,
+  type SseMessage,
+  type SseDetectedDiff,
+  type SseFinalized,
+} from './sse-schemas';
+
+describe('sse-schemas', () => {
+  describe('SseMessageSchema', () => {
+    test('should validate valid SSE message', () => {
+      const validMessage = {
+        appendedMessage: 'Processing your request...',
+      };
+
+      const result = SseMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.appendedMessage).toBe(validMessage.appendedMessage);
+      }
+    });
+
+    test('should validate empty appended message', () => {
+      const validMessage = {
+        appendedMessage: '',
+      };
+
+      const result = SseMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate multiline appended message', () => {
+      const validMessage = {
+        appendedMessage: 'Step 1: Analyzing code\nStep 2: Preparing changes\nStep 3: Applying diff',
+      };
+
+      const result = SseMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate unicode and special characters', () => {
+      const validMessage = {
+        appendedMessage: 'コードを更新中... 🚀 Progress: 75%',
+      };
+
+      const result = SseMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+
+    test('should fail when appendedMessage field is missing', () => {
+      const invalidMessage = {};
+
+      const result = SseMessageSchema.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(['appendedMessage']);
+      }
+    });
+
+    test('should fail when appendedMessage is not a string', () => {
+      const invalidMessage = {
+        appendedMessage: 123,
+      };
+
+      const result = SseMessageSchema.safeParse(invalidMessage);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when appendedMessage is null', () => {
+      const invalidMessage = {
+        appendedMessage: null,
+      };
+
+      const result = SseMessageSchema.safeParse(invalidMessage);
+      expect(result.success).toBe(false);
+    });
+
+    test('should allow extra fields (non-strict mode)', () => {
+      const validMessage = {
+        appendedMessage: 'Valid message',
+        extraField: 'ignored',
+      };
+
+      const result = SseMessageSchema.safeParse(validMessage);
+      expect(result.success).toBe(true);
+    });
+  });
+
+  describe('SseDetectedDiffSchema', () => {
+    test('should validate detected diff with complete diff object', () => {
+      const validDetectedDiff = {
+        diff: {
+          search: 'function oldCode() {',
+          replace: 'function newCode() {',
+          startLine: 10,
+          endLine: 12,
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(validDetectedDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.diff.search).toBe(validDetectedDiff.diff.search);
+        expect(result.data.diff.replace).toBe(validDetectedDiff.diff.replace);
+        expect(result.data.diff.startLine).toBe(validDetectedDiff.diff.startLine);
+        expect(result.data.diff.endLine).toBe(validDetectedDiff.diff.endLine);
+      }
+    });
+
+    test('should validate detected diff without optional endLine', () => {
+      const validDetectedDiff = {
+        diff: {
+          search: 'const value = 42;',
+          replace: 'const value = 100;',
+          startLine: 5,
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(validDetectedDiff);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.diff.endLine).toBeUndefined();
+      }
+    });
+
+    test('should validate diff with empty replace content', () => {
+      const validDetectedDiff = {
+        diff: {
+          search: 'lineToDelete();',
+          replace: '',
+          startLine: 20,
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(validDetectedDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('should fail when diff field is missing', () => {
+      const invalidDetectedDiff = {};
+
+      const result = SseDetectedDiffSchema.safeParse(invalidDetectedDiff);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].code).toBe('invalid_type');
+        expect(result.error.issues[0].path).toEqual(['diff']);
+      }
+    });
+
+    test('should fail when diff has invalid structure', () => {
+      const invalidDetectedDiff = {
+        diff: {
+          search: 'valid search',
+          replace: 'valid replace',
+          // missing required startLine
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(invalidDetectedDiff);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when diff search is empty', () => {
+      const invalidDetectedDiff = {
+        diff: {
+          search: '',
+          replace: 'replacement',
+          startLine: 1,
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(invalidDetectedDiff);
+      expect(result.success).toBe(false);
+    });
+
+    test('should allow extra fields in detected diff', () => {
+      const validDetectedDiff = {
+        diff: {
+          search: 'code',
+          replace: 'new code',
+          startLine: 1,
+        },
+        extraField: 'ignored',
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(validDetectedDiff);
+      expect(result.success).toBe(true);
+    });
+  });
+
+  describe('SseFinalizedSchema', () => {
+    test('should validate minimal finalized response', () => {
+      const validFinalized = {
+        finalized: {
+          message: 'Changes have been applied successfully.',
+          replacements: [],
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(validFinalized);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.finalized.message).toBe(validFinalized.finalized.message);
+        expect(result.data.finalized.replacements).toEqual([]);
+        expect(result.data.finalized.applicationResult).toBeUndefined();
+      }
+    });
+
+    test('should validate finalized response with replacements', () => {
+      const validFinalized = {
+        finalized: {
+          message: 'Successfully applied 2 changes.',
+          replacements: [
+            {
+              search: 'old code 1',
+              replace: 'new code 1',
+              startLine: 5,
+            },
+            {
+              search: 'old code 2',
+              replace: 'new code 2',
+              startLine: 10,
+              endLine: 12,
+            },
+          ],
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(validFinalized);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.finalized.replacements).toHaveLength(2);
+        expect(result.data.finalized.replacements[0].search).toBe('old code 1');
+        expect(result.data.finalized.replacements[1].endLine).toBe(12);
+      }
+    });
+
+    test('should validate finalized response with successful application result', () => {
+      const validFinalized = {
+        finalized: {
+          message: 'All changes applied successfully.',
+          replacements: [
+            {
+              search: 'test code',
+              replace: 'updated code',
+              startLine: 1,
+            },
+          ],
+          applicationResult: {
+            success: true,
+            appliedCount: 1,
+            totalCount: 1,
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(validFinalized);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.finalized.applicationResult?.success).toBe(true);
+        expect(result.data.finalized.applicationResult?.appliedCount).toBe(1);
+        expect(result.data.finalized.applicationResult?.totalCount).toBe(1);
+      }
+    });
+
+    test('should validate finalized response with failed application result', () => {
+      const validFinalized = {
+        finalized: {
+          message: 'Some changes failed to apply.',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: 0,
+            totalCount: 1,
+            failedParts: [
+              {
+                type: 'SEARCH_NOT_FOUND',
+                message: 'Could not find the specified search content',
+                line: 10,
+                details: {
+                  searchContent: 'missing code',
+                  bestMatch: 'similar code',
+                  similarity: 0.7,
+                  suggestions: ['Check line numbers', 'Verify content'],
+                  correctFormat: 'function example() {}',
+                  lineRange: '8-12',
+                },
+              },
+            ],
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(validFinalized);
+      expect(result.success).toBe(true);
+      if (result.success) {
+        expect(result.data.finalized.applicationResult?.success).toBe(false);
+        expect(result.data.finalized.applicationResult?.failedParts).toHaveLength(1);
+        expect(result.data.finalized.applicationResult?.failedParts?.[0].type).toBe('SEARCH_NOT_FOUND');
+        expect(result.data.finalized.applicationResult?.failedParts?.[0].details.similarity).toBe(0.7);
+      }
+    });
+
+    test('should validate all error types in failedParts', () => {
+      const errorTypes = [
+        'SEARCH_NOT_FOUND',
+        'SIMILARITY_TOO_LOW',
+        'MULTIPLE_MATCHES',
+        'EMPTY_SEARCH',
+        'MARKER_SEQUENCE_ERROR',
+        'CONTENT_ERROR',
+      ];
+
+      for (const errorType of errorTypes) {
+        const validFinalized = {
+          finalized: {
+            message: `Error type: ${errorType}`,
+            replacements: [],
+            applicationResult: {
+              success: false,
+              appliedCount: 0,
+              totalCount: 1,
+              failedParts: [
+                {
+                  type: errorType,
+                  message: `Test message for ${errorType}`,
+                  details: {
+                    searchContent: 'test',
+                    suggestions: [],
+                  },
+                },
+              ],
+            },
+          },
+        };
+
+        const result = SseFinalizedSchema.safeParse(validFinalized);
+        expect(result.success).toBe(true);
+        if (result.success) {
+          expect(result.data.finalized.applicationResult?.failedParts?.[0].type).toBe(errorType);
+        }
+      }
+    });
+
+    test('should fail when finalized field is missing', () => {
+      const invalidFinalized = {};
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        expect(result.error.issues[0].code).toBe('invalid_type');
+        expect(result.error.issues[0].path).toEqual(['finalized']);
+      }
+    });
+
+    test('should fail when message is missing', () => {
+      const invalidFinalized = {
+        finalized: {
+          replacements: [],
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const messageError = result.error.issues.find(issue => issue.path.includes('message'));
+        expect(messageError).toBeDefined();
+      }
+    });
+
+    test('should fail when replacements is missing', () => {
+      const invalidFinalized = {
+        finalized: {
+          message: 'Test message',
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+      if (!result.success) {
+        const replacementsError = result.error.issues.find(issue => issue.path.includes('replacements'));
+        expect(replacementsError).toBeDefined();
+      }
+    });
+
+    test('should fail when appliedCount is negative', () => {
+      const invalidFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: -1,
+            totalCount: 1,
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when totalCount is negative', () => {
+      const invalidFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: 0,
+            totalCount: -1,
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when similarity is out of range', () => {
+      const invalidFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: 0,
+            totalCount: 1,
+            failedParts: [
+              {
+                type: 'SIMILARITY_TOO_LOW',
+                message: 'Low similarity',
+                details: {
+                  searchContent: 'test',
+                  similarity: 1.5, // Invalid: > 1.0
+                  suggestions: [],
+                },
+              },
+            ],
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+    });
+
+    test('should fail when line number is not positive', () => {
+      const invalidFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: 0,
+            totalCount: 1,
+            failedParts: [
+              {
+                type: 'SEARCH_NOT_FOUND',
+                message: 'Not found',
+                line: 0, // Invalid: must be positive
+                details: {
+                  searchContent: 'test',
+                  suggestions: [],
+                },
+              },
+            ],
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(invalidFinalized);
+      expect(result.success).toBe(false);
+    });
+
+    test('should allow extra fields in finalized', () => {
+      const validFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+        },
+        extraField: 'ignored',
+      };
+
+      const result = SseFinalizedSchema.safeParse(validFinalized);
+      expect(result.success).toBe(true);
+    });
+  });
+
+  describe('hasApplicationResult helper', () => {
+    test('should return true when applicationResult is present', () => {
+      const finalized: SseFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: {
+            success: true,
+            appliedCount: 1,
+            totalCount: 1,
+          },
+        },
+      };
+
+      expect(hasApplicationResult(finalized)).toBe(true);
+    });
+
+    test('should return false when applicationResult is undefined', () => {
+      const finalized: SseFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+        },
+      };
+
+      expect(hasApplicationResult(finalized)).toBe(false);
+    });
+
+    test('should return false when applicationResult is undefined (explicit)', () => {
+      const finalized: SseFinalized = {
+        finalized: {
+          message: 'Test',
+          replacements: [],
+          applicationResult: undefined,
+        },
+      };
+
+      expect(hasApplicationResult(finalized)).toBe(false);
+    });
+  });
+
+  describe('Type inference', () => {
+    test('SseMessage type should match schema', () => {
+      const message: SseMessage = {
+        appendedMessage: 'Test message',
+      };
+
+      const result = SseMessageSchema.safeParse(message);
+      expect(result.success).toBe(true);
+    });
+
+    test('SseDetectedDiff type should match schema', () => {
+      const detectedDiff: SseDetectedDiff = {
+        diff: {
+          search: 'old',
+          replace: 'new',
+          startLine: 1,
+        },
+      };
+
+      const result = SseDetectedDiffSchema.safeParse(detectedDiff);
+      expect(result.success).toBe(true);
+    });
+
+    test('SseFinalized type should match schema', () => {
+      const finalized: SseFinalized = {
+        finalized: {
+          message: 'Done',
+          replacements: [],
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(finalized);
+      expect(result.success).toBe(true);
+    });
+  });
+
+  describe('Real-world scenarios', () => {
+    test('should validate complete SSE flow', () => {
+      const realWorldFinalized = {
+        finalized: {
+          message: 'Successfully refactored the authentication function and added error handling.',
+          replacements: [
+            {
+              search: 'function authenticate(token) {\n  return validateToken(token);\n}',
+              // eslint-disable-next-line max-len
+              replace: 'async function authenticate(token) {\n  try {\n    if (!token) {\n      throw new Error("Token is required");\n    }\n    return await validateToken(token);\n  } catch (error) {\n    console.error("Authentication failed:", error);\n    throw error;\n  }\n}',
+              startLine: 25,
+              endLine: 27,
+            },
+          ],
+          applicationResult: {
+            success: true,
+            appliedCount: 1,
+            totalCount: 1,
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(realWorldFinalized);
+      expect(result.success).toBe(true);
+    });
+
+    test('should validate error scenario with detailed feedback', () => {
+      const errorScenario = {
+        finalized: {
+          message: 'Failed to apply changes. The specified code was not found.',
+          replacements: [],
+          applicationResult: {
+            success: false,
+            appliedCount: 0,
+            totalCount: 1,
+            failedParts: [
+              {
+                type: 'SEARCH_NOT_FOUND',
+                message: 'Could not find exact match for the specified code',
+                line: 25,
+                details: {
+                  searchContent: 'function authenticate(token) {',
+                  bestMatch: 'function authenticateUser(userToken) {',
+                  similarity: 0.85,
+                  suggestions: [
+                    'Check if the function name has changed',
+                    'Verify the parameter name is correct',
+                    'Ensure the code structure matches exactly',
+                  ],
+                  correctFormat: 'function authenticateUser(userToken) {',
+                  lineRange: '20-30',
+                },
+              },
+            ],
+          },
+        },
+      };
+
+      const result = SseFinalizedSchema.safeParse(errorScenario);
+      expect(result.success).toBe(true);
+    });
+  });
+});