Complete Phase 3 (P0) frontend implementation for unified MCP management:
**New Files:**
- src/hooks/useMcp.ts: React Query hooks for unified MCP operations
- src/components/mcp/UnifiedMcpPanel.tsx: Unified MCP management panel
- src/components/ui/checkbox.tsx: Checkbox component from shadcn/ui
**Features:**
- Unified panel with three-column layout: server info + app checkboxes + actions
- Multi-app control: Claude/Codex/Gemini checkboxes for each server
- Real-time stats: Show enabled server counts per app
- Full CRUD operations: Add, edit, delete, sync all servers
**Integration:**
- Replace old app-specific McpPanel with UnifiedMcpPanel in App.tsx
- Update McpFormModal to support unified mode with apps field
- Add i18n support: mcp.unifiedPanel namespace (zh/en)
**Type Safety:**
- Ensure McpServer.apps field always initialized
- Fix all test files to include apps field
- TypeScript type check passes ✅
**Architecture:**
- Single source of truth: mcp.servers manages all MCP configs
- Per-server app control: apps.claude/codex/gemini boolean flags
- Backward compatible: McpFormModal supports both unified and legacy modes
Next: P1 tasks (import dialogs, sub-components, tests)
- Add postChangeSync.ts utility with Result pattern for graceful error handling
- Replace try-catch with syncCurrentProvidersLiveSafe in useImportExport
- Add directory-change-triggered sync in useSettings to maintain SSOT
- Introduce partial-success status to distinguish import success from sync failures
- Add test coverage for sync behavior in different scenarios
This refactoring ensures config.json changes are reliably synced to live
files while providing better user feedback for edge cases.
Update test infrastructure to support the dual app type parameter pattern
(app_type enum + app string) introduced in 590be4e:
- Add toast.warning mock for partial-success status handling
- Add syncCurrentProvidersLive mock to test post-import sync behavior
- Update MSW handlers to accept both app_type and app parameters with
fallback resolution (app ?? app_type) across 7 provider endpoints
- Add sync_current_providers_live endpoint mock returning success
This ensures test compatibility during the app_type → app parameter
migration and enables testing of the new partial-success import flow.
- Add vitest/globals to tsconfig.json types array to provide type
definitions for global test functions (describe, it, expect, vi)
- Fix vi.fn type parameter in useDirectorySettings.test.tsx from
<[], Promise<string>> to <() => Promise<string>>
- Remove unused setMcpConfig import from MSW handlers
- Add type assertions for mock.calls access in McpFormModal tests
to resolve union type inference issues
This ensures pnpm typecheck passes without errors while maintaining
test functionality with vitest globals: true configuration.
## McpFormModal Component Tests (+5 tests)
### Infrastructure Improvements
- Enhance McpWizardModal mock from null to functional mock with testable onApply callback
- Refactor renderForm helper to support custom onSave/onClose mock injection
- Add McpServer type import for type-safe test data
### New Test Cases
1. **Wizard Integration**: Verify wizard generates config and auto-fills ID + JSON fields
- Click "Use Wizard" → Apply → Form fields populated with wizard-id and config
- Uses act() wrapper for React 18 async state updates
2. **TOML Auto-extraction (Codex)**: Test TOML → JSON conversion with ID extraction
- Parse `[mcp.servers.demo]` → auto-fill ID as "demo"
- Verify server object correctly parsed from TOML format
- Codex-specific feature for config.toml compatibility
3. **TOML Validation Error**: Test missing required field handling
- TOML with type="stdio" but no command → block submit
- Display localized error toast: mcp.error.idRequired (3s duration)
4. **Edit Mode Immutability**: Verify ID field disabled during edit
- ID input has disabled attribute and keeps original value
- Config updates applied while enabled state preserved
- syncOtherSide defaults to false in edit mode
5. **Error Recovery**: Test save failure button state restoration
- Inject failing onSave mock → trigger error
- Verify toast error displays translated message
- Submit button disabled state resets to false for retry
## useMcpActions Hook Tests (+2 tests)
### New API Mocks
- Add syncEnabledToClaude and syncEnabledToCodex mock functions
### New Test Cases
1. **Backend Error Message Mapping**: Map Chinese error to i18n key
- Backend: "stdio 类型的 MCP 服务器必须包含 command 字段"
- Frontend: mcp.error.commandRequired (6s toast duration)
2. **Cross-app Sync Logic**: Verify conditional sync behavior
- claude → claude: setEnabled called, syncEnabledToClaude NOT called
- Validates sync only occurs when crossing app types
## Minor Changes
- McpPanel.test.tsx: Add trailing newline (formatter compliance)
## Test Coverage
- Test files: 17 (unchanged)
- Total tests: 112 → 119 (+7, +6.3%)
- Execution time: 3.20s
- All 119 tests passing ✅
Milestone Achievement: 100% Hooks Coverage 🎉
- Hooks coverage: 87.5% (7/8) → 100% (8/8)
- Total tests: 81 → 105 (+29.6%)
- MCP functionality: 0 → 24 tests
useMcpActions Tests (8 tests):
- Test server reload with loading state management
- Use deferred promise pattern to verify loading state transitions
- Verify intermediate loading=true state during async operation
- Verify error toast (duration: 6s) on reload failure
- Ensure loading returns to false after error
- Test optimistic toggle with rollback on failure
- Immediately update enabled flag before API confirmation
- Verify success toast messages for enable/disable
- Roll back state to original value on API failure
- Show error toast (duration: 6s) when toggle fails
- Test server save with list refresh
- Verify ID rewrite logic: saveServer(newId, {...input, id: oldId}) → {id: newId}
- Verify syncOtherSide option correctly propagated to API
- Refresh server list after successful save
- Propagate errors to caller while showing error toast
- Do not refresh list when save fails
- Test server delete with state management
- Verify deleteServerInConfig called with correct parameters
- Verify list refresh removes deleted server
- Show success toast (duration: 1.5s) on delete
- Keep state unchanged on delete failure
- Propagate error to caller with error toast
useMcpValidation Tests (16 tests):
- Test JSON validation (4 tests)
- Return empty string for blank/whitespace text
- Return "mcp.error.jsonInvalid" for parsing failures
- Reject non-object types (string, array)
- Accept valid object payloads
- Test TOML error formatting (2 tests)
- Map mustBeObject/parseError to "mcp.error.tomlInvalid"
- Append error details for unknown errors
- Test TOML config validation (5 tests)
- Propagate errors from validateToml utility
- Return "mcp.error.commandRequired" for stdio without command
- Return "mcp.wizard.urlRequired" for http without url
- Catch and format tomlToMcpServer exceptions
- Return empty string when validation passes
- Test JSON config validation (5 tests)
- Reject invalid JSON syntax
- Reject mcpServers array format (single object required)
- Require command field for stdio type servers
- Require url field for http type servers
- Accept valid server configurations
Technical Highlights:
- Deferred Promise Pattern: Precise async timing control
- Optimistic Updates: Test immediate feedback + rollback
- Error Propagation: Distinguish caller errors vs toast notifications
- i18n Validation: All validators return translation keys
- Factory Functions: Reusable test data builders
All tests passing: 105/105
Mock Refactoring:
- Extract saveFileDialogMock and exportConfigMock as variables
- Previously used inline vi.fn() which prevented call verification
- Now supports expect().toHaveBeenCalledWith() assertions
- Enables parameter and return value validation
- Add mock reset in beforeEach for test isolation
- Reset saveFileDialogMock state
- Reset exportConfigMock state
- Ensures clean state for each test
New Test: Import Failure Callback Verification
- Add test "does not call onImportSuccess when import fails"
- User selects file successfully
- Import operation fails (success: false)
- Verify onImportSuccess callback NOT called
- Verify status becomes "error"
- Prevents triggering success logic on failure
New Test: Export Success Message Verification
- Add test "propagates export success message to toast with saved path"
- User selects save location: /exports/config.json
- Backend saves to: /final/config.json (may differ)
- Verify exportConfigMock called with user-selected path
- Verify toast success message contains actual saved path
- Ensures user sees correct save location
Coverage Improvements:
- Import failure callback: 50% → 100%
- Export success message: 50% → 100%
- Mock verification capability: 0% → 100%
All tests passing: 81/81 (2 new tests)
useSettings Tests:
- Add test for null settings state protection
- Returns null immediately without calling APIs
- Prevents null pointer errors in save flow
- Add test for save mutation failure
- Verifies error propagates to caller
- Ensures restart flag remains untouched on failure
- Validates no side effects when save fails
useProviderActions Tests:
- Add error propagation tests for CRUD operations
- updateProvider: propagates errors to caller
- addProvider: propagates errors to caller
- deleteProvider: propagates errors to caller
- Add switch mutation error handling tests
- Claude switch: handles errors silently (no throw)
- Codex switch: skips plugin sync when mutation fails
- Verifies plugin sync APIs not called on failure
- Add loading state verification
- Test all mutations pending scenario (existing)
- Test all mutations idle scenario (new)
- Ensures isLoading flag accuracy
useImportExport Edge Case Tests (new file):
- Add test for user cancelling file dialog
- File dialog returns null (user cancelled)
- State remains unchanged (selectedFile: "", status: "idle")
- No error toast shown
- Add test for resetStatus behavior
- Clears error message and status
- Preserves selected file path for retry
- Resets backupId to null
All tests passing: 79/79 (10 new tests)
useDirectorySettings Tests:
- Test directory initialization with overrides and remote defaults
- Verify app config override with space trimming
- Load Claude/Codex directories from remote API
- Calculate resolvedDirs correctly
- Test directory browsing functionality
- Browse Claude/Codex config directories
- Browse app config directory with proper default paths
- Update settings callback when selection succeeds
- Test error handling scenarios
- User cancels directory selection (returns null)
- Directory picker throws error (shows toast)
- Verify settings not updated on failure
- Test directory reset operations
- Reset individual directories to computed defaults
- Reset app config directory
- Batch reset with provided server values
- Use vi.hoisted() for proper mock initialization
- Factory function for settings creation (reusability)
useSettingsMetadata Tests:
- Test portable mode flag loading
- Verify initial loading state
- Load portable flag from API
- Handle async state transitions
- Test error tolerance when API fails
- Silent failure on network errors
- Default to non-portable mode
- Continue without blocking UI
- Test restart flag management
- Set restart required flag
- Acknowledge restart to clear flag
- State updates wrapped in act()
All tests passing: 10/10 (7 useDirectorySettings + 3 useSettingsMetadata)
SettingsDialog Integration Tests:
- Add test for directory browsing and reset functionality
- Verify app config directory browse/reset flow
- Verify Claude config directory manual change, browse, and reset
- Test multiple directory inputs with getAllByTitle pattern
- Add test for export failure error handling
- User cancels file selection (save_file_dialog returns null)
- Export operation fails (disk full scenario)
- Use server.use() to dynamically override MSW handlers
- Verify toast error messages match i18n keys
MSW Handler Extension:
- Add pick_directory handler to support directory selection API
- Consistent with select_config_directory mock strategy
useSettings Hook Unit Tests:
- Add comprehensive tests for settings save logic
- Test restart flag when app config directory changes
- Test no restart when directory unchanged
- Verify space trimming and empty string to undefined conversion
- Test Claude plugin sync failure tolerance
- Add test for settings reset functionality
- Verify form/language/directories reset with server data
- Use factory functions for mock creation (reusability)
- Complete dependency isolation with mock hooks
All tests passing: 9/9 (5 integration + 4 unit tests)
Add extensive unit and component tests covering import/export, settings,
and provider list functionality, advancing to Sprint 2 of test development.
Hook Tests:
- useImportExport (11 tests):
* File selection success/failure flows
* Import process with success/failure/exception paths
* Export functionality with error handling
* User cancellation scenarios
* State management (clear selection, reset status)
* Fake timers for async callback testing
- useSettingsForm (5 tests):
* Settings normalization on initialization
* Language persistence from localStorage
* Field updates with language sync
* Reset functionality with initial language restoration
* Optimization to avoid redundant language changes
Component Tests:
- ProviderList (3 tests):
* Loading state with skeleton placeholders
* Empty state with create callback
* Render order from useDragSort with action callbacks
* Props pass-through (isCurrent, isEditMode, dragHandleProps)
* Mock ProviderCard to isolate component under test
Technical Highlights:
- Fake timers (vi.useFakeTimers) for async control
- i18n mock with changeLanguage spy
- Partial mock of @dnd-kit/sortable using vi.importActual
- ProviderCard render spy for props verification
- Comprehensive error handling coverage
Test Coverage:
✓ 19 new test cases (11 + 5 + 3)
✓ Total: 35 tests passing
✓ Execution time: 865ms
✓ TypeScript: 0 errors
Related: Import/export, settings management, provider list rendering
Sprint Progress: Sprint 1 complete, Sprint 2 in progress (component tests)