mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
* test(copilot): add edge case test for error with code field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Changes from fix/bug-fixes-1-0 * refactor(auto-mode): enhance orphaned feature detection and improve project initialization - Updated detectOrphanedFeatures method to accept preloaded features, reducing redundant disk reads. - Improved project initialization by creating required directories and files in parallel for better performance. - Adjusted planning mode handling in UI components to clarify approval requirements for different modes. - Added refresh functionality for file editor tabs to ensure content consistency with disk state. These changes enhance performance, maintainability, and user experience across the application. * feat(orphaned-features): add orphaned features management routes and UI integration - Introduced new routes for managing orphaned features, including listing, resolving, and bulk resolving. - Updated the UI to include an Orphaned Features section in project settings and navigation. - Enhanced the execution service to support new orphaned feature functionalities. These changes improve the application's capability to handle orphaned features effectively, enhancing user experience and project management. * fix: Normalize line endings and resolve stale dirty states in file editor * chore: Update .gitignore and enhance orphaned feature handling - Added a blank line in .gitignore for better readability. - Introduced a hash to worktree paths in orphaned feature resolution to prevent conflicts. - Added validation for target branch existence during orphaned feature resolution. - Improved prompt formatting in execution service for clarity. - Enhanced error handling in project selector for project initialization failures. - Refactored orphaned features section to improve state management and UI responsiveness. These changes improve code maintainability and user experience when managing orphaned features. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
computeIsDirty,
|
|
updateTabWithContent as updateTabContent,
|
|
markTabAsSaved as markTabSaved,
|
|
} from '../../../../ui/src/components/views/file-editor-view/file-editor-dirty-utils.ts';
|
|
|
|
/**
|
|
* Unit tests for the file editor store logic, focusing on the unsaved indicator fix.
|
|
*
|
|
* The bug was: File unsaved indicators weren't working reliably - editing a file
|
|
* and saving it would sometimes leave the dirty indicator (dot) visible.
|
|
*
|
|
* Root causes:
|
|
* 1. Stale closure in handleSave - captured activeTab could have old content
|
|
* 2. Editor buffer not synced - CodeMirror might have buffered changes not yet in store
|
|
*
|
|
* Fix:
|
|
* - handleSave now gets fresh state from store using getState()
|
|
* - handleSave gets current content from editor via getValue()
|
|
* - Content is synced to store before saving if it differs
|
|
*
|
|
* Since we can't easily test the React/zustand store in node environment,
|
|
* we test the pure logic that the store uses for dirty state tracking.
|
|
*/
|
|
|
|
describe('File editor dirty state logic', () => {
|
|
describe('updateTabContent', () => {
|
|
it('should set isDirty to true when content differs from originalContent', () => {
|
|
const tab = {
|
|
content: 'original content',
|
|
originalContent: 'original content',
|
|
isDirty: false,
|
|
};
|
|
|
|
const updated = updateTabContent(tab, 'modified content');
|
|
|
|
expect(updated.isDirty).toBe(true);
|
|
expect(updated.content).toBe('modified content');
|
|
expect(updated.originalContent).toBe('original content');
|
|
});
|
|
|
|
it('should set isDirty to false when content matches originalContent', () => {
|
|
const tab = {
|
|
content: 'original content',
|
|
originalContent: 'original content',
|
|
isDirty: false,
|
|
};
|
|
|
|
// First modify it
|
|
let updated = updateTabContent(tab, 'modified content');
|
|
expect(updated.isDirty).toBe(true);
|
|
|
|
// Now update back to original
|
|
updated = updateTabContent(updated, 'original content');
|
|
expect(updated.isDirty).toBe(false);
|
|
});
|
|
|
|
it('should handle empty content correctly', () => {
|
|
const tab = {
|
|
content: '',
|
|
originalContent: '',
|
|
isDirty: false,
|
|
};
|
|
|
|
const updated = updateTabContent(tab, 'new content');
|
|
|
|
expect(updated.isDirty).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('markTabSaved', () => {
|
|
it('should set isDirty to false and update both content and originalContent', () => {
|
|
const tab = {
|
|
content: 'original content',
|
|
originalContent: 'original content',
|
|
isDirty: false,
|
|
};
|
|
|
|
// First modify
|
|
let updated = updateTabContent(tab, 'modified content');
|
|
expect(updated.isDirty).toBe(true);
|
|
|
|
// Then save
|
|
updated = markTabSaved(updated, 'modified content');
|
|
|
|
expect(updated.isDirty).toBe(false);
|
|
expect(updated.content).toBe('modified content');
|
|
expect(updated.originalContent).toBe('modified content');
|
|
});
|
|
|
|
it('should correctly clear dirty state when save is triggered after edit', () => {
|
|
// This test simulates the bug scenario:
|
|
// 1. User edits file -> isDirty = true
|
|
// 2. User saves -> markTabSaved should set isDirty = false
|
|
let tab = {
|
|
content: 'initial',
|
|
originalContent: 'initial',
|
|
isDirty: false,
|
|
};
|
|
|
|
// Simulate user editing
|
|
tab = updateTabContent(tab, 'initial\nnew line');
|
|
|
|
// Should be dirty
|
|
expect(tab.isDirty).toBe(true);
|
|
|
|
// Simulate save (with the content that was saved)
|
|
tab = markTabSaved(tab, 'initial\nnew line');
|
|
|
|
// Should NOT be dirty anymore
|
|
expect(tab.isDirty).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('race condition handling', () => {
|
|
it('should correctly handle updateTabContent after markTabSaved with same content', () => {
|
|
// This tests the scenario where:
|
|
// 1. CodeMirror has a pending onChange with content "B"
|
|
// 2. User presses save when editor shows "B"
|
|
// 3. markTabSaved is called with "B"
|
|
// 4. CodeMirror's pending onChange fires with "B" (same content)
|
|
// Result: isDirty should remain false
|
|
let tab = {
|
|
content: 'A',
|
|
originalContent: 'A',
|
|
isDirty: false,
|
|
};
|
|
|
|
// User edits to "B"
|
|
tab = updateTabContent(tab, 'B');
|
|
|
|
// Save with "B"
|
|
tab = markTabSaved(tab, 'B');
|
|
|
|
// Late onChange with same content "B"
|
|
tab = updateTabContent(tab, 'B');
|
|
|
|
expect(tab.isDirty).toBe(false);
|
|
expect(tab.content).toBe('B');
|
|
});
|
|
|
|
it('should correctly handle updateTabContent after markTabSaved with different content', () => {
|
|
// This tests the scenario where:
|
|
// 1. CodeMirror has a pending onChange with content "C"
|
|
// 2. User presses save when store has "B"
|
|
// 3. markTabSaved is called with "B"
|
|
// 4. CodeMirror's pending onChange fires with "C" (different content)
|
|
// Result: isDirty should be true (file changed after save)
|
|
let tab = {
|
|
content: 'A',
|
|
originalContent: 'A',
|
|
isDirty: false,
|
|
};
|
|
|
|
// User edits to "B"
|
|
tab = updateTabContent(tab, 'B');
|
|
|
|
// Save with "B"
|
|
tab = markTabSaved(tab, 'B');
|
|
|
|
// Late onChange with different content "C"
|
|
tab = updateTabContent(tab, 'C');
|
|
|
|
// File changed after save, so it should be dirty
|
|
expect(tab.isDirty).toBe(true);
|
|
expect(tab.content).toBe('C');
|
|
expect(tab.originalContent).toBe('B');
|
|
});
|
|
|
|
it('should handle rapid edit-save-edit cycle correctly', () => {
|
|
// Simulate rapid user actions
|
|
let tab = {
|
|
content: 'v1',
|
|
originalContent: 'v1',
|
|
isDirty: false,
|
|
};
|
|
|
|
// Edit 1
|
|
tab = updateTabContent(tab, 'v2');
|
|
expect(tab.isDirty).toBe(true);
|
|
|
|
// Save 1
|
|
tab = markTabSaved(tab, 'v2');
|
|
expect(tab.isDirty).toBe(false);
|
|
|
|
// Edit 2
|
|
tab = updateTabContent(tab, 'v3');
|
|
expect(tab.isDirty).toBe(true);
|
|
|
|
// Save 2
|
|
tab = markTabSaved(tab, 'v3');
|
|
expect(tab.isDirty).toBe(false);
|
|
|
|
// Edit 3 (back to v2)
|
|
tab = updateTabContent(tab, 'v2');
|
|
expect(tab.isDirty).toBe(true);
|
|
|
|
// Save 3
|
|
tab = markTabSaved(tab, 'v2');
|
|
expect(tab.isDirty).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('handleSave stale closure fix simulation', () => {
|
|
it('demonstrates the fix: using fresh content instead of closure content', () => {
|
|
// This test demonstrates why the fix was necessary.
|
|
// The old handleSave captured activeTab in closure, which could be stale.
|
|
// The fix gets fresh state from getState() and uses editor.getValue().
|
|
|
|
// Simulate store state
|
|
let storeState = {
|
|
tabs: [
|
|
{
|
|
id: 'tab-1',
|
|
content: 'A',
|
|
originalContent: 'A',
|
|
isDirty: false,
|
|
},
|
|
],
|
|
activeTabId: 'tab-1',
|
|
};
|
|
|
|
// Simulate a "stale closure" capturing the tab state
|
|
const staleClosureTab = storeState.tabs[0];
|
|
|
|
// User edits - store state updates
|
|
storeState = {
|
|
...storeState,
|
|
tabs: [
|
|
{
|
|
id: 'tab-1',
|
|
content: 'B',
|
|
originalContent: 'A',
|
|
isDirty: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
// OLD BUG: Using stale closure tab would save "A" (old content)
|
|
const oldBugSavedContent = staleClosureTab!.content;
|
|
expect(oldBugSavedContent).toBe('A'); // Wrong! Should be "B"
|
|
|
|
// FIX: Using fresh state from getState() gets correct content
|
|
const freshTab = storeState.tabs[0];
|
|
const fixedSavedContent = freshTab!.content;
|
|
expect(fixedSavedContent).toBe('B'); // Correct!
|
|
});
|
|
|
|
it('demonstrates syncing editor content before save', () => {
|
|
// This test demonstrates why we need to get content from editor directly.
|
|
// The store might have stale content if onChange hasn't fired yet.
|
|
|
|
// Simulate store state (has old content because onChange hasn't fired)
|
|
let storeContent = 'A';
|
|
|
|
// Editor has newer content (not yet synced to store)
|
|
const editorContent = 'B';
|
|
|
|
// FIX: Use editor content if available, fall back to store content
|
|
const contentToSave = editorContent ?? storeContent;
|
|
|
|
expect(contentToSave).toBe('B'); // Correctly saves editor content
|
|
|
|
// Simulate syncing to store before save
|
|
if (editorContent !== null && editorContent !== storeContent) {
|
|
storeContent = editorContent;
|
|
}
|
|
|
|
// Now store is synced
|
|
expect(storeContent).toBe('B');
|
|
|
|
// After save, markTabSaved would set originalContent = savedContent
|
|
// and isDirty = false (if no more changes come in)
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle whitespace-only changes as dirty', () => {
|
|
let tab = {
|
|
content: 'hello',
|
|
originalContent: 'hello',
|
|
isDirty: false,
|
|
};
|
|
|
|
tab = updateTabContent(tab, 'hello ');
|
|
expect(tab.isDirty).toBe(true);
|
|
});
|
|
|
|
it('should treat CRLF and LF line endings as equivalent (not dirty)', () => {
|
|
let tab = {
|
|
content: 'line1\nline2',
|
|
originalContent: 'line1\nline2',
|
|
isDirty: false,
|
|
};
|
|
|
|
// CodeMirror normalizes \r\n to \n internally, so content that only
|
|
// differs by line endings should NOT be considered dirty.
|
|
tab = updateTabContent(tab, 'line1\r\nline2');
|
|
expect(tab.isDirty).toBe(false);
|
|
});
|
|
|
|
it('should handle unicode content correctly', () => {
|
|
let tab = {
|
|
content: '你好世界',
|
|
originalContent: '你好世界',
|
|
isDirty: false,
|
|
};
|
|
|
|
tab = updateTabContent(tab, '你好宇宙');
|
|
expect(tab.isDirty).toBe(true);
|
|
|
|
tab = markTabSaved(tab, '你好宇宙');
|
|
expect(tab.isDirty).toBe(false);
|
|
});
|
|
|
|
it('should handle very large content efficiently', () => {
|
|
// Generate a large string (1MB)
|
|
const largeOriginal = 'x'.repeat(1024 * 1024);
|
|
const largeModified = largeOriginal + 'y';
|
|
|
|
let tab = {
|
|
content: largeOriginal,
|
|
originalContent: largeOriginal,
|
|
isDirty: false,
|
|
};
|
|
|
|
tab = updateTabContent(tab, largeModified);
|
|
|
|
expect(tab.isDirty).toBe(true);
|
|
});
|
|
});
|
|
});
|