/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { act, renderHook } from '@testing-library/react'; import { vi, describe, it, expect, beforeEach } from 'vitest'; import { useInputHistoryStore } from './useInputHistoryStore.js'; describe('useInputHistoryStore', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should initialize with empty input history', () => { const { result } = renderHook(() => useInputHistoryStore()); expect(result.current.inputHistory).toEqual([]); }); it('should add input to history', () => { const { result } = renderHook(() => useInputHistoryStore()); act(() => { result.current.addInput('test message 1'); }); expect(result.current.inputHistory).toEqual(['test message 1']); act(() => { result.current.addInput('test message 2'); }); expect(result.current.inputHistory).toEqual([ 'test message 1', 'test message 2', ]); }); it('should not add empty or whitespace-only inputs', () => { const { result } = renderHook(() => useInputHistoryStore()); act(() => { result.current.addInput(''); }); expect(result.current.inputHistory).toEqual([]); act(() => { result.current.addInput(' '); }); expect(result.current.inputHistory).toEqual([]); }); it('should deduplicate consecutive identical messages', () => { const { result } = renderHook(() => useInputHistoryStore()); act(() => { result.current.addInput('test message'); }); act(() => { result.current.addInput('test message'); // Same as previous }); expect(result.current.inputHistory).toEqual(['test message']); act(() => { result.current.addInput('different message'); }); act(() => { result.current.addInput('test message'); // Same as first, but not consecutive }); expect(result.current.inputHistory).toEqual([ 'test message', 'different message', 'test message', ]); }); it('should initialize from logger successfully', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockResolvedValue(['newest', 'middle', 'oldest']), }; const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Should reverse the order to oldest first expect(result.current.inputHistory).toEqual(['oldest', 'middle', 'newest']); expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1); }); it('should handle logger initialization failure gracefully', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockRejectedValue(new Error('Logger error')), }; const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); expect(result.current.inputHistory).toEqual([]); expect(consoleSpy).toHaveBeenCalledWith( 'Failed to initialize input history from logger:', expect.any(Error), ); consoleSpy.mockRestore(); }); it('should initialize only once', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockResolvedValue(['message1', 'message2']), }; const { result } = renderHook(() => useInputHistoryStore()); // Call initializeFromLogger twice await act(async () => { await result.current.initializeFromLogger(mockLogger); }); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Should be called only once expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1); expect(result.current.inputHistory).toEqual(['message2', 'message1']); }); it('should handle null logger gracefully', async () => { const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(null); }); expect(result.current.inputHistory).toEqual([]); }); it('should trim input before adding to history', () => { const { result } = renderHook(() => useInputHistoryStore()); act(() => { result.current.addInput(' test message '); }); expect(result.current.inputHistory).toEqual(['test message']); }); describe('deduplication logic from previous implementation', () => { it('should deduplicate consecutive messages from past sessions during initialization', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockResolvedValue([ 'message1', 'message1', 'message2', 'message2', 'message3', ]), // newest first with duplicates }; const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Should deduplicate consecutive messages and reverse to oldest first expect(result.current.inputHistory).toEqual([ 'message3', 'message2', 'message1', ]); }); it('should deduplicate across session boundaries', async () => { const mockLogger = { getPreviousUserMessages: vi.fn().mockResolvedValue(['old2', 'old1']), // newest first }; const { result } = renderHook(() => useInputHistoryStore()); // Initialize with past session await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Add current session inputs act(() => { result.current.addInput('old2'); // Same as last past session message }); // Should deduplicate across session boundary expect(result.current.inputHistory).toEqual(['old1', 'old2']); act(() => { result.current.addInput('new1'); }); expect(result.current.inputHistory).toEqual(['old1', 'old2', 'new1']); }); it('should preserve non-consecutive duplicates', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockResolvedValue(['message2', 'message1', 'message2']), // newest first with non-consecutive duplicate }; const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Non-consecutive duplicates should be preserved expect(result.current.inputHistory).toEqual([ 'message2', 'message1', 'message2', ]); }); it('should handle complex deduplication with current session', () => { const { result } = renderHook(() => useInputHistoryStore()); // Add multiple messages with duplicates act(() => { result.current.addInput('hello'); }); act(() => { result.current.addInput('hello'); // consecutive duplicate }); act(() => { result.current.addInput('world'); }); act(() => { result.current.addInput('world'); // consecutive duplicate }); act(() => { result.current.addInput('hello'); // non-consecutive duplicate }); // Should have deduplicated consecutive ones expect(result.current.inputHistory).toEqual(['hello', 'world', 'hello']); }); it('should maintain oldest-first order in final output', async () => { const mockLogger = { getPreviousUserMessages: vi .fn() .mockResolvedValue(['newest', 'middle', 'oldest']), // newest first }; const { result } = renderHook(() => useInputHistoryStore()); await act(async () => { await result.current.initializeFromLogger(mockLogger); }); // Add current session messages act(() => { result.current.addInput('current1'); }); act(() => { result.current.addInput('current2'); }); // Should maintain oldest-first order expect(result.current.inputHistory).toEqual([ 'oldest', 'middle', 'newest', 'current1', 'current2', ]); }); }); });