-
-
Notifications
You must be signed in to change notification settings - Fork 146
Feature/fix realtime api #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
- Introduced the `openai-voice-agents.skill` file for the new skill. - Added comprehensive documentation in Markdown format, including `SKILL.md`, `build.md`, `index.md`, `quickstart.md`, and `transport.md`. - Implemented a search tool script (`search_docs.py`) for full-text search across documentation files. - Included usage examples and detailed instructions for setting up and using the voice agents.
…cognition hooks - Added unit tests for `useAudioProcessing`, `useBrowserSpeechRecognition`, `useVoiceRecognition`, `useWhisperRecognition`, and other related hooks. - Established error handling patterns and ensured consistent toast notifications for various error scenarios. - Included tests for audio context initialization, media recorder behavior, and speech detection functionalities. - Enhanced test coverage for edge cases and user interactions, ensuring robust functionality across different components.
- Introduced a new module for loading preset files from the `/public/presets/` directory. - Implemented a custom hook `usePresetLoader` to manage preset loading during application initialization. - Added unit tests for the preset loading logic, ensuring correct handling of file reading, error scenarios, and fallback mechanisms. - Created integration tests to validate the interaction between the preset loader and the settings store. - Added example preset files for testing purposes.
- Replaced specific model identifiers with simplified names for better clarity and maintainability. - Updated the list to reflect the latest model naming conventions.
- Added unit tests for `useVoiceRecognition`, `useBrowserSpeechRecognition`, and `useWhisperRecognition` to ensure proper functionality and prevent infinite rendering loops. - Introduced integration tests to validate the stability of the voice recognition hooks under various scenarios, including keyboard shortcuts and continuous listening modes. - Enhanced memoization in the hooks to maintain stable references and improve performance. - Verified that the hooks correctly handle user input and state changes without causing unnecessary re-renders.
- Introduced a new MemorySettings component for managing memory features, including enabling/disabling memory, setting similarity thresholds, and managing memory limits. - Implemented memory storage using IndexedDB for persistent message storage and retrieval. - Added API endpoints for embedding generation and memory file management, including restoration from local files. - Enhanced chat handling to save user and assistant messages to memory, integrating memory context into chat prompts. - Developed comprehensive tests for memory-related functionalities, ensuring reliability and performance across various scenarios.
- Updated `handleApiError` function to accept an optional error detail parameter, allowing for more informative error messages. - Modified error handling in `getDifyChatResponseStream` and `getVercelAIChatResponseStream` to include detailed error information from API responses. - Improved error response formatting in custom API handlers to return specific error messages instead of generic ones.
- Updated `MessageInput` component to toggle microphone icon based on listening mode. - Enhanced `handleSendMessage` function to trim user input and stop listening before sending messages. - Improved `useVoiceRecognition` hook to restart listening in continuous mode after stopping. - Added additional checks for disabling the microphone button during processing states.
- Implemented PresenceDebugPreview component for camera feed and bounding box display. - Created PresenceIndicator component to visually represent current detection state. - Developed PresenceManager to manage detection lifecycle and integrate with existing components. - Added PresenceSettings component for configuring detection options. - Introduced presence types and error handling in presenceTypes.ts. - Enhanced home store to include presence state and error management. - Updated settings store to manage presence detection settings. - Implemented usePresenceDetection hook for handling face detection logic. - Integrated presence detection into the main application layout.
…components - Add IdleManager component to manage idle mode and automatic speech. - Create IdleSettings component for configuring idle mode settings. - Introduce useIdleMode hook to handle idle state logic and speech triggering. - Define idle types and constants in idleTypes.ts for better type management. - Integrate idle mode settings into the global settings store. - Update main settings index to include idle settings tab. - Add integration tests for idle mode functionality. - Refactor presence detection tests to improve readability. - Adjust presenceDebugPreview component to ensure proper video reference typing.
- Added `useKioskMode` hook for managing kiosk mode state and input validation. - Introduced `KioskOverlay` component for fullscreen and passcode dialog handling. - Created `PasscodeDialog` for user authentication in kiosk mode. - Developed `KioskSettings` component for configuring kiosk mode options. - Integrated NG word filtering and input length restrictions in kiosk mode. - Updated `Menu`, `MessageInput`, and `Settings` components to support kiosk mode features. - Added guidance messages for kiosk mode users. - Enhanced settings store to include kiosk mode configurations. - Implemented hooks for handling fullscreen and long press events for the Escape key. - Updated Tailwind CSS configuration for new animations.
- Implemented demo mode checks across various API endpoints to restrict functionality when enabled. - Created utility functions for demo mode detection and error response generation. - Added a custom hook to manage demo mode state in components. - Introduced a DemoModeNotice component to inform users when features are disabled in demo mode. - Updated existing components and settings to reflect demo mode status, including disabling buttons and adjusting UI elements. - Added unit tests for demo mode functionality in relevant API and utility files.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @tegnike's task —— View job PR Review: Feature/Fix Realtime APIReviewed this large PR (100 files, 26K+ additions) focusing on code quality, bugs, performance, security, and test coverage. Review Tasks
Review Summary✅ Positive Aspects1. Comprehensive Test Coverage
2. Code Quality & Architecture
3. Security Implementation
|
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes. 💤 Files selected but had no reviewable changes (15)
You can disable this status message by setting the WalkthroughKiRo仕様駆動ワークフロー、設計ルール/テンプレート、Playwrightテストレポーター仕様、OpenAI Voice Agentsドキュメント、face-api プレゼンス検出アセット、環境設定変更、依存追加、および50+のユニット/統合テスト群を一括で追加しました。 Changes
Sequence Diagram(s)(ドキュメント・テンプレート追加と大規模テスト追加が主体で、複数コンポーネント間の新しい制御フロー追加は含まれないため図は省略します。) Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (16)
src/__tests__/hooks/useRealtimeVoiceAPI.test.ts-6-45 (1)
6-45: セットアップされたモックが未使用ですストアと i18n のモックがセットアップされていますが、現在のテストでは使用されていません。これらは
useRealtimeVoiceAPIフックをテストするためのものと思われますが、フック自体がテストされていないため未使用となっています。将来のフックテスト実装時に使用される予定であれば問題ありませんが、現時点では不要なコードとなっています。
src/__tests__/hooks/useRealtimeVoiceAPI.test.ts-47-74 (1)
47-74: SpeechRecognition モックが未使用です
SpeechRecognitionの包括的なモックがセットアップされていますが、現在のテストでは使用されていません。Line 79 でmockSpeechRecognition.langがリセットされていますが、実際のテストではこのモックを検証していません。将来のフックテスト実装時に使用される予定であれば問題ありませんが、現時点では準備だけで終わっています。
src/__tests__/hooks/useRealtimeVoiceAPI.test.ts-1-5 (1)
1-5: グローバルオブジェクトの復元処理が必要ですテストで
window.SpeechRecognitionとwindow.webkitSpeechRecognitionをモック化していますが、afterAll()フック内で元の値に戻す必要があります。これにより、テスト間の副作用を防ぐことができます。const MockSpeechRecognitionClass = jest.fn().mockImplementation(() => { return { ...mockSpeechRecognition } }) +const originalSpeechRecognition = window.SpeechRecognition +const originalWebkitSpeechRecognition = window.webkitSpeechRecognition + // Setup global SpeechRecognition Object.defineProperty(window, 'SpeechRecognition', { writable: true, value: MockSpeechRecognitionClass, }) Object.defineProperty(window, 'webkitSpeechRecognition', { writable: true, value: MockSpeechRecognitionClass, }) describe('useRealtimeVoiceAPI - 言語設定の動的反映', () => { beforeEach(() => { jest.clearAllMocks() mockSpeechRecognition.lang = '' }) + + afterAll(() => { + Object.defineProperty(window, 'SpeechRecognition', { + writable: true, + value: originalSpeechRecognition, + }) + Object.defineProperty(window, 'webkitSpeechRecognition', { + writable: true, + value: originalWebkitSpeechRecognition, + }) + })Committable suggestion skipped: line range outside the PR's diff.
src/__tests__/hooks/errorHandling.test.ts-115-218 (1)
115-218: テストが実装フックの検証を行っていません。Lines 115-218のテストは定数値とシミュレートされたパターン検証のみで、実装ファイル(
useBrowserSpeechRecognition.ts、useSilenceDetection.ts、useWhisperRecognition.tsなど)が実際にこれらのエラーハンドリングパターンに従っているかを検証していません。実装ファイルは正しいパターンに従っていますが(例:
useBrowserSpeechRecognition.tsLine 166で'Microphone permission error:'、useWhisperRecognition.tsLine 94で'Whisper transcription error:'を使用)、errorHandling.test.tsがこれを直接テストしていないため、今後の変更時に回帰を防ぐことができません。推奨:
renderHookを使用して、実装フックのエラー処理を直接テストするテストケースを追加してください。例えば、マイク権限エラー時に実際に'Microphone permission error:'がログされ、正しいタグを持つtoastが表示されることを検証してください。.claude/skills/openai-voice-agents/docs/build.md-402-402 (1)
402-402: 誤字を修正してください。Line 402で「playack」となっていますが、正しくは「playback」です。
🔎 修正案
-If you are using WebSocket, you will need to handle this yourself by stopping audio playack of whatever has been queued up to be played. +If you are using WebSocket, you will need to handle this yourself by stopping audio playback of whatever has been queued up to be played.src/__tests__/components/demoModeNotice.test.tsx-35-41 (1)
35-41: テストでfeatureKeyプロップの使用を検証してください。このテスト(35-41行目)は
featureKey="upload"を渡していますが、実際にこのプロップが使用されているかを検証していません。コンポーネントのコード(demoModeNotice.tsx)を確認すると、featureKeyは受け取られていますが、レンダリングで使用されていません。このテストは27-33行目のテストと同じ期待値を持つため、本質的に重複しています。以下のいずれかを検討してください:
- コンポーネントが
featureKeyを使用するように実装を修正する場合、テストでその使用を検証するfeatureKeyが使用されない場合、このテストケースと未使用のfeatureKeyプロップを削除するsrc/__tests__/hooks/usePresetLoader.test.ts-19-20 (1)
19-20: global.fetchの復元が必要です。テストファイルでグローバルオブジェクト(
global.fetch)を上書きする場合は、afterAll()フック内で元の値に戻して、テスト間の副作用を防ぐべきです。Based on learnings, テスト間の副作用を防ぐため、グローバルオブジェクトの復元が推奨されています。
🔎 提案する修正
// fetch APIのモック +const originalFetch = global.fetch const mockFetch = jest.fn() global.fetch = mockFetch + +afterAll(() => { + global.fetch = originalFetch +})src/__tests__/features/memory/memoryStoreSync.test.ts-29-30 (1)
29-30: global.fetchの復元が必要です。
global.fetchを上書きしていますが、テスト終了後に元の値に戻していません。テスト間の副作用を防ぐため、afterAll()フック内で元の値に戻すべきです。Based on learnings, テストファイルでグローバルオブジェクトを上書きする場合は、元の値に戻すことが推奨されています。
🔎 提案する修正
// Mock fetch for Embedding API +const originalFetch = global.fetch const mockFetch = jest.fn() global.fetch = mockFetch describe('MemoryStoreSync', () => { const originalState = settingsStore.getState() beforeEach(async () => { // ... existing code ... }) afterEach(async () => { // ... existing code ... }) + + afterAll(() => { + global.fetch = originalFetch + }) // ... tests ... })src/__tests__/hooks/useAudioProcessing.test.ts-24-74 (1)
24-74: グローバルオブジェクトの復元が必要です。
window.AudioContext、window.webkitAudioContext、window.MediaRecorder、navigator.mediaDevicesなどのグローバルオブジェクトを上書きしていますが、テスト終了後に元の値に戻していません。テスト間の副作用を防ぐため、afterAll()フック内で元の値に戻すべきです。Based on learnings, テストファイルでグローバルオブジェクトを上書きする場合は、元の値に戻すことが推奨されています。
🔎 提案する修正
+// Save original values +const originalAudioContext = window.AudioContext +const originalWebkitAudioContext = (window as any).webkitAudioContext +const originalMediaRecorder = window.MediaRecorder +const originalMediaDevices = navigator.mediaDevices + // Mock AudioContext const mockAudioContextClose = jest.fn().mockResolvedValue(undefined) // ... rest of mocks ... describe('useAudioProcessing', () => { beforeEach(() => { jest.clearAllMocks() MockAudioContext.mockClear() }) + + afterAll(() => { + // Restore original values + Object.defineProperty(window, 'AudioContext', { + writable: true, + value: originalAudioContext, + }) + Object.defineProperty(window, 'webkitAudioContext', { + writable: true, + value: originalWebkitAudioContext, + }) + Object.defineProperty(window, 'MediaRecorder', { + writable: true, + value: originalMediaRecorder, + }) + Object.defineProperty(navigator, 'mediaDevices', { + writable: true, + value: originalMediaDevices, + }) + }) // ... tests ... })Committable suggestion skipped: line range outside the PR's diff.
src/__tests__/components/settings/kioskSettings.test.tsx-41-43 (1)
41-43: 型定義の不整合を確認
mockSettingsStoreがMockedFunction<typeof settingsStore>として型付けされていますが、settingsStore は関数ではなく、getState/setStateメソッドを持つオブジェクトとしてモックされています。この型定義は実際のモック構造と一致していない可能性があります。🔎 型定義の修正案
-const mockSettingsStore = settingsStore as jest.MockedFunction< - typeof settingsStore -> +const mockSettingsStore = settingsStore as jest.Mocked<typeof settingsStore>.claude/commands/kiro/spec-requirements.md-107-109 (1)
107-109: ファイル末尾が不完全です。
spec-status.mdと同様に、このファイルもthinkという単語で終わっています。これがKiRoワークフローテンプレートの意図的なパターンであれば問題ありませんが、そうでなければ修正が必要です。src/__tests__/features/memory/memoryService.test.ts-483-486 (1)
483-486:MemorySearchResultインターフェースを削除し、インポートに追加してください。
MemorySearchResultはすでに@/features/memory/memoryServiceからエクスポートされているため、ファイル末尾のローカル定義は不要です。既存のインポートにMemorySearchResultを追加してください。.claude/commands/kiro/spec-status.md-95-97 (1)
95-97: ファイル末尾が不完全です。ファイルが
thinkという単語で終わっており、コードブロックが正しく閉じられていないか、内容が不完全なようです。これは意図的なプレースホルダーでしょうか?🔎 修正案
- Run with no argument or use wildcard - Shows all specs in `.kiro/specs/` with their status - -thinkまたは、
thinkがテンプレートの一部である場合は、その目的を明確にするコメントを追加してください。CLAUDE.md-17-30 (1)
17-30: コードブロックに言語指定を追加マークダウンのコードブロックには言語指定子を追加することで、適切なシンタックスハイライトが適用されます。
🔎 提案される修正
-``` +```plaintext src/ ├── components/ # UIコンポーネント ├── features/ # 機能別モジュールsrc/__tests__/hooks/useVoiceRecognition.test.ts-1039-1104 (1)
1039-1104: stopListening の呼び出し順序検証が機能していません。Line 1086-1090で
stopListeningSpyを作成していますが、このスパイはresult.current.stopListeningに割り当てられていません。そのため、イベントハンドラー内で実際に呼ばれるstopListeningはスパイではなく元の関数であり、callOrder配列には'stopListening'が記録されません。実装(useVoiceRecognition.ts の handleKeyUp)は設計通りに
await stopListening()の後にonChatProcessStart()を呼び出していますが、このテストでは呼び出し順序を実際には検証できていません。Line 1103の最終アサーションも、stopListeningが定義されているかのみを確認しており、呼び出し順序を検証していません。.claude/commands/kiro/spec-tasks.md-151-153 (1)
151-153: ファイル末尾の不完全なコードブロック。ファイルが
thinkと開始バッククォートで終了していますが、これは不完全なコードブロックまたはフォーマットエラーのように見えます。🔎 修正の提案
**Note**: The implementation phase will guide you through executing tasks with appropriate context and validation. - -think -```または意図的なブロックであれば閉じる必要があります:
**Note**: The implementation phase will guide you through executing tasks with appropriate context and validation. - -think -``` +```
🧹 Nitpick comments (48)
src/__tests__/hooks/useFullscreen.test.ts (2)
115-142: エラーハンドリングのテストカバレッジを追加することを推奨します。基本的な動作は適切にテストされていますが、実装にはエラーハンドリング(try-catchとconsole.warn)が含まれています。requestFullscreenが失敗するケースのテストを追加することで、カバレッジがより包括的になります。
🔎 エラーケースのテスト例
+ it('should handle requestFullscreen errors gracefully', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() + mockRequestFullscreen.mockRejectedValueOnce(new Error('User gesture required')) + + const { result } = renderHook(() => useFullscreen()) + + await act(async () => { + await result.current.requestFullscreen() + }) + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Fullscreen request failed:', + expect.any(Error) + ) + consoleWarnSpy.mockRestore() + })
144-165: exitFullscreenについてもエラーハンドリングのテストを追加することを推奨します。基本動作は正しくテストされていますが、実装にはエラーハンドリングが含まれているため、exitFullscreenが失敗するケースもテストすることで完全性が向上します。
🔎 エラーケースのテスト例
+ it('should handle exitFullscreen errors gracefully', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() + mockFullscreenElement = document.documentElement + mockExitFullscreen.mockRejectedValueOnce(new Error('Exit failed')) + + const { result } = renderHook(() => useFullscreen()) + + await act(async () => { + await result.current.exitFullscreen() + }) + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Exit fullscreen failed:', + expect.any(Error) + ) + consoleWarnSpy.mockRestore() + })src/__tests__/components/settings/slideConvert.test.tsx (5)
38-52: セレクターパターンの型安全性を向上できます。
selector ? selector(state) : stateのパターンは、セレクターがfalseや0などの falsy 値を返す場合に誤動作する可能性があります。🔎 より安全な実装の提案
default: jest.fn((selector) => { const state = { selectAIService: 'openai', selectLanguage: 'ja', selectAIModel: 'gpt-4o', enableMultiModal: true, multiModalMode: 'auto', customModel: '', } - return selector ? selector(state) : state + return typeof selector === 'function' ? selector(state) : state }),
85-97: ボタン要素のクエリ方法をより堅牢にできます。
getByTextとclosest('button')の組み合わせは、DOM 構造の変更に脆弱です。getByRoleを使用することで、セマンティックかつ保守性の高いテストになります。🔎 推奨される実装
it('should render file upload button enabled in normal mode', () => { render(<SlideConvert onFolderUpdate={jest.fn()} />) - const uploadButton = screen.getByText('ファイルを選択') + const uploadButton = screen.getByRole('button', { name: 'ファイルを選択' }) expect(uploadButton).toBeInTheDocument() - expect(uploadButton.closest('button')).not.toBeDisabled() + expect(uploadButton).not.toBeDisabled() }) it('should render convert button enabled in normal mode', () => { render(<SlideConvert onFolderUpdate={jest.fn()} />) - const convertButton = screen.getByText('変換') + const convertButton = screen.getByRole('button', { name: '変換' }) expect(convertButton).toBeInTheDocument() - expect(convertButton.closest('button')).not.toBeDisabled() + expect(convertButton).not.toBeDisabled() })
112-122: ボタン要素のクエリ方法を統一して堅牢にしてください。通常モードのテストと同様に、
getByRoleを使用することで、DOM 構造の変更に強いテストになります。🔎 推奨される実装
it('should render file upload button disabled in demo mode', () => { render(<SlideConvert onFolderUpdate={jest.fn()} />) - const uploadButton = screen.getByText('ファイルを選択') - expect(uploadButton.closest('button')).toBeDisabled() + const uploadButton = screen.getByRole('button', { name: 'ファイルを選択' }) + expect(uploadButton).toBeDisabled() }) it('should render convert button disabled in demo mode', () => { render(<SlideConvert onFolderUpdate={jest.fn()} />) - const convertButton = screen.getByText('変換') - expect(convertButton.closest('button')).toBeDisabled() + const convertButton = screen.getByRole('button', { name: '変換' }) + expect(convertButton).toBeDisabled() })
131-135: スタイルのテスト方法を実装詳細に依存しないようにできます。CSS クラス名
opacity-50を直接テストすると、スタイリング実装の変更に脆弱になります。data 属性や計算されたスタイルをテストする方が保守性が高くなります。🔎 推奨される実装案
オプション 1: data 属性を使用(コンポーネント側の変更が必要)
コンポーネントに
data-demo-disabledなどの属性を追加し、それをテスト:it('should apply grayed out style to form in demo mode', () => { const { container } = render(<SlideConvert onFolderUpdate={jest.fn()} />) const form = container.querySelector('form') - expect(form?.parentElement).toHaveClass('opacity-50') + expect(form?.parentElement).toHaveAttribute('data-demo-disabled', 'true') })オプション 2: 計算されたスタイルをテスト(より統合的)
it('should apply grayed out style to form in demo mode', () => { const { container } = render(<SlideConvert onFolderUpdate={jest.fn()} />) const form = container.querySelector('form') - expect(form?.parentElement).toHaveClass('opacity-50') + const wrapper = form?.parentElement + expect(wrapper).toHaveStyle({ opacity: expect.stringMatching(/0\.5|0\.50/) }) })
73-137: テストカバレッジを拡張できます。現在のテストは基本的な UI 状態を適切にカバーしていますが、以下のシナリオを追加することでより堅牢になります:
- ファイルアップロードのインタラクション
- 変換ボタンクリック時の動作
- モデル選択の変更
- ローディング状態の表示
- エラーハンドリング
- アクセシビリティ(ARIA 属性、キーボードナビゲーション)
これらのテストシナリオの実装をお手伝いしましょうか?新しい issue を作成することもできます。
src/__tests__/hooks/useRealtimeVoiceAPI.test.ts (1)
82-102: テストカバレッジの拡張を推奨
getVoiceLanguageCode関数は 16 種類の言語をサポートしていますが(src/utils/voiceLanguage.tsを参照)、現在のテストは 4 言語とフォールバックのみをカバーしています。以下の言語のテストが不足しています:vi, fr, es, pt, de, ru, it, ar, hi, pl, th
テストカバレッジを向上させるため、これらの言語マッピングのテストケースを追加することを検討してください。
🔎 提案: 不足している言語のテストケースを追加
it('不明な言語はja-JPにフォールバックする', () => { expect(getVoiceLanguageCode('unknown')).toBe('ja-JP') }) + + it('viを渡すとvi-VNを返す', () => { + expect(getVoiceLanguageCode('vi')).toBe('vi-VN') + }) + + it('frを渡すとfr-FRを返す', () => { + expect(getVoiceLanguageCode('fr')).toBe('fr-FR') + }) + + it('esを渡すとes-ESを返す', () => { + expect(getVoiceLanguageCode('es')).toBe('es-ES') + }) + + // 他の言語も同様に追加 })src/__tests__/hooks/errorHandling.test.ts (1)
87-113: 定数を実装コードと共有することを検討してください。これらの定数はテスト専用に定義されていますが、実装コードでも同じ値を使用する必要があります。定数を共通モジュールに移動し、テストと実装の両方で使用することで、一貫性を保証できます。
💡 共通定数モジュールの例
新しいファイル
src/features/constants/errorHandling.tsを作成:export const ERROR_TOAST_TAGS = { MICROPHONE_PERMISSION_ERROR: 'microphone-permission-error', MICROPHONE_PERMISSION_ERROR_FIREFOX: 'microphone-permission-error-firefox', SPEECH_RECOGNITION_NOT_SUPPORTED: 'speech-recognition-not-supported', SPEECH_RECOGNITION_ERROR: 'speech-recognition-error', NO_SPEECH_DETECTED: 'no-speech-detected', NO_SPEECH_DETECTED_LONG_SILENCE: 'no-speech-detected-long-silence', WHISPER_ERROR: 'whisper-error', } as const export const ERROR_TOAST_MESSAGE_KEYS = { MICROPHONE_PERMISSION_DENIED: 'Toasts.MicrophonePermissionDenied', FIREFOX_NOT_SUPPORTED: 'Toasts.FirefoxNotSupported', SPEECH_RECOGNITION_NOT_SUPPORTED: 'Toasts.SpeechRecognitionNotSupported', SPEECH_RECOGNITION_ERROR: 'Toasts.SpeechRecognitionError', NO_SPEECH_DETECTED: 'Toasts.NoSpeechDetected', WHISPER_ERROR: 'Toasts.WhisperError', } as constこのファイルをテストと実装の両方でインポートして使用します。
.claude/skills/openai-voice-agents/docs/index.md (1)
17-17: 見出しレベルの段階的な増加を確認してください見出しが
h3(###)として定義されていますが、その前にh2レベルの見出しが存在しません。Markdownの構造上、見出しレベルは1段階ずつ増加させる必要があります。🔎 推奨される修正
-### Key features +## Key features.kiro/settings/templates/steering/tech.md (1)
51-51: Markdownの書式を改善できますイタリック体(強調)が使用されていますが、これは注釈として意図されているようです。Markdownリンターがこれを見出しの誤用として検出しています。
🔎 推奨される修正
より明確な注釈形式に変更することをお勧めします:
---- - -_Document standards and patterns, not every dependency_ +--- +> **注記:** 標準とパターンを文書化し、すべての依存関係を列挙しないでください。または、プレーンテキストとして:
---- - -_Document standards and patterns, not every dependency_ +--- +注: 標準とパターンを文書化し、すべての依存関係を列挙しないでください。package.json (1)
49-49: face-api.js@0.22.2 のセキュリティ確認済み
face-api.js@0.22.2は最新バージョンであり、公開されているセキュリティアドバイザリ(Snyk、Socket等)でも既知の脆弱性は報告されていません。ただし、このパッケージは約5年間更新されておらず事実上メンテナンスが停止している状態です。将来の互換性について懸念がある場合は、積極的にメンテナンスされている @vladmandic/face-api フォークの使用を検討してください。.claude/skills/openai-voice-agents/docs/quickstart.md (1)
15-155: コードブロックに言語指定子を追加することを検討してください。ドキュメント全体で複数のコードブロックに言語指定子が欠けています(lines 15, 23, 35, 63, 79, 95, 103, 153)。シンタックスハイライトのために言語を指定することで、可読性が向上します。
例:
```bash npm install @openai/agents zod@3 ```ただし、これはOpenAIの公式ドキュメントから取得されたコンテンツであるため、アップストリームで修正される可能性があります。
.claude/agents/playwright-reporter.md (1)
73-73: マークダウンのエスケープ記法に問題があります
mcp**chrome-devtools**\*の記法が意図した表記と異なっている可能性があります。ヘッダーのtoolsフィールド(行4)ではmcp__chrome-devtools__*となっていますが、ここではアスタリスクのエスケープにより太字として解釈されています。🔎 修正案
-- **mcp**chrome-devtools**\***: ブラウザ操作(スクリーンショット、ページ操作など) +- **mcp__chrome-devtools__\***: ブラウザ操作(スクリーンショット、ページ操作など)src/__tests__/components/settings/voice.test.tsx (1)
228-236: クラウドTTSオプションの存在確認が不十分な可能性
if (option)の条件により、オプションが存在しない場合にテストがサイレントにパスしてしまいます。これにより、期待するオプションが実際には存在しないケースを検出できません。🔎 修正案
cloudOptions.forEach((optionValue) => { const option = Array.from(options).find( (opt) => opt.value === optionValue ) - if (option) { - expect(option).not.toBeDisabled() - } + // オプションが存在することを確認し、無効化されていないことを検証 + expect(option).toBeDefined() + expect(option).not.toBeDisabled() })src/__tests__/features/stores/settingsRealtimeApi.test.ts (2)
68-79: テスト内容とテスト名が一致していませんテスト名は「setState後も」と記載されていますが、実際には初期状態のみを検証しており、
setStateを呼び出した後の動作は検証されていません。コメントでUI保護について言及していますが、テストとして不完全です。🔎 修正案(テスト名をより正確に)
- it('should keep realtimeAPIMode false in demo mode even after setState', () => { - // Note: In demo mode, UI buttons are disabled, so setState shouldn't be called - // This test verifies the initial state is correctly set to false + it('should initialize realtimeAPIMode to false in demo mode', () => { const settingsStore = require('@/features/stores/settings').default // Initial state should be false in demo mode expect(settingsStore.getState().realtimeAPIMode).toBe(false) - - // Even if we try to set it (which shouldn't happen in practice due to disabled UI), - // the UI level protection prevents this. But the store itself doesn't block setState. - // The protection is at the initialization and rehydration level. })
81-86: 同様にテスト名と内容の不一致がありますこのテストも「setState後も」と記載されていますが、初期状態の検証のみです。
🔎 修正案
- it('should keep audioMode false in demo mode even after setState', () => { + it('should initialize audioMode to false in demo mode', () => { const settingsStore = require('@/features/stores/settings').default // Initial state should be false in demo mode expect(settingsStore.getState().audioMode).toBe(false) }).kiro/settings/templates/steering-custom/authentication.md (1)
20-25: コードブロックに言語指定がありません静的解析ツール(markdownlint)により、フェンスコードブロックに言語指定がないことが報告されています。ドキュメントの一貫性のため、言語を指定するか
textを使用することを推奨します。🔎 修正案
-``` +```text 1) User proves identity (credentials or provider) 2) Server verifies and issues token/session 3) Client sends token per request 4) Server verifies token and proceeds</details> </blockquote></details> <details> <summary>.kiro/settings/templates/steering-custom/deployment.md (1)</summary><blockquote> `19-21`: **フェンスドコードブロックに言語指定の追加を検討してください。** 静的解析ツールがこのコードブロックに言語指定がないことを指摘しています。フロー図として `text` または `plaintext` を指定することで、リンターの警告を解消できます。 <details> <summary>🔎 修正案</summary> ```diff -``` +```text Code → Test → Build → Scan → Deploy (staged) → Verify -``` +```src/__tests__/components/settings/modelProvider/OpenAIConfig.test.tsx (1)
152-161: セレクターの脆弱性: インデックスベースのボタン選択は壊れやすいです。
buttons[0]でRealtime APIトグルを取得していますが、UIの変更でボタンの順序が変わると失敗する可能性があります。より堅牢なセレクターの使用を検討してください。🔎 改善案
it('should allow toggling Realtime API mode in normal mode', () => { render(<OpenAIConfig {...defaultProps} />) - const buttons = screen.getAllByRole('button') - const realtimeToggle = buttons[0] // First OFF button is Realtime API toggle + const realtimeSection = screen + .getByText('Realtime APIモード') + .closest('div')?.parentElement + const realtimeToggle = realtimeSection?.querySelector('button') - fireEvent.click(realtimeToggle) + fireEvent.click(realtimeToggle!) // Check that setState was called (mock would be called) const settingsStore = require('@/features/stores/settings').default expect(settingsStore.setState).toHaveBeenCalled() })src/__tests__/features/memory/memoryStore.test.ts (1)
8-13:structuredCloneポリフィルの制限についてコメントを追加することを検討してください。
JSON.parse(JSON.stringify(obj))による実装は、Dateオブジェクト、undefined、関数、循環参照などを正しく処理できません。MemoryRecordはシリアライズ可能なデータのみを含むため、このテストでは問題ありませんが、制限事項をコメントで明記すると将来の保守性が向上します。🔎 改善案
// Polyfill structuredClone for fake-indexeddb in Jest environment +// Note: This simplified polyfill only handles JSON-serializable data. +// It doesn't support Date objects, undefined, functions, or circular references. if (typeof globalThis.structuredClone === 'undefined') { globalThis.structuredClone = <T>(obj: T): T => { return JSON.parse(JSON.stringify(obj)) } }.kiro/settings/rules/tasks-parallel-analysis.md (1)
1-39: LGTM!並列タスク分析ルールが包括的に文書化されています。並列実行可能なタスクを識別するための基準、マーキング規則、品質チェックリストが明確に定義されています。
オプションの改善提案:9〜14行目の基準リストで、4つの項目すべてが「**No」で始まっています。これは技術文書としては許容範囲ですが、可読性をさらに向上させたい場合は、一部を言い換えることを検討してください(例:「Lacks data dependency」、「Touches no conflicting files」など)。
src/__tests__/features/kiosk/guidanceMessage.test.tsx (1)
79-93: スタイリングテストが機能していますが、より具体的な検証を検討してください。テストは正しく動作していますが、87〜92行目のフォントサイズテストで正規表現
/text-(2xl|3xl|4xl)/を使用しています。コンポーネントコードを見ると、text-3xlが具体的に使用されています。より厳密な検証を行いたい場合は、以下を検討してください:
🔎 より具体的なアサーション案
- it('applies large font size', () => { + it('applies text-3xl font size', () => { render(<GuidanceMessage message="話しかけてね!" visible={true} />) const element = screen.getByTestId('guidance-message') - expect(element.className).toMatch(/text-(2xl|3xl|4xl)/) + expect(element).toHaveClass('text-3xl') })現在の実装でも機能しますが、より具体的なアサーションはリグレッションをより確実に検出できます。
src/__tests__/hooks/useDemoMode.test.ts (1)
40-47: メモ化の検証方法を明確化することを推奨します。現在のテストは返されたオブジェクト全体の参照を比較していますが、
useDemoModeはuseMemoを使用してisDemoModeの値をメモ化しています(オブジェクト自体ではありません)。テストの意図をより明確にするため、
isDemoModeの値の安定性を検証することを検討してください。🔎 テストの意図を明確化する改善案
it('should memoize the result', () => { process.env.NEXT_PUBLIC_DEMO_MODE = 'true' const { result, rerender } = renderHook(() => useDemoMode()) - const firstResult = result.current + const firstIsDemoMode = result.current.isDemoMode rerender() - expect(result.current).toEqual(firstResult) + expect(result.current.isDemoMode).toBe(firstIsDemoMode) + expect(result.current.isDemoMode).toBe(true) }).claude/skills/openai-voice-agents/SKILL.md (1)
34-39: コードブロックに言語指定を追加してください。Markdownのコードブロックに言語を指定することで、構文ハイライトとツールの互換性が向上します。
🔎 修正案
-``` +```markdown [Answer based on documentation] **Source:** [source_url] **Fetched:** [fetched_at] -``` +```src/__tests__/hooks/useWhisperRecognition.test.ts (1)
79-129: テストの重複を統合することを推奨します。Lines 79-129の3つのテストは、いずれも同じパターン(再レンダリング後のハンドラ参照の安定性)を検証しています。
テストの保守性を向上させるため、これらを1つの統合テストにまとめることを検討してください。
🔎 統合案
- describe('Requirement 7.1: processWhisperRecognition関数がuseCallbackでラップされている', () => { - it('processWhisperRecognitionがstopListeningの依存配列に含まれること', () => { + describe('Requirement 7.1: ハンドラ関数の参照安定性', () => { + it('すべてのハンドラ関数が再レンダリング後も同じ参照を保持すること', () => { const { result, rerender } = renderHook(() => useWhisperRecognition(mockOnChatProcessStart) ) const stopListeningFirst = result.current.stopListening + const startListeningFirst = result.current.startListening + const toggleListeningFirst = result.current.toggleListening rerender() const stopListeningSecond = result.current.stopListening + const startListeningSecond = result.current.startListening + const toggleListeningSecond = result.current.toggleListening expect(stopListeningFirst).toBe(stopListeningSecond) + expect(startListeningFirst).toBe(startListeningSecond) + expect(toggleListeningFirst).toBe(toggleListeningSecond) }) - - it('startListeningの参照が安定していること', () => { ... }) - it('toggleListeningの参照が安定していること', () => { ... }) }).kiro/settings/rules/design-discovery-full.md (1)
1-113: 設計ディスカバリープロセスのドキュメントが包括的です。設計調査の手順が論理的に構成されており、要件分析から技術調査、リスク評価までカバーしています。
静的解析ツールが指摘しているように、11行目などでボールドテキスト(
**Map Requirements to Technical Needs**)が見出しの代わりに使用されています。一貫性のため、以下のようにMarkdownの見出し記法(###)を使用することを検討してください:🔎 オプショナルな改善提案
-**Map Requirements to Technical Needs** +### Map Requirements to Technical Needs同様のパターンが20行目、30行目などにも存在します。
.kiro/settings/templates/specs/tasks.md (1)
1-23: タスクフォーマットテンプレートが明確です。実装タスクの構造化されたフォーマット(メジャータスクのみ、メジャー+サブタスク構造)が提供されており、並列実行マーカーやテストカバレッジの表記方法も説明されています。
静的解析ツールが指摘しているように、21行目のコードスパン内にスペースが含まれています:
🔎 オプショナルな改善提案
-> **Parallel marker**: Append ` (P)` only to tasks that can be executed in parallel. Omit the marker when running in `--sequential` mode. +> **Parallel marker**: Append `(P)` only to tasks that can be executed in parallel. Omit the marker when running in `--sequential` mode.src/__tests__/features/memory/memoryContextBuilder.test.ts (2)
23-33: ヘルパー関数の重複を解消してください。
createMemoryRecordが2箇所で定義されています(Lines 23-33と184-194)。テストファイル全体で共有できるようにトップレベルで1つ定義し、各describeブロック内で再利用することを推奨します。🔎 提案: ヘルパー関数をトップレベルに移動
+// 全テストで共有するヘルパー関数 +const createMemoryRecord = ( + overrides: Partial<MemoryRecord> = {} +): MemoryRecord => ({ + id: 'test-id', + role: 'user', + content: 'テストメッセージ', + embedding: [0.1, 0.2, 0.3], + timestamp: '2025-01-15T14:30:00Z', + sessionId: 'session-1', + ...overrides, +}) + describe('MemoryContextBuilder', () => { let builder: MemoryContextBuilder beforeEach(() => { builder = new MemoryContextBuilder() }) describe('buildContext', () => { - const createMemoryRecord = ( - overrides: Partial<MemoryRecord> = {} - ): MemoryRecord => ({ - id: 'test-id', - role: 'user', - content: 'テストメッセージ', - embedding: [0.1, 0.2, 0.3], - timestamp: '2025-01-15T14:30:00Z', - sessionId: 'session-1', - ...overrides, - }) it('should return empty string for empty memories array (Req 4.4)', () => { // ... }) }) describe('token limit enforcement (Req 4.3, 4.5)', () => { - const createMemoryRecord = ( - overrides: Partial<MemoryRecord> = {} - ): MemoryRecord => ({ - id: 'test-id', - role: 'user', - content: 'テストメッセージ', - embedding: [0.1, 0.2, 0.3], - timestamp: '2025-01-15T14:30:00Z', - sessionId: 'session-1', - ...overrides, - }) it('should truncate memories when exceeding maxTokens', () => { // ... }) }) })Also applies to: 184-194
161-167: トークン推定のテストをより具体的にすることを検討してください。現在のテストは結果が0より大きいか、範囲内かのみをチェックしています。トークン推定ロジックが正しく動作していることをより明確に検証するため、少なくとも1つのテストケースで期待されるトークン数の具体的な値をアサートすることを推奨します。
例: 英語の場合、約4文字で1トークンなので、25文字の場合は約6-7トークンになることを検証。
src/__tests__/components/presenceSettings.test.tsx (2)
119-125: ボタンの特定方法を改善してください。
getAllByText('StatusOff')[0]を使用してトグルボタンを特定していますが、これは配列のインデックスに依存しており、UI構造の変更に対して脆弱です。より堅牢なアプローチとして、
data-testid属性を使用するか、より具体的なセレクタ(例:getByRole('button', { name: /presence detection/i }))を使用することを推奨します。
196-204: ボタンの特定方法の一貫性を保ってください。Line 119と同様に、
getAllByText('StatusOff')[1]を使用してデバッグモードトグルを特定しています。UI要素の順序に依存せず、より明示的な方法で要素を特定することを推奨します。src/__tests__/hooks/usePresetLoader.test.ts (1)
215-260: 不要な動的インポートを削除してください。
getPresetWithFallbackは既にファイル上部(Line 13)でインポートされているため、各テストケース内での動的インポート(await import(...))は不要です。これによりテストの実行速度が低下し、コードの可読性も下がります。🔎 提案する修正
+import { getPresetWithFallback } from '@/features/presets/presetLoader' + describe('getPresetWithFallback', () => { it('コンテンツが存在する場合はそれを返す', async () => { - const { getPresetWithFallback } = await import( - '@/features/presets/presetLoader' - ) - const result = getPresetWithFallback('カスタムプロンプト', undefined) expect(result).toBe('カスタムプロンプト') }) it('コンテンツがnullの場合は環境変数を使用', async () => { - const { getPresetWithFallback } = await import( - '@/features/presets/presetLoader' - ) - const result = getPresetWithFallback(null, '環境変数のプロンプト') expect(result).toBe('環境変数のプロンプト') }) // 他のテストケースも同様に修正 }).claude/commands/kiro/steering.md (1)
96-105: コードブロックに言語識別子を追加してください。出力例のコードブロックに言語識別子が指定されていません。Markdown linterの警告を解消し、シンタックスハイライトを有効にするため、言語識別子(例:
textまたはmarkdown)を追加することを推奨します。🔎 提案する修正
### Bootstrap: -``` +```text ✅ Steering Created ## Generated: - product.md: [Brief description] - tech.md: [Key stack] - structure.md: [Organization] Review and approve as Source of Truth. ``` ### Sync: -``` +```text ✅ Steering Updated ## Changes: - tech.md: React 18 → 19 - structure.md: Added API pattern ## Code Drift: - Components not following import conventions ## Recommendations: - Consider api-standards.md ```Also applies to: 109-121
src/__tests__/hooks/useSilenceDetection.test.ts (1)
107-153:resolveStopListeningの初期化パターンについてテスト3.2で
resolveStopListeningがletで宣言されていますが、Promiseが作成される前にアクセスされる可能性があります。現在の実装では問題ありませんが、より安全なパターンとして明示的な型アサーションまたは初期化を検討してください。🔎 より安全な初期化パターンの提案
- it('3.2: stopListeningFnが実行中でも追加の呼び出しがブロックされる', async () => { - let resolveStopListening: () => void + it('3.2: stopListeningFnが実行中でも追加の呼び出しがブロックされる', async () => { + let resolveStopListening: (() => void) | undefined const mockStopListening = jest.fn().mockImplementation(() => { return new Promise<void>((resolve) => { resolveStopListening = resolve }) }).claude/commands/kiro/steering-custom.md (1)
87-105: コードブロックに言語指定を追加してください。markdownlintが指摘しているように、フェンスドコードブロックには言語を指定することが推奨されます。出力例のブロックには
textまたはmarkdownを指定してください。🔎 修正案
-``` +```text ✅ Custom Steering Created ## Created:src/__tests__/hooks/usePresenceDetection.test.ts (2)
673-740: スキップされたテストについてクールダウン中の再検知抑制テストがスキップされており、TODOコメントで
setIntervalのコールバック更新タイミングの問題が説明されています。実際の動作ではuseEffectでインターバルが再作成されるため正常に機能するとのことです。このスキップされたテストを追跡するために、Issueを作成することをお勧めします。
このテストの問題を追跡するためのIssueを作成しますか?
262-275: videoRefの設定パターンをヘルパー関数に抽出することを検討
videoRef.current = mockVideoElementの設定パターンがテスト全体で繰り返されています。可読性とメンテナンス性を向上させるために、ヘルパー関数への抽出を検討してください。🔎 ヘルパー関数の提案
// テストファイルの先頭に追加 const setVideoRef = ( result: { current: { videoRef: React.RefObject<HTMLVideoElement | null> } } ) => { ;(result.current.videoRef as React.MutableRefObject<HTMLVideoElement | null>).current = mockVideoElement } // 使用例 setVideoRef(result)src/__tests__/features/memory/memoryService.test.ts (1)
454-479: パフォーマンステストのしきい値について要件3.5では100ms以内の検索完了を求めていますが、テストでは500msのバッファを許容しています。APIコールはモックされているため、実際の処理時間のみを測定しています。
テスト環境のばらつきを考慮しつつも、より厳密なしきい値(例:200ms)を設定することで、パフォーマンス回帰をより早期に検出できる可能性があります。
.claude/skills/openai-voice-agents/scripts/search_docs.py (3)
16-16: 未使用のインポート:datetime
datetimeモジュールがインポートされていますが、コード内で使用されていません。削除してください。🔎 修正案
from pathlib import Path from typing import List, Dict, Tuple, Optional -from datetime import datetime
159-160: 例外処理をより具体的にすることを検討Ruffが指摘しているように、広範な
Exceptionのキャッチは具体性に欠けます。ファイル読み込み操作で発生する可能性のある具体的な例外をキャッチすることを検討してください。🔎 修正案
- except Exception as e: + except (OSError, IOError, UnicodeDecodeError) as e: print(f"Error reading {file_path}: {e}", file=sys.stderr)
50-54: シンプルなYAMLパーサーの制限事項について現在の実装は
key: value形式の単純なYAMLのみを処理します。以下のケースは正しく処理されません:
- 複数行の値
- コロンを含む引用符付き文字列(例:
title: "Hello: World")- 配列やネストされた構造
これが意図的な制限であれば、docstringに注記を追加することをお勧めします。より堅牢な処理が必要な場合は、
pyyamlライブラリの使用を検討してください。.claude/commands/kiro/validate-design.md (1)
94-94: 箇条書きの書式を修正箇条書きの項目として正しく認識されるよう、ハイフンの前に改行を追加してください。
🔎 提案される修正
-- Review feedback and apply changes if needed - Run `/kiro:spec-tasks $1` to generate implementation tasks +- Review feedback and apply changes if needed +- Run `/kiro:spec-tasks $1` to generate implementation tasks.claude/commands/kiro/spec-design.md (1)
117-121: 文章の可読性を改善3つの連続した文が "For" で始まっているため、文章構造を変えることで可読性が向上します。
🔎 提案される修正
-- **Type Safety**: - - Enforce strong typing aligned with the project's technology stack. - - For statically typed languages, define explicit types/interfaces and avoid unsafe casts. - - For TypeScript, never use `any`; prefer precise types and generics. - - For dynamically typed languages, provide type hints/annotations where available (e.g., Python type hints) and validate inputs at boundaries. - - Document public interfaces and contracts clearly to ensure cross-component type safety. +- **Type Safety**: + - Enforce strong typing aligned with the project's technology stack. + - In statically typed languages, define explicit types/interfaces and avoid unsafe casts. + - TypeScript code must never use `any`; prefer precise types and generics. + - Dynamically typed languages should provide type hints/annotations where available (e.g., Python type hints) and validate inputs at boundaries. + - Document public interfaces and contracts clearly to ensure cross-component type safety..claude/commands/kiro/validate-gap.md (1)
16-16: 文法を修正"needs identified" は非標準的な表現です。"needs to be identified" に修正することで、より標準的な英語表現になります。
🔎 提案される修正
- - Technical research needs identified for design phase + - Technical research needs to be identified for design phasesrc/__tests__/hooks/useIdleMode.test.ts (1)
162-174: タイマークリーンアップのテストが間接的です。このテストはエラーが発生しないことを確認していますが、タイマーが実際にクリアされたことを直接検証していません。現状でも機能しますが、より強固な検証が望ましい場合があります。
🔎 より明示的な検証の提案
describe('useEffect cleanupでタイマークリア', () => { it('should cleanup timer on unmount', () => { - const { unmount } = renderHook(() => useIdleMode({})) + const { result, unmount } = renderHook(() => useIdleMode({})) + const initialSeconds = result.current.secondsUntilNextSpeech unmount() - // Timer should be cleared (no error on advancing timers after unmount) - expect(() => { - act(() => { - jest.advanceTimersByTime(1000) - }) - }).not.toThrow() + // アンマウント後もタイマーが進まないことを確認 + act(() => { + jest.advanceTimersByTime(5000) + }) + // アンマウント後は状態が更新されないことを検証 + expect(true).toBe(true) // No React warnings about updating unmounted component }) })src/__tests__/integration/presenceDetectionIntegration.test.tsx (1)
218-242: コールバックテストが実際の呼び出しを検証していません。このテストはコールバックプロパティを受け取ってフックが初期化されることを確認していますが、顔検出やグリーティング時にコールバックが実際に呼び出されることを検証していません。これは Task 5.1 の統合テストとしては基本動作の確認で十分かもしれませんが、将来的にはコールバック実行のテストも検討してください。
src/__tests__/hooks/useVoiceRecognition.test.ts (1)
646-648: 弱いアサーションexpect(true).toBe(true)が使用されています。Lines 647 と 708 で
expect(true).toBe(true)が使用されていますが、これは「エラーが発生しないこと」を確認するだけで、実際の動作を検証していません。将来的にはより具体的なアサーションへの改善を検討してください。🔎 改善案
// エラーなくモード変更が完了すること - expect(true).toBe(true) + // モード変更後もフックが正常に動作することを確認 + expect(result.current.isListening).toBeDefined()// 少なくともエラーなく動作することを確認 - expect(true).toBe(true) + // continuousMicListeningModeがfalseの場合、自動でリスニングが開始されないことを確認 + expect(result.current.isListening).toBe(false)Also applies to: 706-709
| it('should not render when debug mode is disabled', () => { | ||
| mockSettingsStore.mockImplementation((selector) => { | ||
| const state = { presenceDebugMode: false } | ||
| return selector(state as any) | ||
| }) | ||
|
|
||
| const { container } = render( | ||
| <PresenceDebugPreview videoRef={mockVideoRef} detectionResult={null} /> | ||
| ) | ||
| expect(container.firstChild).toBeNull() | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# PresenceDebugPreviewコンポーネントの実装を探す
find . -name "*.tsx" -o -name "*.ts" | grep -v __tests__ | xargs grep -l "PresenceDebugPreview" | head -5Repository: tegnike/aituber-kit
Length of output: 141
🏁 Script executed:
# PresenceDebugPreviewコンポーネントの条件付きレンダリング実装を確認
rg "PresenceDebugPreview" --type tsx --type ts -g "!**/__tests__/**" -A 30Repository: tegnike/aituber-kit
Length of output: 90
🏁 Script executed:
# テストファイルの完全な内容を確認
cat -n src/__tests__/components/presenceDebugPreview.test.tsx | head -60Repository: tegnike/aituber-kit
Length of output: 2160
🏁 Script executed:
cat -n src/components/presenceDebugPreview.tsx | head -80Repository: tegnike/aituber-kit
Length of output: 2886
🏁 Script executed:
# Check the full component implementation
wc -l src/components/presenceDebugPreview.tsxRepository: tegnike/aituber-kit
Length of output: 106
🏁 Script executed:
cat -n src/components/presenceDebugPreview.tsx | tail -30Repository: tegnike/aituber-kit
Length of output: 1149
🏁 Script executed:
cat -n src/__tests__/components/presenceDebugPreview.test.tsx | sed -n '130,160p'Repository: tegnike/aituber-kit
Length of output: 1109
クリティカル:コンポーネント実装に条件付きレンダリング不足。
PresenceDebugPreviewコンポーネントは、presenceDebugModeがfalseのとき、コンポーネント全体を返さないようにする必要があります。現在の実装では、デバッグモードが無効な場合でも<div>ラッパーとビデオ要素が常にレンダリングされており、テスト期待値(container.firstChildがnull)に反しています。
コンポーネントの早期リターン処理を追加してください:
if (!presenceDebugMode) {
return null
}また、行142のバウンディングボックステストも確認が必要です。実装ではミラー計算(videoWidth - box.x - box.width)とスケール係数が適用されているため、テスト期待値の計算ロジックと一致していません。
🧰 Tools
🪛 GitHub Actions: Run Tests
[error] 48-48: PresenceDebugPreview should render null when debug mode is disabled, but a video element was found.
🤖 Prompt for AI Agents
In src/__tests__/components/presenceDebugPreview.test.tsx around lines 39 to 49:
the test expects the component to render nothing when presenceDebugMode is false
but the component currently always renders its wrapper; update the
PresenceDebugPreview component to early-return null when presenceDebugMode is
falsy (i.e. add if (!presenceDebugMode) return null at the top of the render),
and also verify/fix the bounding-box logic to match the implementation’s mirror
and scale math (ensure tests compute mirrored X as videoWidth - box.x -
box.width and apply the same scale factor the component uses, or adjust the
component to match the test’s expected coordinate math).
| it('should remove older memories first when truncating (Req 4.3)', () => { | ||
| const memories: MemoryRecord[] = [ | ||
| createMemoryRecord({ | ||
| id: 'old-1', | ||
| content: '古いメッセージ1', | ||
| timestamp: '2025-01-15T08:00:00Z', | ||
| }), | ||
| createMemoryRecord({ | ||
| id: 'old-2', | ||
| content: '古いメッセージ2', | ||
| timestamp: '2025-01-15T09:00:00Z', | ||
| }), | ||
| createMemoryRecord({ | ||
| id: 'new-1', | ||
| content: '新しいメッセージ', | ||
| timestamp: '2025-01-15T14:00:00Z', | ||
| }), | ||
| ] | ||
|
|
||
| const options: ContextOptions = { maxTokens: 50 } | ||
| const result = builder.buildContext(memories, options) | ||
|
|
||
| // 新しいメッセージが優先される(古いものから削除される) | ||
| // トークン上限が厳しい場合、古いメッセージが削除される | ||
| const tokens = builder.estimateTokens(result) | ||
| expect(tokens).toBeLessThanOrEqual(50) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
古いメモリの削除順序をより厳密に検証してください。
このテストはトークン上限以下であることのみを確認していますが、「古いメッセージから優先的に削除される」という要件(Req 4.3)を直接検証していません。
result の内容を解析して、新しいメッセージ('新しいメッセージ')が含まれ、古いメッセージ('古いメッセージ1' など)が削除されていることを明示的に確認することを推奨します。
🔎 提案: 削除順序の検証を追加
const options: ContextOptions = { maxTokens: 50 }
const result = builder.buildContext(memories, options)
+ // 新しいメッセージが含まれることを確認
+ expect(result).toContain('新しいメッセージ')
+
// 新しいメッセージが優先される(古いものから削除される)
// トークン上限が厳しい場合、古いメッセージが削除される
const tokens = builder.estimateTokens(result)
expect(tokens).toBeLessThanOrEqual(50)🤖 Prompt for AI Agents
In src/__tests__/features/memory/memoryContextBuilder.test.ts around lines 216
to 242, the test only asserts the resulting token count is <= 50 but does not
verify that older memories were removed first as required by Req 4.3; update the
test to parse the builder.buildContext(memories, options) result and assert that
the new message ('新しいメッセージ' / id 'new-1') is present and at least one or both
older messages ('古いメッセージ1' id 'old-1', '古いメッセージ2' id 'old-2') are absent (or
that only the newest entries remain), ensuring the removal order is explicitly
validated in addition to the token count.
| /** | ||
| * @jest-environment jsdom | ||
| */ | ||
|
|
||
| /** | ||
| * エラーハンドリング統一テスト (Requirement 8) | ||
| * | ||
| * 各フックのエラーハンドリングパターンが以下の基準で統一されていることを検証: | ||
| * 1. console.error出力形式の統一 | ||
| * 2. toastStoreを使用したユーザー通知パターンの統一 | ||
| * 3. 同じエラーカテゴリに対して同じトーストタグを使用 | ||
| */ | ||
|
|
||
| import toastStore from '@/features/stores/toast' | ||
|
|
||
| // Mock stores | ||
| jest.mock('@/features/stores/settings', () => ({ | ||
| __esModule: true, | ||
| default: Object.assign( | ||
| jest.fn((selector) => { | ||
| const state = { | ||
| selectLanguage: 'ja', | ||
| realtimeAPIMode: false, | ||
| initialSpeechTimeout: 10, | ||
| noSpeechTimeout: 3, | ||
| continuousMicListeningMode: false, | ||
| openaiKey: 'test-key', | ||
| whisperTranscriptionModel: 'whisper-1', | ||
| } | ||
| return selector(state) | ||
| }), | ||
| { | ||
| getState: jest.fn(() => ({ | ||
| selectLanguage: 'ja', | ||
| realtimeAPIMode: false, | ||
| initialSpeechTimeout: 10, | ||
| noSpeechTimeout: 3, | ||
| continuousMicListeningMode: false, | ||
| openaiKey: 'test-key', | ||
| whisperTranscriptionModel: 'whisper-1', | ||
| })), | ||
| setState: jest.fn(), | ||
| } | ||
| ), | ||
| })) | ||
|
|
||
| const mockAddToast = jest.fn() | ||
| jest.mock('@/features/stores/toast', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| getState: jest.fn(() => ({ addToast: mockAddToast })), | ||
| }, | ||
| })) | ||
|
|
||
| jest.mock('@/features/stores/home', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| setState: jest.fn(), | ||
| getState: jest.fn(() => ({ | ||
| chatProcessing: false, | ||
| isSpeaking: false, | ||
| })), | ||
| }, | ||
| })) | ||
|
|
||
| jest.mock('@/features/stores/websocketStore', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| getState: () => ({ wsManager: null }), | ||
| }, | ||
| })) | ||
|
|
||
| jest.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string) => key, | ||
| }), | ||
| })) | ||
|
|
||
| jest.mock('@/features/messages/speakQueue', () => ({ | ||
| SpeakQueue: { | ||
| stopAll: jest.fn(), | ||
| onSpeakCompletion: jest.fn(), | ||
| removeSpeakCompletionCallback: jest.fn(), | ||
| }, | ||
| })) | ||
|
|
||
| // エラーハンドリングで使用されるべきトーストタグの定義 | ||
| const EXPECTED_TOAST_TAGS = { | ||
| // マイク権限関連 | ||
| MICROPHONE_PERMISSION_ERROR: 'microphone-permission-error', | ||
| MICROPHONE_PERMISSION_ERROR_FIREFOX: 'microphone-permission-error-firefox', | ||
|
|
||
| // 音声認識関連 | ||
| SPEECH_RECOGNITION_NOT_SUPPORTED: 'speech-recognition-not-supported', | ||
| SPEECH_RECOGNITION_ERROR: 'speech-recognition-error', | ||
|
|
||
| // 音声検出関連 | ||
| NO_SPEECH_DETECTED: 'no-speech-detected', | ||
| NO_SPEECH_DETECTED_LONG_SILENCE: 'no-speech-detected-long-silence', | ||
|
|
||
| // Whisper API関連 | ||
| WHISPER_ERROR: 'whisper-error', | ||
| } | ||
|
|
||
| // エラーハンドリングで使用されるべきトーストメッセージキーの定義 | ||
| const EXPECTED_TOAST_MESSAGE_KEYS = { | ||
| MICROPHONE_PERMISSION_DENIED: 'Toasts.MicrophonePermissionDenied', | ||
| FIREFOX_NOT_SUPPORTED: 'Toasts.FirefoxNotSupported', | ||
| SPEECH_RECOGNITION_NOT_SUPPORTED: 'Toasts.SpeechRecognitionNotSupported', | ||
| SPEECH_RECOGNITION_ERROR: 'Toasts.SpeechRecognitionError', | ||
| NO_SPEECH_DETECTED: 'Toasts.NoSpeechDetected', | ||
| WHISPER_ERROR: 'Toasts.WhisperError', | ||
| } | ||
|
|
||
| describe('エラーハンドリングの統一 (Requirement 8)', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| }) | ||
|
|
||
| describe('8.1 エラーカテゴリとトーストタグの一致', () => { | ||
| it('マイク権限エラーには統一されたタグが使用される', () => { | ||
| // 期待される動作: マイク権限エラー時に 'microphone-permission-error' タグを使用 | ||
| expect(EXPECTED_TOAST_TAGS.MICROPHONE_PERMISSION_ERROR).toBe( | ||
| 'microphone-permission-error' | ||
| ) | ||
| }) | ||
|
|
||
| it('音声認識未サポートエラーには統一されたタグが使用される', () => { | ||
| // 期待される動作: SpeechRecognition APIがない場合に統一タグを使用 | ||
| expect(EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_NOT_SUPPORTED).toBe( | ||
| 'speech-recognition-not-supported' | ||
| ) | ||
| }) | ||
|
|
||
| it('音声未検出には統一されたタグが使用される', () => { | ||
| // 期待される動作: 音声が検出されなかった場合に統一タグを使用 | ||
| expect(EXPECTED_TOAST_TAGS.NO_SPEECH_DETECTED).toBe('no-speech-detected') | ||
| }) | ||
|
|
||
| it('Whisperエラーには統一されたタグが使用される', () => { | ||
| // 期待される動作: Whisper APIエラー時に統一タグを使用 | ||
| expect(EXPECTED_TOAST_TAGS.WHISPER_ERROR).toBe('whisper-error') | ||
| }) | ||
| }) | ||
|
|
||
| describe('8.2 console.error出力形式の統一', () => { | ||
| it('エラーメッセージは一貫したプレフィックスを使用すべき', () => { | ||
| // エラーログの形式が統一されているかを確認するためのパターン | ||
| // 例: 'Error starting recognition:', error | ||
| // 例: 'Whisper transcription error:', error | ||
| // 統一形式: '[コンテキスト] エラー説明:', error | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
|
|
||
| // シミュレートされたエラーログを確認 | ||
| console.error('Error starting recognition:', new Error('test')) | ||
| console.error('Microphone permission error:', new Error('test')) | ||
| console.error('Speech recognition error:', new Error('test')) | ||
|
|
||
| // 各呼び出しで第1引数が文字列であることを確認 | ||
| consoleSpy.mock.calls.forEach((call) => { | ||
| expect(typeof call[0]).toBe('string') | ||
| // エラーメッセージが':'で終わる形式であることを確認 | ||
| expect(call[0]).toMatch(/:\s*$/) | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
| }) | ||
|
|
||
| describe('8.3 toastStoreを使用したユーザー通知パターンの統一', () => { | ||
| it('toastには必須フィールド(message, type, tag)が含まれる', () => { | ||
| // 期待されるtoastの構造 | ||
| const expectedToastStructure = { | ||
| message: expect.any(String), | ||
| type: expect.stringMatching(/^(info|error|warning|success)$/), | ||
| tag: expect.any(String), | ||
| } | ||
|
|
||
| // 各エラーカテゴリで使用されるtoastパターンを検証 | ||
| const sampleToasts = [ | ||
| { | ||
| message: 'Toasts.MicrophonePermissionDenied', | ||
| type: 'error', | ||
| tag: 'microphone-permission-error', | ||
| }, | ||
| { | ||
| message: 'Toasts.NoSpeechDetected', | ||
| type: 'info', | ||
| tag: 'no-speech-detected', | ||
| }, | ||
| { | ||
| message: 'Toasts.WhisperError', | ||
| type: 'error', | ||
| tag: 'whisper-error', | ||
| }, | ||
| ] | ||
|
|
||
| sampleToasts.forEach((toast) => { | ||
| expect(toast).toMatchObject(expectedToastStructure) | ||
| }) | ||
| }) | ||
|
|
||
| it('エラータイプにはerror、情報にはinfoが使用される', () => { | ||
| // エラーカテゴリとtype の対応関係を確認 | ||
| const errorTypeMap = { | ||
| 'microphone-permission-error': 'error', | ||
| 'speech-recognition-not-supported': 'error', | ||
| 'speech-recognition-error': 'error', | ||
| 'whisper-error': 'error', | ||
| 'no-speech-detected': 'info', // 音声未検出は情報レベル | ||
| } | ||
|
|
||
| Object.entries(errorTypeMap).forEach(([tag, expectedType]) => { | ||
| expect(expectedType).toMatch(/^(error|info)$/) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('統一されたエラーハンドリングパターンの定義', () => { | ||
| /** | ||
| * このテストは、各フックで使用すべき統一されたエラーハンドリングパターンを定義します。 | ||
| * 実装時には、以下のパターンに従ってエラーハンドリングを行います。 | ||
| */ | ||
|
|
||
| it('マイク権限エラー時の統一パターン', () => { | ||
| const handleMicrophonePermissionError = ( | ||
| error: Error, | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Microphone permission error:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.MICROPHONE_PERMISSION_DENIED), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.MICROPHONE_PERMISSION_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Permission denied') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleMicrophonePermissionError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Microphone permission error:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.MICROPHONE_PERMISSION_DENIED, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.MICROPHONE_PERMISSION_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声認識未サポートエラー時の統一パターン', () => { | ||
| const handleSpeechRecognitionNotSupported = ( | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Speech Recognition API is not supported in this browser') | ||
| toastStore.getState().addToast({ | ||
| message: t( | ||
| EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_NOT_SUPPORTED | ||
| ), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| }) | ||
| } | ||
|
|
||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleSpeechRecognitionNotSupported(mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Speech Recognition API is not supported in this browser' | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声未検出時の統一パターン', () => { | ||
| const handleNoSpeechDetected = (t: (key: string) => string) => { | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.NO_SPEECH_DETECTED), | ||
| type: 'info', | ||
| tag: EXPECTED_TOAST_TAGS.NO_SPEECH_DETECTED, | ||
| }) | ||
| } | ||
|
|
||
| const mockT = (key: string) => key | ||
|
|
||
| handleNoSpeechDetected(mockT) | ||
|
|
||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.NO_SPEECH_DETECTED, | ||
| type: 'info', | ||
| tag: EXPECTED_TOAST_TAGS.NO_SPEECH_DETECTED, | ||
| }) | ||
| }) | ||
|
|
||
| it('Whisper APIエラー時の統一パターン', () => { | ||
| const handleWhisperError = (error: Error, t: (key: string) => string) => { | ||
| console.error('Whisper transcription error:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.WHISPER_ERROR), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.WHISPER_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Whisper API failed') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleWhisperError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Whisper transcription error:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.WHISPER_ERROR, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.WHISPER_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声認識開始エラー時の統一パターン', () => { | ||
| const handleSpeechRecognitionStartError = ( | ||
| error: Error, | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Error starting recognition:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_ERROR), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Failed to start') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleSpeechRecognitionStartError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Error starting recognition:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_ERROR, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
テストファイル全体の設計について
このテストファイルは「統一されたエラーハンドリングパターン」を定義する仕様書としては価値がありますが、実装の品質を保証するテストとしては不十分です。
現在の問題:
- テストが実装を検証していない
- 回帰を検出できない
- 実装がパターンから逸脱しても検出されない
推奨されるアプローチ:
-
このファイルを2つに分割:
errorHandling.constants.ts: 定数定義(実装でも使用)errorHandling.test.ts: 実際の実装を検証するテスト
-
または、実際のフックをテストに含め、統一パターンに従っているかを検証
このアプローチにより、テストの価値が大幅に向上します。
この改善案の実装サンプルコードを生成しましょうか?新しいissueとして追跡することも可能です。
| describe('統一されたエラーハンドリングパターンの定義', () => { | ||
| /** | ||
| * このテストは、各フックで使用すべき統一されたエラーハンドリングパターンを定義します。 | ||
| * 実装時には、以下のパターンに従ってエラーハンドリングを行います。 | ||
| */ | ||
|
|
||
| it('マイク権限エラー時の統一パターン', () => { | ||
| const handleMicrophonePermissionError = ( | ||
| error: Error, | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Microphone permission error:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.MICROPHONE_PERMISSION_DENIED), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.MICROPHONE_PERMISSION_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Permission denied') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleMicrophonePermissionError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Microphone permission error:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.MICROPHONE_PERMISSION_DENIED, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.MICROPHONE_PERMISSION_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声認識未サポートエラー時の統一パターン', () => { | ||
| const handleSpeechRecognitionNotSupported = ( | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Speech Recognition API is not supported in this browser') | ||
| toastStore.getState().addToast({ | ||
| message: t( | ||
| EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_NOT_SUPPORTED | ||
| ), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| }) | ||
| } | ||
|
|
||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleSpeechRecognitionNotSupported(mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Speech Recognition API is not supported in this browser' | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_NOT_SUPPORTED, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声未検出時の統一パターン', () => { | ||
| const handleNoSpeechDetected = (t: (key: string) => string) => { | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.NO_SPEECH_DETECTED), | ||
| type: 'info', | ||
| tag: EXPECTED_TOAST_TAGS.NO_SPEECH_DETECTED, | ||
| }) | ||
| } | ||
|
|
||
| const mockT = (key: string) => key | ||
|
|
||
| handleNoSpeechDetected(mockT) | ||
|
|
||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.NO_SPEECH_DETECTED, | ||
| type: 'info', | ||
| tag: EXPECTED_TOAST_TAGS.NO_SPEECH_DETECTED, | ||
| }) | ||
| }) | ||
|
|
||
| it('Whisper APIエラー時の統一パターン', () => { | ||
| const handleWhisperError = (error: Error, t: (key: string) => string) => { | ||
| console.error('Whisper transcription error:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.WHISPER_ERROR), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.WHISPER_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Whisper API failed') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleWhisperError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Whisper transcription error:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.WHISPER_ERROR, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.WHISPER_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) | ||
|
|
||
| it('音声認識開始エラー時の統一パターン', () => { | ||
| const handleSpeechRecognitionStartError = ( | ||
| error: Error, | ||
| t: (key: string) => string | ||
| ) => { | ||
| console.error('Error starting recognition:', error) | ||
| toastStore.getState().addToast({ | ||
| message: t(EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_ERROR), | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_ERROR, | ||
| }) | ||
| } | ||
|
|
||
| const testError = new Error('Failed to start') | ||
| const mockT = (key: string) => key | ||
|
|
||
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation() | ||
| handleSpeechRecognitionStartError(testError, mockT) | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Error starting recognition:', | ||
| testError | ||
| ) | ||
| expect(mockAddToast).toHaveBeenCalledWith({ | ||
| message: EXPECTED_TOAST_MESSAGE_KEYS.SPEECH_RECOGNITION_ERROR, | ||
| type: 'error', | ||
| tag: EXPECTED_TOAST_TAGS.SPEECH_RECOGNITION_ERROR, | ||
| }) | ||
|
|
||
| consoleSpy.mockRestore() | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
実際のフック実装をテストするように変更すべきです。
このテストスイートは、テスト内で定義されたサンプルハンドラー関数を検証していますが、実際のフック実装を検証していません。これでは実装が統一されたパターンに従っているかを保証できません。
推奨される改善:
- 実際のフック(例:
useBrowserSpeechRecognition)をインポート - React Testing Libraryの
renderHookを使用してフックをテスト - エラー条件を発生させる
- 実際に呼び出される
console.errorとtoastStore.addToastを検証
💡 実装テストの例
import { renderHook } from '@testing-library/react'
import { useBrowserSpeechRecognition } from '@/features/hooks/useBrowserSpeechRecognition'
describe('useBrowserSpeechRecognition エラーハンドリング', () => {
it('マイク権限エラー時に統一パターンを使用', async () => {
// マイク権限拒否をシミュレート
const mockGetUserMedia = jest.fn().mockRejectedValue(
new DOMException('Permission denied', 'NotAllowedError')
)
Object.defineProperty(global.navigator, 'mediaDevices', {
value: { getUserMedia: mockGetUserMedia },
writable: true,
})
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
const { result } = renderHook(() => useBrowserSpeechRecognition())
// フックのメソッドを呼び出してエラーを発生させる
await act(async () => {
await result.current.startListening()
})
// 統一パターンに従っているかを検証
expect(consoleSpy).toHaveBeenCalledWith(
'Microphone permission error:',
expect.any(Error)
)
expect(mockAddToast).toHaveBeenCalledWith({
message: expect.stringContaining('MicrophonePermissionDenied'),
type: 'error',
tag: 'microphone-permission-error',
})
consoleSpy.mockRestore()
})
})このアプローチにより、実装が実際に統一パターンに従っているかを検証できます。
🤖 Prompt for AI Agents
src/__tests__/hooks/errorHandling.test.ts lines 221-369: the test currently
asserts on inline sample handler functions rather than the real hook
implementation; update the tests to import the actual hook (e.g.,
useBrowserSpeechRecognition), use renderHook from React Testing Library,
simulate error conditions (mock navigator.mediaDevices.getUserMedia,
SpeechRecognition, or Whisper API failures) to trigger the hook's error paths,
spy on console.error and mockAddToast, call the hook methods (via
result.current) inside act to produce the error, and assert that console.error
and toastStore.addToast were called with the unified messages/tags.
| beforeEach(() => { | ||
| settingsStore.setState({ | ||
| kioskModeEnabled: DEFAULT_KIOSK_CONFIG.kioskModeEnabled, | ||
| kioskPasscode: DEFAULT_KIOSK_CONFIG.kioskPasscode, | ||
| kioskGuidanceMessage: DEFAULT_KIOSK_CONFIG.kioskGuidanceMessage, | ||
| kioskGuidanceTimeout: DEFAULT_KIOSK_CONFIG.kioskGuidanceTimeout, | ||
| kioskMaxInputLength: DEFAULT_KIOSK_CONFIG.kioskMaxInputLength, | ||
| kioskNgWords: DEFAULT_KIOSK_CONFIG.kioskNgWords, | ||
| kioskNgWordEnabled: DEFAULT_KIOSK_CONFIG.kioskNgWordEnabled, | ||
| kioskTemporaryUnlock: DEFAULT_KIOSK_CONFIG.kioskTemporaryUnlock, | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -A 30 'DEFAULT_KIOSK_CONFIG' --type=tsRepository: tegnike/aituber-kit
Length of output: 31557
DEFAULT_KIOSK_CONFIGにkioskGuidanceMessageとkioskGuidanceTimeoutプロパティが存在しません。
beforeEachでこれらのプロパティを設定しようとしていますが、実際のDEFAULT_KIOSK_CONFIG定義には存在しません。結果として、これらの値はundefinedとして店舗状態に設定されることになります。DEFAULT_KIOSK_CONFIGにこれらのプロパティを追加するか、beforeEachから削除してください。
🤖 Prompt for AI Agents
In src/__tests__/hooks/useKioskMode.test.ts around lines 14 to 25, the test's
beforeEach references kioskGuidanceMessage and kioskGuidanceTimeout which are
not present on DEFAULT_KIOSK_CONFIG; either add these two properties with
appropriate default values and types to the DEFAULT_KIOSK_CONFIG definition (in
the module where it is declared) or remove those keys from the beforeEach setup
so you only set properties that actually exist; ensure whichever fix you choose
keeps types consistent with the store and tests.
| describe('SpeechRecognition初期化時の言語設定', () => { | ||
| it('ハードコードされたja-JPではなく、getVoiceLanguageCodeを使用すべき', () => { | ||
| // このテストは現在の実装が期待に沿っていないことを確認する(REDフェーズ) | ||
| // 現在のコード: newRecognition.lang = 'ja-JP' (ハードコード) | ||
| // 期待するコード: newRecognition.lang = getVoiceLanguageCode(selectLanguage) | ||
|
|
||
| // selectLanguage='en'の場合、期待値は'en-US' | ||
| const expectedLang = getVoiceLanguageCode('en') | ||
| expect(expectedLang).toBe('en-US') | ||
|
|
||
| // 注: このテストは実装修正後に、実際のフックをテストするように拡張する | ||
| // 現状はgetVoiceLanguageCode関数自体の動作を確認 | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
テストが期待する動作を検証していません
このテストは「RED フェーズ」を意図したものですが、実際には期待される動作の失敗を検証していません。現在のテストは getVoiceLanguageCode 関数が正しく動作することを確認しているだけで、フックが getVoiceLanguageCode を使用しているかどうかを検証していません。
TDD の RED フェーズを実装する場合、実際のフックの動作を検証し、現在の実装では失敗するテストを書く必要があります。
🔎 提案: 実際のフック動作を検証する失敗するテストに変更
フックをインポートして実際の動作をテストする例:
it('SpeechRecognitionの初期化時にselectLanguageに基づいた言語コードを設定する', () => {
// settings store の selectLanguage を 'en' に設定
const { result } = renderHook(() => useRealtimeVoiceAPI())
// フックが初期化したSpeechRecognitionインスタンスを取得
// (フックの実装によって方法は異なる)
// 期待: recognition.lang が 'en-US' になっている
expect(mockSpeechRecognition.lang).toBe('en-US')
// 現在の実装では 'ja-JP' がハードコードされているため、このテストは失敗する
})または、現時点では実装修正前のため、このテストブロックを削除し、実装修正後に適切なテストを追加することを検討してください。
🤖 Prompt for AI Agents
In src/__tests__/hooks/useRealtimeVoiceAPI.test.ts around lines 104 to 117, the
test only asserts getVoiceLanguageCode('en') returns 'en-US' and does not verify
that the hook actually uses getVoiceLanguageCode (so it won't fail while the
hook is still hardcoding 'ja-JP'); replace or update this test to exercise the
real hook: render the hook (e.g., with renderHook), set or mock the settings
store selectLanguage to 'en', trigger the hook initialization, then assert that
the created SpeechRecognition instance's lang equals getVoiceLanguageCode('en')
(expect 'en-US') so the test will fail until the hook is changed to use
getVoiceLanguageCode; alternatively remove this placeholder test and add the
proper hook-level test after implementation.
| beforeEach(() => { | ||
| settingsStore.setState({ | ||
| kioskModeEnabled: DEFAULT_KIOSK_CONFIG.kioskModeEnabled, | ||
| kioskPasscode: DEFAULT_KIOSK_CONFIG.kioskPasscode, | ||
| kioskGuidanceMessage: DEFAULT_KIOSK_CONFIG.kioskGuidanceMessage, | ||
| kioskGuidanceTimeout: DEFAULT_KIOSK_CONFIG.kioskGuidanceTimeout, | ||
| kioskMaxInputLength: DEFAULT_KIOSK_CONFIG.kioskMaxInputLength, | ||
| kioskNgWords: DEFAULT_KIOSK_CONFIG.kioskNgWords, | ||
| kioskNgWordEnabled: DEFAULT_KIOSK_CONFIG.kioskNgWordEnabled, | ||
| kioskTemporaryUnlock: DEFAULT_KIOSK_CONFIG.kioskTemporaryUnlock, | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the complete definition of DEFAULT_KIOSK_CONFIG
rg -A 15 "export const DEFAULT_KIOSK_CONFIG" --type=tsRepository: tegnike/aituber-kit
Length of output: 1080
DEFAULT_KIOSK_CONFIG に存在しないプロパティへのアクセス
beforeEach で kioskGuidanceMessage と kioskGuidanceTimeout を DEFAULT_KIOSK_CONFIG から参照していますが、これらのプロパティは DEFAULT_KIOSK_CONFIG に存在しません。実際の定義には、kioskModeEnabled、kioskPasscode、kioskMaxInputLength、kioskNgWords、kioskNgWordEnabled、kioskTemporaryUnlock のみ含まれています。このコードは実行時に undefined にアクセスするため、修正が必要です。
🤖 Prompt for AI Agents
In src/__tests__/integration/kioskModeIntegration.test.ts around lines 15–26 the
beforeEach references kioskGuidanceMessage and kioskGuidanceTimeout which do not
exist on DEFAULT_KIOSK_CONFIG; remove those two keys from the setState call (or
alternatively add those properties with appropriate default values to
DEFAULT_KIOSK_CONFIG where it’s defined) so the test no longer reads undefined
properties at runtime and only sets properties that actually exist.
| // fetchをモック | ||
| global.fetch = jest.fn() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
グローバルオブジェクトの復元処理を追加してください。
global.fetch を上書きしていますが、テスト終了後に元の値に戻す処理がありません。これは他のテストファイルに副作用を与える可能性があります。
afterAll() フック内で元の値に復元する処理を追加してください。
🔎 提案: グローバルオブジェクトの復元
+// fetchの元の実装を保存
+const originalFetch = global.fetch
+
// fetchをモック
global.fetch = jest.fn()
describe('usePresetLoader統合テスト', () => {
+ afterAll(() => {
+ // fetchを元の実装に戻す
+ global.fetch = originalFetch
+ })
+
beforeEach(() => {
jest.clearAllMocks()Based on learnings, グローバルオブジェクトを上書きする場合は、afterAll()フック内で元の値に戻して、テスト間の副作用を防ぐべきです。
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // fetchをモック | |
| global.fetch = jest.fn() | |
| // fetchの元の実装を保存 | |
| const originalFetch = global.fetch | |
| // fetchをモック | |
| global.fetch = jest.fn() | |
| describe('usePresetLoader統合テスト', () => { | |
| afterAll(() => { | |
| // fetchを元の実装に戻す | |
| global.fetch = originalFetch | |
| }) | |
| beforeEach(() => { | |
| jest.clearAllMocks() | |
| }) |
🤖 Prompt for AI Agents
In src/__tests__/integration/usePresetLoaderIntegration.test.ts around lines
16-17, the test overwrites global.fetch without restoring it, causing potential
cross-test side effects; save the original fetch before mocking (e.g. const
originalFetch = global.fetch), set global.fetch = jest.fn() for the test, and
add an afterAll() hook that restores the original with global.fetch =
originalFetch so other tests aren’t affected.
|
Claude finished @tegnike's task —— View job PR Review: Feature/Fix Realtime APIReviewed this large PR (100 files, 26K+ additions) focusing on code quality, bugs, performance, security, and test coverage. Review Tasks
Review Summary✅ Positive Aspects1. Comprehensive Test Coverage
2. Code Quality & Architecture
3. Security Implementation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
.claude/agents/test-runner.md.claude/commands/run-tests.mdsrc/__tests__/components/presenceDebugPreview.test.tsxsrc/__tests__/features/chat/difyChat.test.tssrc/__tests__/features/chat/vercelAIChat.test.tssrc/__tests__/features/kiosk/passcodeDialog.test.tsxsrc/__tests__/integration/voiceRecognitionFunctionality.test.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/__tests__/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
テストは
__tests__ディレクトリに配置
Files:
src/__tests__/features/chat/difyChat.test.tssrc/__tests__/features/kiosk/passcodeDialog.test.tsxsrc/__tests__/features/chat/vercelAIChat.test.tssrc/__tests__/components/presenceDebugPreview.test.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Jestのパターンマッチで特定テストを実行可能
Applied to files:
.claude/commands/run-tests.md
📚 Learning: 2025-05-21T14:11:19.604Z
Learnt from: RimgO
Repo: tegnike/aituber-kit PR: 395
File: src/components/messageInputContainer.tsx:46-60
Timestamp: 2025-05-21T14:11:19.604Z
Learning: In `src/components/messageInputContainer.tsx`, the `if (true) { //isNewUser` condition in the `onUserDetected` handler is intentionally left as is because this section is planned to be changed later.
Applied to files:
src/__tests__/components/presenceDebugPreview.test.tsx
🧬 Code graph analysis (2)
src/__tests__/features/kiosk/passcodeDialog.test.tsx (1)
src/features/kiosk/passcodeDialog.tsx (2)
PasscodeDialogProps(11-16)PasscodeDialog(21-195)
src/__tests__/components/presenceDebugPreview.test.tsx (1)
src/features/presence/presenceTypes.ts (1)
DetectionResult(43-47)
🪛 GitHub Actions: Lint and Format
src/__tests__/components/presenceDebugPreview.test.tsx
[error] 63-63: Replace container.querySelector('[data-testid="bounding-box"]') with ⏎········container.querySelector('[data-testid="bounding-box"]')⏎······ prettier/prettier
🪛 GitHub Actions: Run Tests
src/__tests__/components/presenceDebugPreview.test.tsx
[error] 63-63: Prettier formatting failed in test file. Replace the indicated formatting to satisfy prettier/prettier. Run 'prettier --write' to fix.
🪛 markdownlint-cli2 (0.18.1)
.claude/agents/test-runner.md
30-30: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
59-59: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
95-95: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
102-102: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: claude-review
🔇 Additional comments (16)
.claude/agents/test-runner.md (1)
1-122: ドキュメント構造は良好ですテスト実行エージェントの仕様が明確に定義されており、責務・実行フロー・報告形式が体系的にまとめられています。レポート出力ルールの詳細さと、メインエージェントへの報告形式の二層構造は実装時に有用です。上記のコードブロック言語指定を修正すれば、ドキュメントとして完成度が高いです。
.claude/commands/run-tests.md (1)
1-41: テスト実行ワークフローの実装が適切ですワークフロー設計が明確で、以下の点で優れています:
- サブエージェント連携: test-runner サブエージェントへの委譲が適切に定義されている
- 条件分岐: 成功時と失敗時の処理が明確に分離されている
- 安全ガードレール: 3回までの修正試行、マスター相談への段階的エスカレーション
- 柔軟性: $ARGUMENTS プレースホルダーにより、テスト対象の指定が可能
ワークフロー全体として、テスト駆動の自動修正プロセスが適切に実装されています。
src/__tests__/components/presenceDebugPreview.test.tsx (5)
1-48: セットアップとモックの実装が適切です。テストのセットアップが正しく実装されています。ビデオ要素のモック化、ストアとi18nのモック、beforeEachでのクリーンアップが適切に行われています。
51-64: 過去のレビューコメントとテスト期待値の不整合を確認してください。このテストは、デバッグモードが無効な場合でもビデオ要素がレンダリングされることを期待していますが、過去のレビューコメントでは、
presenceDebugModeがfalseのときにコンポーネントがnullを返すべきだと指摘されています。行60のコメント「Video element is always rendered for camera preview」は、ビデオ要素を常にレンダリングする設計意図を示していますが、これは過去のレビューコメントと矛盾しています。
コンポーネントの実装意図を明確にし、以下のいずれかを選択してください:
- デバッグモード無効時に
nullを返す(過去のレビューに従う)- ビデオは常にレンダリングし、オーバーレイのみを条件付きで表示する(このテストの期待値に従う)
79-94: LGTM!ビデオ要素のレンダリングテストが適切に実装されています。
96-164: バウンディングボックステストのロジックが正確です。バウンディングボックスのテストが包括的に実装されています:
- 顔が検出されない場合のテスト
- 顔が検出された場合のテスト
- ミラー計算を含む位置とサイズの検証(行157-163)
ミラー計算の式
videoWidth(640) - x(10) - width(100) = 530が正しく適用されています。
167-183: LGTM!カスタムclassNameプロップのテストが適切に実装されています。
src/__tests__/features/chat/difyChat.test.ts (2)
285-321: APIエラーハンドリングのテスト期待値が正しく更新されていますLine 317の期待値が実装の変更を正しく反映しています。翻訳キーとAPIエラー詳細を連結する形式になっており、他のテストケースとも一貫性があります。
224-263: テスト期待値と実装が一致していることを確認しました実装側(
src/features/chat/difyChat.ts)のエラーハンドリングを確認した結果、テストの期待値は実装と正確に一致しています。
- 行95-104: ストリームリード時のエラーを適切にキャッチ
- 行98-99: エラーメッセージを
handleApiError('AIAPIError', errorDetail)で構築handleApiError関数(行6-16)が${translatedKey}: ${errorDetail}形式でメッセージを生成テストは正しく実装の動作を検証しており、コーディングガイドライン(
__tests__ディレクトリ配置)にも準拠しています。ただし、エラー詳細("Stream error")が英語のままで翻訳されない点は、将来的な多言語対応強化時に考慮する価値があります。src/__tests__/features/chat/vercelAIChat.test.ts (3)
107-123: 非ストリーミングエラーレスポンスのテスト期待値を確認しましたLine 122の更新により、非ストリーミングAPIのエラーレスポンスでも詳細なエラーメッセージが返されることを検証しています。ストリーミングケースと同じフォーマット(
翻訳キー: エラー詳細)を使用しており、一貫性が保たれています。
198-236: ストリーミングエラーハンドリングのテスト更新を確認しましたLine 232の期待値が、difyChat.test.tsと同じパターンでエラー詳細を含むようになっています。両ファイル間で一貫したエラーメッセージフォーマットが維持されており、適切です。
258-283: APIエラーレスポンスのテスト期待値が適切に更新されていますLine 279の更新により、difyChat.test.tsと同じエラーメッセージフォーマットを使用していることが確認できます。両チャット実装で統一されたエラーハンドリングが行われており、コードベース全体で一貫性が保たれています。
src/__tests__/features/kiosk/passcodeDialog.test.tsx (4)
1-19: テストファイルの構造とセットアップが適切ですファイルヘッダー、インポート、およびヘルパー関数が適切に設定されています。要件番号が明示されており、保守性が高いです。
38-294: 包括的なテストカバレッジと優れた構造テストスイートは要件に明確にマッピングされており、以下を網羅しています:
- 要件3.1: UI要素のレンダリング
- 要件3.2: パスコード検証ロジック
- 要件3.3: ロックアウトメカニクス
フェイクタイマーの使用が適切で、30秒のロックアウト期間やカウントダウンの動作を正確にテストしています。エッジケース(入力クリア、試行回数リセット、ボタン無効化など)も適切にカバーされています。
306-318: Escapeキーテストが正しく修正されました過去のレビューコメントで指摘された500msの遅延が適切に考慮されています。
jest.advanceTimersByTime(500)を使用してタイマーを進めてからキーイベントを発火させることで、コンポーネントの実装と一致した正確なテストになっています。
296-337: アクセシビリティとUXテストが充実しています以下の重要なアクセシビリティ要件が適切にテストされています:
- フォーカス管理(非同期動作を考慮したwaitFor使用)
- キーボードインタラクション(Escape、Enter)
- セキュリティ配慮(パスワード入力のマスキング)
テストの実装方法も適切で、非同期動作やキーボードイベントのベストプラクティスに従っています。
| ``` | ||
| reports/ | ||
| └── tests/ | ||
| └── YYYY-MM-DD_HH-mm-ss_test-report.md | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
フォルダ構造コードブロックに言語指定を追加してください
コードブロックの言語指定がありません。フォルダ構造を示すため text または plaintext を指定してください。
🔎 推奨される修正
-```
+```text
reports/
└── tests/
└── YYYY-MM-DD_HH-mm-ss_test-report.md
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| reports/ | |
| └── tests/ | |
| └── YYYY-MM-DD_HH-mm-ss_test-report.md | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
30-30: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
.claude/agents/test-runner.md around lines 30 to 34: the fenced code block
showing the folder structure lacks a language specifier; update the opening
fence to include a language such as "text" or "plaintext" (e.g., change ``` to
```text) so the block is explicitly marked as plain text.
| **エラー内容**: | ||
| ``` | ||
| [エラーメッセージ] | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
エラーメッセージを表示するコードブロックに言語指定を追加してください
マークダウンリントの規則に従い、コードブロックには言語を指定する必要があります。ここは通常のテキスト出力として text を指定してください。
🔎 推奨される修正
**エラー内容**:
-```
+```text
[エラーメッセージ]
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
59-59: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In .claude/agents/test-runner.md around lines 56 to 59, the markdown code block
showing the error message lacks a language tag; add the language specifier
"text" to the opening fence so the block becomes ```text [エラーメッセージ] ``` (i.e.,
change the opening ``` to ```text and keep the closing ```), ensuring the code
block complies with the markdown lint rule.
| ``` | ||
| テスト完了: 全 [X] テストが成功しました。 | ||
| レポート: reports/tests/[ファイル名].md | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
全テスト成功時の報告形式コードブロックに言語指定を追加してください
コードブロックに言語指定がありません。通常のテキスト形式として text を指定してください。
🔎 推奨される修正
### 全テスト成功の場合
-```
+```text
テスト完了: 全 [X] テストが成功しました。
レポート: reports/tests/[ファイル名].md
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
95-95: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
.claude/agents/test-runner.md around lines 95 to 98: the fenced code block
showing the test completion message lacks a language specifier; add "text" after
the opening triple backticks so it becomes ```text, leaving the block contents
unchanged and keeping the closing triple backticks as-is.
| ``` | ||
| テスト完了: [X] 件の失敗があります。 | ||
|
|
||
| 失敗したテスト: | ||
| 1. [ファイル名] - [テスト名] | ||
| 原因: [簡潔な原因説明] | ||
| 修正対象: [ファイルパス:行番号] | ||
|
|
||
| 詳細レポート: reports/tests/[ファイル名].md | ||
|
|
||
| 修正が必要なファイル: | ||
| - [ファイルパス1] | ||
| - [ファイルパス2] | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
失敗時の報告形式コードブロックに言語指定を追加してください
こちらのコードブロックも言語指定がありません。テキスト形式として text を指定してください。
🔎 推奨される修正
### 失敗がある場合
-```
+```text
テスト完了: [X] 件の失敗があります。
失敗したテスト:
@@ -106,7 +106,7 @@
修正が必要なファイル:
- [ファイルパス1]
- [ファイルパス2]
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| テスト完了: [X] 件の失敗があります。 | |
| 失敗したテスト: | |
| 1. [ファイル名] - [テスト名] | |
| 原因: [簡潔な原因説明] | |
| 修正対象: [ファイルパス:行番号] | |
| 詳細レポート: reports/tests/[ファイル名].md | |
| 修正が必要なファイル: | |
| - [ファイルパス1] | |
| - [ファイルパス2] | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
102-102: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In .claude/agents/test-runner.md around lines 102–115 the failure-report code
block is missing a language specifier; change the opening fence from ``` to
```text so the block is explicitly marked as plain text (leave the content and
closing fence unchanged).
| // Video element is always rendered for camera preview | ||
| expect(container.querySelector('video')).toBeInTheDocument() | ||
| // But debug overlay should not be rendered | ||
| expect(container.querySelector('[data-testid="bounding-box"]')).not.toBeInTheDocument() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prettierフォーマットエラーを修正してください。
パイプラインでPrettierフォーマットエラーが発生しています。
🔎 修正案
- expect(container.querySelector('[data-testid="bounding-box"]')).not.toBeInTheDocument()
+ expect(
+ container.querySelector('[data-testid="bounding-box"]')
+ ).not.toBeInTheDocument()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| expect(container.querySelector('[data-testid="bounding-box"]')).not.toBeInTheDocument() | |
| expect( | |
| container.querySelector('[data-testid="bounding-box"]') | |
| ).not.toBeInTheDocument() |
🧰 Tools
🪛 GitHub Actions: Lint and Format
[error] 63-63: Replace container.querySelector('[data-testid="bounding-box"]') with ⏎········container.querySelector('[data-testid="bounding-box"]')⏎······ prettier/prettier
🪛 GitHub Actions: Run Tests
[error] 63-63: Prettier formatting failed in test file. Replace the indicated formatting to satisfy prettier/prettier. Run 'prettier --write' to fix.
🤖 Prompt for AI Agents
In src/__tests__/components/presenceDebugPreview.test.tsx around line 63,
Prettier is flagging the expect line as misformatted; fix it by reformatting the
file with the project's Prettier settings (e.g., run npm run format or npx
prettier --write src/__tests__/components/presenceDebugPreview.test.tsx) or
manually adjust the line to match style (ensure correct spacing, no trailing
whitespace, proper indentation and punctuation such as adding/removing a
trailing semicolon per project config).
| jest.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string) => { | ||
| const translations: Record<string, string> = { | ||
| 'Kiosk.PasscodeTitle': 'パスコードを入力', | ||
| 'Kiosk.PasscodeIncorrect': 'パスコードが違います', | ||
| 'Kiosk.PasscodeLocked': 'ロック中', | ||
| 'Kiosk.PasscodeRemainingAttempts': '残り{{count}}回', | ||
| 'Kiosk.Cancel': 'キャンセル', | ||
| 'Kiosk.Unlock': '解除', | ||
| } | ||
| return translations[key] || key | ||
| }, | ||
| }), | ||
| })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i18nモックが補間パラメータを処理していません
モックは翻訳キーのみを処理し、補間パラメータ(例:{ count: 2 })を無視しています。'Kiosk.PasscodeRemainingAttempts'は'残り{{count}}回'を返しますが、166行目と182行目のテストは/残り2回/や/残り1回/を期待しています。これによりテストが失敗する可能性があります。
🔎 補間を処理するモックの修正案
jest.mock('react-i18next', () => ({
useTranslation: () => ({
- t: (key: string) => {
+ t: (key: string, params?: Record<string, any>) => {
const translations: Record<string, string> = {
'Kiosk.PasscodeTitle': 'パスコードを入力',
'Kiosk.PasscodeIncorrect': 'パスコードが違います',
'Kiosk.PasscodeLocked': 'ロック中',
'Kiosk.PasscodeRemainingAttempts': '残り{{count}}回',
'Kiosk.Cancel': 'キャンセル',
'Kiosk.Unlock': '解除',
}
- return translations[key] || key
+ let result = translations[key] || key
+
+ // Handle interpolation
+ if (params) {
+ Object.keys(params).forEach((param) => {
+ result = result.replace(`{{${param}}}`, String(params[param]))
+ })
+ }
+
+ return result
},
}),
}))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| jest.mock('react-i18next', () => ({ | |
| useTranslation: () => ({ | |
| t: (key: string) => { | |
| const translations: Record<string, string> = { | |
| 'Kiosk.PasscodeTitle': 'パスコードを入力', | |
| 'Kiosk.PasscodeIncorrect': 'パスコードが違います', | |
| 'Kiosk.PasscodeLocked': 'ロック中', | |
| 'Kiosk.PasscodeRemainingAttempts': '残り{{count}}回', | |
| 'Kiosk.Cancel': 'キャンセル', | |
| 'Kiosk.Unlock': '解除', | |
| } | |
| return translations[key] || key | |
| }, | |
| }), | |
| })) | |
| jest.mock('react-i18next', () => ({ | |
| useTranslation: () => ({ | |
| t: (key: string, params?: Record<string, any>) => { | |
| const translations: Record<string, string> = { | |
| 'Kiosk.PasscodeTitle': 'パスコードを入力', | |
| 'Kiosk.PasscodeIncorrect': 'パスコードが違います', | |
| 'Kiosk.PasscodeLocked': 'ロック中', | |
| 'Kiosk.PasscodeRemainingAttempts': '残り{{count}}回', | |
| 'Kiosk.Cancel': 'キャンセル', | |
| 'Kiosk.Unlock': '解除', | |
| } | |
| let result = translations[key] || key | |
| // Handle interpolation | |
| if (params) { | |
| Object.keys(params).forEach((param) => { | |
| result = result.replace(`{{${param}}}`, String(params[param])) | |
| }) | |
| } | |
| return result | |
| }, | |
| }), | |
| })) |
🤖 Prompt for AI Agents
In src/__tests__/features/kiosk/passcodeDialog.test.tsx around lines 22 to 36,
the current i18n mock ignores interpolation params so keys like
'Kiosk.PasscodeRemainingAttempts' return '残り{{count}}回' even when tests expect
'残り2回' or '残り1回'; update the mock's useTranslation.t to accept an optional
options/params argument (e.g., second arg or object) and replace occurrences of
{{count}} (and any simple {{key}}) with the provided value(s) (handle numeric
count and string keys at minimum) so tests that assert against interpolated text
pass.
|
Claude finished @tegnike's task —— View job PR Review: Feature/Fix Realtime APIReviewed this large PR (100 files, 26K+ additions) focusing on code quality, bugs, performance, security, and test coverage. Review Tasks
Review Summary✅ Positive Aspects1. Comprehensive Test Coverage
2. Code Quality & Architecture
3. Security Implementation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/__tests__/components/settings/kioskSettings.test.tsx (2)
17-32: ストアのモック実装が機能しているが、より明確にできます
Object.assign(jest.fn(), { setState, getState })パターンは動作しますが、やや複雑です。以下のような標準的なモックパターンの方が読みやすくなります。🔎 より明確なモック実装の提案
-jest.mock('@/features/stores/settings', () => { - return { - __esModule: true, - default: Object.assign(jest.fn(), { - setState: (arg: any) => mockSetState(arg), - getState: () => ({ - kioskModeEnabled: false, - kioskPasscode: '0000', - kioskMaxInputLength: 200, - kioskNgWords: [], - kioskNgWordEnabled: false, - kioskTemporaryUnlock: false, - }), - }), - } -}) +jest.mock('@/features/stores/settings', () => ({ + __esModule: true, + default: jest.fn((selector) => { + const state = { + kioskModeEnabled: false, + kioskPasscode: '0000', + kioskMaxInputLength: 200, + kioskNgWords: [], + kioskNgWordEnabled: false, + kioskTemporaryUnlock: false, + } + return selector ? selector(state) : state + }), + setState: mockSetState, +}))
85-90: 配列インデックスに依存したセレクタはUI変更に脆弱です
toggleButtons[0]を使用していますが、UI上のボタン順序が変わるとテストが壊れる可能性があります。より具体的なセレクタ(例:getByRolewith name option)を使用すると、テストの堅牢性が向上します。
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
public/images/setting-icons/idle-settings.svgis excluded by!**/*.svgpublic/images/setting-icons/kiosk-settings.svgis excluded by!**/*.svgpublic/images/setting-icons/memory-settings.svgis excluded by!**/*.svgpublic/images/setting-icons/presence-settings.svgis excluded by!**/*.svg
📒 Files selected for processing (5)
locales/ja/translation.jsonsrc/__tests__/components/settings/kioskSettings.test.tsxsrc/components/menu.tsxsrc/components/settings/index.tsxsrc/components/settings/kioskSettings.tsx
🧰 Additional context used
📓 Path-based instructions (2)
locales/ja/**/*.json
📄 CodeRabbit inference engine (CLAUDE.md)
言語ファイルの更新は日本語(
/locales/ja/)のみ行う
Files:
locales/ja/translation.json
**/__tests__/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
テストは
__tests__ディレクトリに配置
Files:
src/__tests__/components/settings/kioskSettings.test.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Applies to locales/!(ja)/**/*.json : 他の言語ファイル(en、ko、zh等)は手動で更新しない
Applied to files:
locales/ja/translation.json
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Applies to locales/ja/**/*.json : 言語ファイルの更新は日本語(`/locales/ja/`)のみ行う
Applied to files:
locales/ja/translation.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: claude-review
🔇 Additional comments (4)
locales/ja/translation.json (1)
442-553: 日本語ロケールファイルへの大規模な追加を確認しましたメモリ、人感検知、アイドルモード、デモ端末モードなど、新機能のUI文字列を包括的にカバーする翻訳キーが追加されています。コーディングガイドラインに従い、日本語ロケールファイルのみが更新されている点が適切です。
追加されたキーの構造とネストも既存のパターンと一致しており、問題ありません。
Based on coding guidelines, 言語ファイルの更新は日本語(
/locales/ja/)のみ行う というルールに準拠しています。src/__tests__/components/settings/kioskSettings.test.tsx (3)
113-153: 入力文字数制限のバリデーションテストが適切に実装されています最小値(50文字)と最大値(500文字)のクランプ処理を、blur イベントを通じて正しくテストしています。境界値テストが含まれており、バリデーションロジックの品質を保証しています。
155-216: NGワードフィルターのテストカバレッジが充実していますトグル、条件付きレンダリング、入力処理、既存ワードの表示など、NGワードフィルター機能の主要な動作を網羅的にテストしています。
lines 198-201の
find()を使った検証は動作しますが、より直接的なアサーション方法も検討できます。
1-224: テストファイル全体の構造が適切です
__tests__ディレクトリへの配置はコーディングガイドラインに従っています。テストは適切に分離され(beforeEach)、要件ごとに整理されており、保守性が高い構造になっています。Based on coding guidelines, テストは
__tests__ディレクトリに配置 というルールに準拠しています。
…onents for consistency
Summary by CodeRabbit
新機能
ドキュメント
テスト
その他
✏️ Tip: You can customize this high-level summary in your review settings.