|
1 | | -import { describe, expect, it } from 'bun:test' |
| 1 | +import * as envModule from '@codebuff/internal/env' |
| 2 | +import { describe, expect, it, spyOn } from 'bun:test' |
2 | 3 | import { applyPatch } from 'diff' |
3 | 4 |
|
4 | 5 | import { processStrReplace } from '../process-str-replace' |
| 6 | +import { mockFileContext } from './test-utils' |
| 7 | +import { |
| 8 | + executeBatchStrReplaces, |
| 9 | + benchifyCanFixLanguage, |
| 10 | +} from '../tools/batch-str-replace' |
5 | 11 |
|
6 | 12 | describe('processStrReplace', () => { |
7 | 13 | it('should replace exact string matches', async () => { |
@@ -213,6 +219,25 @@ describe('processStrReplace', () => { |
213 | 219 | } |
214 | 220 | }) |
215 | 221 |
|
| 222 | + it('should handle replacement where old string equals new string', async () => { |
| 223 | + const initialContent = 'const x = 1;\nconst y = 2;\n' |
| 224 | + const oldStr = 'const y = 2;' |
| 225 | + const newStr = 'const y = 2;' // Same as old string |
| 226 | + |
| 227 | + const result = await processStrReplace( |
| 228 | + 'test.ts', |
| 229 | + [{ old: oldStr, new: newStr, allowMultiple: false }], |
| 230 | + Promise.resolve(initialContent), |
| 231 | + ) |
| 232 | + |
| 233 | + expect(result).not.toBeNull() |
| 234 | + expect('content' in result).toBe(true) |
| 235 | + if ('content' in result) { |
| 236 | + expect(result.content).toBe('const x = 1;\nconst y = 2;\n') |
| 237 | + expect(result.messages).toEqual([]) |
| 238 | + } |
| 239 | + }) |
| 240 | + |
216 | 241 | // New comprehensive tests for allowMultiple functionality |
217 | 242 | describe('allowMultiple functionality', () => { |
218 | 243 | it('should error when multiple occurrences exist and allowMultiple is false', async () => { |
@@ -417,3 +442,142 @@ function test3() { |
417 | 442 | ) |
418 | 443 | }) |
419 | 444 | }) |
| 445 | + |
| 446 | +// Tests for Benchify resilience |
| 447 | +describe('Benchify resilience', () => { |
| 448 | + describe('happy path', () => { |
| 449 | + it('should identify Benchify-supported file types correctly', () => { |
| 450 | + const testCases = [ |
| 451 | + { path: 'component.tsx', expected: true }, |
| 452 | + { path: 'utils.ts', expected: true }, |
| 453 | + { path: 'script.js', expected: true }, |
| 454 | + { path: 'styles.jsx', expected: true }, |
| 455 | + { path: 'README.md', expected: false }, |
| 456 | + { path: 'config.json', expected: false }, |
| 457 | + { path: 'styles.css', expected: false }, |
| 458 | + { path: 'index.html', expected: false }, |
| 459 | + { path: 'test.py', expected: false }, |
| 460 | + ] |
| 461 | + |
| 462 | + for (const { path, expected } of testCases) { |
| 463 | + const result = benchifyCanFixLanguage(path) |
| 464 | + expect(result).toBe(expected) |
| 465 | + } |
| 466 | + }) |
| 467 | + |
| 468 | + it('should handle file extensions case sensitivity', () => { |
| 469 | + expect(benchifyCanFixLanguage('Component.TSX')).toBe(false) // Wrong case |
| 470 | + expect(benchifyCanFixLanguage('component.tsx')).toBe(true) // Correct case |
| 471 | + expect(benchifyCanFixLanguage('utils.TS')).toBe(false) // Wrong case |
| 472 | + expect(benchifyCanFixLanguage('utils.ts')).toBe(true) // Correct case |
| 473 | + }) |
| 474 | + |
| 475 | + it('should handle file paths with multiple dots', () => { |
| 476 | + expect(benchifyCanFixLanguage('component.test.tsx')).toBe(true) |
| 477 | + expect(benchifyCanFixLanguage('utils.spec.ts')).toBe(true) |
| 478 | + expect(benchifyCanFixLanguage('config.local.js')).toBe(true) |
| 479 | + expect(benchifyCanFixLanguage('styles.module.css')).toBe(false) |
| 480 | + }) |
| 481 | + |
| 482 | + it('should handle files without extensions', () => { |
| 483 | + expect(benchifyCanFixLanguage('Dockerfile')).toBe(false) |
| 484 | + expect(benchifyCanFixLanguage('Makefile')).toBe(false) |
| 485 | + expect(benchifyCanFixLanguage('README')).toBe(false) |
| 486 | + }) |
| 487 | + }) |
| 488 | + |
| 489 | + it('should fall back gracefully when Benchify is disabled', async () => { |
| 490 | + // Test with no API key - spy on the env object directly |
| 491 | + spyOn(envModule, 'env', 'get').mockReturnValue({ |
| 492 | + // Empty object simulates no BENCHIFY_API_KEY |
| 493 | + } as any) |
| 494 | + |
| 495 | + const result = await executeBatchStrReplaces({ |
| 496 | + deferredStrReplaces: [ |
| 497 | + { |
| 498 | + toolCall: { |
| 499 | + toolName: 'str_replace' as const, |
| 500 | + toolCallId: 'test-call', |
| 501 | + input: { |
| 502 | + path: 'test.ts', |
| 503 | + replacements: [{ old: 'old', new: 'new', allowMultiple: false }], |
| 504 | + }, |
| 505 | + }, |
| 506 | + }, |
| 507 | + ], |
| 508 | + toolCalls: [], |
| 509 | + toolResults: [], |
| 510 | + ws: {} as any, |
| 511 | + fileContext: mockFileContext, |
| 512 | + agentStepId: 'test-step', |
| 513 | + clientSessionId: 'test-session', |
| 514 | + userInputId: 'test-input', |
| 515 | + onResponseChunk: () => {}, |
| 516 | + state: { messages: [] }, |
| 517 | + userId: 'test-user', |
| 518 | + }) |
| 519 | + |
| 520 | + // Should complete without error even when Benchify is unavailable |
| 521 | + expect(result).toBeUndefined() // Function returns void |
| 522 | + }) |
| 523 | + |
| 524 | + describe('Batch str_replace integration tests', () => { |
| 525 | + it('should handle empty deferred list without error', async () => { |
| 526 | + // Simple test that doesn't require complex mocking |
| 527 | + expect( |
| 528 | + executeBatchStrReplaces({ |
| 529 | + deferredStrReplaces: [], |
| 530 | + toolCalls: [], |
| 531 | + toolResults: [], |
| 532 | + ws: {} as any, |
| 533 | + fileContext: mockFileContext, |
| 534 | + agentStepId: 'test-step', |
| 535 | + clientSessionId: 'test-session', |
| 536 | + userInputId: 'test-input', |
| 537 | + onResponseChunk: () => {}, |
| 538 | + state: { messages: [] }, |
| 539 | + userId: 'test-user', |
| 540 | + }), |
| 541 | + ).resolves.toBeUndefined() // Should complete without throwing |
| 542 | + }) |
| 543 | + }) |
| 544 | + |
| 545 | + it('should identify Benchify-supported file types correctly', () => { |
| 546 | + const testCases = [ |
| 547 | + { path: 'component.tsx', expected: true }, |
| 548 | + { path: 'utils.ts', expected: true }, |
| 549 | + { path: 'script.js', expected: true }, |
| 550 | + { path: 'styles.jsx', expected: true }, |
| 551 | + { path: 'README.md', expected: false }, |
| 552 | + { path: 'config.json', expected: false }, |
| 553 | + { path: 'styles.css', expected: false }, |
| 554 | + { path: 'index.html', expected: false }, |
| 555 | + { path: 'test.py', expected: false }, |
| 556 | + ] |
| 557 | + |
| 558 | + for (const { path, expected } of testCases) { |
| 559 | + const result = benchifyCanFixLanguage(path) |
| 560 | + expect(result).toBe(expected) |
| 561 | + } |
| 562 | + }) |
| 563 | + |
| 564 | + it('should handle executeBatchStrReplaces with empty list', async () => { |
| 565 | + // Simple test that doesn't require complex mocking |
| 566 | + const result = await executeBatchStrReplaces({ |
| 567 | + deferredStrReplaces: [], |
| 568 | + toolCalls: [], |
| 569 | + toolResults: [], |
| 570 | + ws: {} as any, |
| 571 | + fileContext: mockFileContext, |
| 572 | + agentStepId: 'test-step', |
| 573 | + clientSessionId: 'test-session', |
| 574 | + userInputId: 'test-input', |
| 575 | + onResponseChunk: () => {}, |
| 576 | + state: { messages: [] }, |
| 577 | + userId: 'test-user', |
| 578 | + }) |
| 579 | + |
| 580 | + // Should complete without throwing an error |
| 581 | + expect(result).toBeUndefined() // Function returns void |
| 582 | + }) |
| 583 | +}) |
0 commit comments