fix: embed conversation history in AI agent chat messages
The AI chat panel (AiPanel → useAiAgent) was sending each message as a standalone request with no prior context. Root cause: useAiAgent.sendMessage called streamClaudeAgent with raw text, never embedding history. - Add agentMessagesToChatHistory() to convert AiAgentMessage[] to ChatMessage[] - Embed trimmed history in each agent request via formatMessageWithHistory - Use messagesRef/statusRef to avoid stale closures in async callbacks - Also fix useAIChat (dead code path) with same ref pattern - Update mock layers to detect history presence for testability - Add Playwright smoke tests verifying history accumulates and resets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,4 +165,28 @@ describe('useAIChat', () => {
|
||||
expect(lastCall[0]).toBe('hello')
|
||||
expect(lastCall[2]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reads latest messages from ref (not stale closure)', async () => {
|
||||
const { result } = renderHook(() => useAIChat([]))
|
||||
|
||||
// Send first message
|
||||
act(() => { result.current.sendMessage('msg1') })
|
||||
await act(async () => { vi.advanceTimersByTime(50) })
|
||||
|
||||
// Verify messages state is correct
|
||||
expect(result.current.messages).toHaveLength(2)
|
||||
expect(result.current.messages[0].content).toBe('msg1')
|
||||
expect(result.current.messages[1].content).toBe('mock response')
|
||||
|
||||
// Send second message — the ref should have the latest messages
|
||||
// even without depending on messages in useCallback deps
|
||||
act(() => { result.current.sendMessage('msg2') })
|
||||
|
||||
const secondCall = streamClaudeChatMock.mock.calls[1]
|
||||
const sentMessage = secondCall[0]
|
||||
// Must contain history from first exchange
|
||||
expect(sentMessage).toContain('[user]: msg1')
|
||||
expect(sentMessage).toContain('[assistant]: mock response')
|
||||
expect(sentMessage).toContain('[user]: msg2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
* Conversation continuity embeds prior exchanges in each prompt
|
||||
* (each CLI invocation is a fresh subprocess with no memory).
|
||||
* History is trimmed to MAX_HISTORY_TOKENS, dropping oldest first.
|
||||
*
|
||||
* Uses a ref (messagesRef) to read the latest messages in callbacks,
|
||||
* avoiding stale closure issues with React's useCallback memoization.
|
||||
*/
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import {
|
||||
type ChatMessage, type ChatStreamCallbacks, nextMessageId,
|
||||
@@ -57,10 +60,18 @@ export function useAIChat(
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [streamingContent, setStreamingContent] = useState('')
|
||||
const abortRef = useRef(false)
|
||||
const isStreamingRef = useRef(false)
|
||||
const messagesRef = useRef<ChatMessage[]>([])
|
||||
|
||||
/** Internal: send text with explicit history context. */
|
||||
const doSend = useCallback((text: string, history: ChatMessage[]) => {
|
||||
if (!text.trim() || isStreaming) return
|
||||
// Keep refs in sync with state — runs after render, before next user interaction.
|
||||
useEffect(() => { messagesRef.current = messages }, [messages])
|
||||
useEffect(() => { isStreamingRef.current = isStreaming }, [isStreaming])
|
||||
|
||||
/** Internal: send text, reading history from the messages ref. */
|
||||
const doSend = useCallback((text: string, historyOverride?: ChatMessage[]) => {
|
||||
if (!text.trim() || isStreamingRef.current) return
|
||||
|
||||
const history = historyOverride ?? messagesRef.current
|
||||
|
||||
setMessages(prev => [...prev, { role: 'user', content: text.trim(), id: nextMessageId() }])
|
||||
setIsStreaming(true)
|
||||
@@ -80,11 +91,11 @@ export function useAIChat(
|
||||
|
||||
streamClaudeChat(formattedMessage, systemPrompt, undefined, callbacks)
|
||||
.catch(() => { /* errors forwarded via onError */ })
|
||||
}, [isStreaming, contextNotes])
|
||||
}, [contextNotes])
|
||||
|
||||
const sendMessage = useCallback((text: string) => {
|
||||
doSend(text, messages)
|
||||
}, [doSend, messages])
|
||||
doSend(text)
|
||||
}, [doSend])
|
||||
|
||||
const clearConversation = useCallback(() => {
|
||||
abortRef.current = true
|
||||
@@ -94,15 +105,16 @@ export function useAIChat(
|
||||
}, [])
|
||||
|
||||
const retryMessage = useCallback((msgIndex: number) => {
|
||||
const currentMessages = messagesRef.current
|
||||
const userMsgIndex = msgIndex - 1
|
||||
if (userMsgIndex < 0) return
|
||||
const userMsg = messages[userMsgIndex]
|
||||
const userMsg = currentMessages[userMsgIndex]
|
||||
if (userMsg.role !== 'user') return
|
||||
|
||||
const historyForRetry = messages.slice(0, userMsgIndex)
|
||||
const historyForRetry = currentMessages.slice(0, userMsgIndex)
|
||||
setMessages(prev => prev.slice(0, userMsgIndex))
|
||||
doSend(userMsg.content, historyForRetry)
|
||||
}, [messages, doSend])
|
||||
}, [doSend])
|
||||
|
||||
return { messages, isStreaming, streamingContent, sendMessage, clearConversation, retryMessage }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { detectFileOperation, parseBashFileCreation, useAiAgent } from './useAiAgent'
|
||||
import type { AgentFileCallbacks } from './useAiAgent'
|
||||
import { detectFileOperation, parseBashFileCreation, agentMessagesToChatHistory, useAiAgent } from './useAiAgent'
|
||||
import type { AgentFileCallbacks, AiAgentMessage } from './useAiAgent'
|
||||
import { streamClaudeAgent } from '../utils/ai-agent'
|
||||
|
||||
vi.mock('../utils/ai-agent', () => ({
|
||||
@@ -237,4 +237,130 @@ describe('useAiAgent', () => {
|
||||
expect(mockStreamClaudeAgent).toHaveBeenCalledTimes(1)
|
||||
expect(mockStreamClaudeAgent.mock.calls[0][1]).toBe('default-system-prompt')
|
||||
})
|
||||
|
||||
it('sends first message without conversation history', async () => {
|
||||
const { result } = renderHook(() => useAiAgent(VAULT, undefined))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage('Hello')
|
||||
})
|
||||
|
||||
expect(mockStreamClaudeAgent).toHaveBeenCalledTimes(1)
|
||||
const sentMessage = mockStreamClaudeAgent.mock.calls[0][0]
|
||||
expect(sentMessage).toBe('Hello')
|
||||
expect(sentMessage).not.toContain('<conversation_history>')
|
||||
})
|
||||
|
||||
it('embeds conversation history in second message', async () => {
|
||||
// Mock that simulates a response for the first message
|
||||
mockStreamClaudeAgent.mockImplementation(async (_msg, _sys, _vault, callbacks) => {
|
||||
callbacks.onText('The answer is 4')
|
||||
callbacks.onDone()
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useAiAgent(VAULT, undefined))
|
||||
|
||||
// First message
|
||||
await act(async () => {
|
||||
await result.current.sendMessage('What is 2+2?')
|
||||
})
|
||||
|
||||
// Second message — should include history from first exchange
|
||||
await act(async () => {
|
||||
await result.current.sendMessage('What was my previous question?')
|
||||
})
|
||||
|
||||
expect(mockStreamClaudeAgent).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First call: no history
|
||||
expect(mockStreamClaudeAgent.mock.calls[0][0]).toBe('What is 2+2?')
|
||||
|
||||
// Second call: includes history
|
||||
const secondMsg = mockStreamClaudeAgent.mock.calls[1][0]
|
||||
expect(secondMsg).toContain('<conversation_history>')
|
||||
expect(secondMsg).toContain('What is 2+2?')
|
||||
expect(secondMsg).toContain('The answer is 4')
|
||||
expect(secondMsg).toContain('What was my previous question?')
|
||||
})
|
||||
|
||||
it('accumulates history across multiple exchanges', async () => {
|
||||
let callCount = 0
|
||||
mockStreamClaudeAgent.mockImplementation(async (_msg, _sys, _vault, callbacks) => {
|
||||
callCount++
|
||||
callbacks.onText(`Response ${callCount}`)
|
||||
callbacks.onDone()
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useAiAgent(VAULT, undefined))
|
||||
|
||||
await act(async () => { await result.current.sendMessage('Q1') })
|
||||
await act(async () => { await result.current.sendMessage('Q2') })
|
||||
await act(async () => { await result.current.sendMessage('Q3') })
|
||||
|
||||
expect(mockStreamClaudeAgent).toHaveBeenCalledTimes(3)
|
||||
|
||||
// First: no history
|
||||
expect(mockStreamClaudeAgent.mock.calls[0][0]).toBe('Q1')
|
||||
|
||||
// Second: history from first exchange
|
||||
const msg2 = mockStreamClaudeAgent.mock.calls[1][0]
|
||||
expect(msg2).toContain('Q1')
|
||||
expect(msg2).toContain('Response 1')
|
||||
expect(msg2).toContain('Q2')
|
||||
|
||||
// Third: history from both exchanges
|
||||
const msg3 = mockStreamClaudeAgent.mock.calls[2][0]
|
||||
expect(msg3).toContain('Q1')
|
||||
expect(msg3).toContain('Response 1')
|
||||
expect(msg3).toContain('Q2')
|
||||
expect(msg3).toContain('Response 2')
|
||||
expect(msg3).toContain('Q3')
|
||||
})
|
||||
|
||||
it('resets history after clearConversation', async () => {
|
||||
mockStreamClaudeAgent.mockImplementation(async (_msg, _sys, _vault, callbacks) => {
|
||||
callbacks.onText('reply')
|
||||
callbacks.onDone()
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useAiAgent(VAULT, undefined))
|
||||
|
||||
await act(async () => { await result.current.sendMessage('hello') })
|
||||
|
||||
act(() => { result.current.clearConversation() })
|
||||
|
||||
await act(async () => { await result.current.sendMessage('fresh start') })
|
||||
|
||||
const lastCall = mockStreamClaudeAgent.mock.calls[mockStreamClaudeAgent.mock.calls.length - 1]
|
||||
expect(lastCall[0]).toBe('fresh start')
|
||||
expect(lastCall[0]).not.toContain('<conversation_history>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('agentMessagesToChatHistory', () => {
|
||||
it('returns empty array for no messages', () => {
|
||||
expect(agentMessagesToChatHistory([])).toEqual([])
|
||||
})
|
||||
|
||||
it('converts agent messages to user/assistant pairs', () => {
|
||||
const msgs: AiAgentMessage[] = [
|
||||
{ userMessage: 'Q1', response: 'A1', actions: [], id: 'm1' },
|
||||
{ userMessage: 'Q2', response: 'A2', actions: [], id: 'm2' },
|
||||
]
|
||||
const result = agentMessagesToChatHistory(msgs)
|
||||
expect(result).toHaveLength(4)
|
||||
expect(result[0]).toMatchObject({ role: 'user', content: 'Q1' })
|
||||
expect(result[1]).toMatchObject({ role: 'assistant', content: 'A1' })
|
||||
expect(result[2]).toMatchObject({ role: 'user', content: 'Q2' })
|
||||
expect(result[3]).toMatchObject({ role: 'assistant', content: 'A2' })
|
||||
})
|
||||
|
||||
it('skips assistant entry when response is undefined (still streaming)', () => {
|
||||
const msgs: AiAgentMessage[] = [
|
||||
{ userMessage: 'Q1', actions: [], id: 'm1', isStreaming: true },
|
||||
]
|
||||
const result = agentMessagesToChatHistory(msgs)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toMatchObject({ role: 'user', content: 'Q1' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,10 @@ import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import type { AiAction } from '../components/AiMessage'
|
||||
import type { NoteReference } from '../utils/ai-context'
|
||||
import { streamClaudeAgent, buildAgentSystemPrompt } from '../utils/ai-agent'
|
||||
import { nextMessageId } from '../utils/ai-chat'
|
||||
import {
|
||||
nextMessageId, trimHistory, formatMessageWithHistory,
|
||||
type ChatMessage, MAX_HISTORY_TOKENS,
|
||||
} from '../utils/ai-chat'
|
||||
|
||||
export type AgentStatus = 'idle' | 'thinking' | 'tool-executing' | 'done' | 'error'
|
||||
|
||||
@@ -36,6 +39,18 @@ export interface AgentFileCallbacks {
|
||||
onVaultChanged?: () => void
|
||||
}
|
||||
|
||||
/** Convert completed agent messages to ChatMessage pairs for history embedding. */
|
||||
export function agentMessagesToChatHistory(msgs: AiAgentMessage[]): ChatMessage[] {
|
||||
const history: ChatMessage[] = []
|
||||
for (const msg of msgs) {
|
||||
history.push({ role: 'user', content: msg.userMessage, id: msg.id ?? '' })
|
||||
if (msg.response) {
|
||||
history.push({ role: 'assistant', content: msg.response, id: `${msg.id}-resp` })
|
||||
}
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
export function useAiAgent(
|
||||
vaultPath: string,
|
||||
contextPrompt?: string,
|
||||
@@ -48,13 +63,20 @@ export function useAiAgent(
|
||||
const fileCallbacksRef = useRef(fileCallbacks)
|
||||
// Track tool inputs for file-operation detection on ToolDone
|
||||
const toolInputMapRef = useRef<Map<string, { tool: string; input?: string }>>(new Map())
|
||||
// Refs for latest state — avoids stale closures in callbacks.
|
||||
// Synced via useEffect (runs after render, before next user interaction).
|
||||
const messagesRef = useRef<AiAgentMessage[]>([])
|
||||
const statusRef = useRef<AgentStatus>('idle')
|
||||
useEffect(() => { messagesRef.current = messages }, [messages])
|
||||
useEffect(() => { statusRef.current = status }, [status])
|
||||
|
||||
useEffect(() => {
|
||||
fileCallbacksRef.current = fileCallbacks
|
||||
}, [fileCallbacks])
|
||||
|
||||
const sendMessage = useCallback(async (text: string, references?: NoteReference[]) => {
|
||||
if (!text.trim() || status === 'thinking' || status === 'tool-executing') return
|
||||
const currentStatus = statusRef.current
|
||||
if (!text.trim() || currentStatus === 'thinking' || currentStatus === 'tool-executing') return
|
||||
|
||||
const refs = references && references.length > 0 ? references : undefined
|
||||
|
||||
@@ -87,7 +109,13 @@ export function useAiAgent(
|
||||
|
||||
const systemPrompt = contextPrompt ?? buildAgentSystemPrompt()
|
||||
|
||||
await streamClaudeAgent(text.trim(), systemPrompt, vaultPath, {
|
||||
// Embed conversation history from previous exchanges.
|
||||
// Uses messagesRef (not closure-captured messages) to avoid stale closures.
|
||||
const chatHistory = agentMessagesToChatHistory(messagesRef.current.filter(m => !m.isStreaming))
|
||||
const trimmedHistory = trimHistory(chatHistory, MAX_HISTORY_TOKENS)
|
||||
const formattedMessage = formatMessageWithHistory(trimmedHistory, text.trim())
|
||||
|
||||
await streamClaudeAgent(formattedMessage, systemPrompt, vaultPath, {
|
||||
onThinking: (chunk) => {
|
||||
if (abortRef.current.aborted) return
|
||||
update(m => ({ ...m, reasoning: (m.reasoning ?? '') + chunk }))
|
||||
@@ -174,7 +202,7 @@ export function useAiAgent(
|
||||
fileCallbacksRef.current?.onVaultChanged?.()
|
||||
},
|
||||
})
|
||||
}, [status, vaultPath, contextPrompt])
|
||||
}, [vaultPath, contextPrompt])
|
||||
|
||||
const clearConversation = useCallback(() => {
|
||||
abortRef.current.aborted = true
|
||||
|
||||
@@ -46,6 +46,22 @@ export interface AgentStreamCallbacks {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a mock response for browser/test mode.
|
||||
* Inspects the message for conversation history so Playwright tests
|
||||
* can verify that history is actually being sent.
|
||||
*/
|
||||
function mockAgentResponse(message: string): string {
|
||||
if (message.includes('<conversation_history>')) {
|
||||
const allUserLines = message.match(/\[user\]: .+/g) ?? []
|
||||
const turnCount = allUserLines.length
|
||||
const lastLine = allUserLines[allUserLines.length - 1] ?? ''
|
||||
const lastUserMsg = lastLine.replace('[user]: ', '')
|
||||
return `[mock-with-history turns=${turnCount}] You asked: "${lastUserMsg}" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].`
|
||||
}
|
||||
return `[mock-no-history] You said: "${message}" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].`
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream an agent task through the Claude CLI subprocess with full tool access.
|
||||
* The CLI handles the tool-use loop; we receive events for UI updates.
|
||||
@@ -58,7 +74,7 @@ export async function streamClaudeAgent(
|
||||
): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
setTimeout(() => {
|
||||
callbacks.onText('This note is related to [[Build Laputa App]] and [[Matteo Cellini]]. The AI Agent requires the Claude CLI for full functionality.')
|
||||
callbacks.onText(mockAgentResponse(message))
|
||||
callbacks.onDone()
|
||||
}, 300)
|
||||
return
|
||||
|
||||
@@ -200,8 +200,29 @@ describe('streamClaudeChat', () => {
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
|
||||
expect(sessionId).toBe('mock-session')
|
||||
expect(onText).toHaveBeenCalledWith(expect.stringContaining('Claude CLI'))
|
||||
expect(onText).toHaveBeenCalledWith(expect.stringContaining('[mock-no-history]'))
|
||||
expect(onDone).toHaveBeenCalled()
|
||||
expect(onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('mock detects conversation history in message', async () => {
|
||||
const onText = vi.fn()
|
||||
const onDone = vi.fn()
|
||||
const onError = vi.fn()
|
||||
|
||||
const msgWithHistory = formatMessageWithHistory(
|
||||
[{ role: 'user', content: 'What is 2+2?', id: 'm1' }, { role: 'assistant', content: '4', id: 'm2' }],
|
||||
'What was my previous question?',
|
||||
)
|
||||
|
||||
await streamClaudeChat(msgWithHistory, undefined, undefined, {
|
||||
onText, onError, onDone,
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
|
||||
expect(onText).toHaveBeenCalledWith(expect.stringContaining('[mock-with-history'))
|
||||
expect(onText).toHaveBeenCalledWith(expect.stringContaining('turns=2'))
|
||||
expect(onText).toHaveBeenCalledWith(expect.stringContaining('What was my previous question?'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -148,6 +148,23 @@ function handleChatStreamEvent(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a mock response for browser/test mode.
|
||||
* Inspects the message for conversation history so Playwright tests
|
||||
* can verify that history is actually being sent.
|
||||
*/
|
||||
function mockChatResponse(message: string): string {
|
||||
if (message.includes('<conversation_history>')) {
|
||||
const allUserLines = message.match(/\[user\]: .+/g) ?? []
|
||||
const turnCount = allUserLines.length
|
||||
// The last [user] line is the actual new message
|
||||
const lastLine = allUserLines[allUserLines.length - 1] ?? ''
|
||||
const lastUserMsg = lastLine.replace('[user]: ', '')
|
||||
return `[mock-with-history turns=${turnCount}] You asked: "${lastUserMsg}"`
|
||||
}
|
||||
return `[mock-no-history] You said: "${message}"`
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a chat message through the Claude CLI subprocess.
|
||||
* Returns the session ID for conversation continuity via --resume.
|
||||
@@ -160,7 +177,7 @@ export async function streamClaudeChat(
|
||||
): Promise<string> {
|
||||
if (!isTauri()) {
|
||||
setTimeout(() => {
|
||||
callbacks.onText('AI Chat requires the Claude CLI. Install it and run the native app.')
|
||||
callbacks.onText(mockChatResponse(message))
|
||||
callbacks.onDone()
|
||||
}, 300)
|
||||
return 'mock-session'
|
||||
|
||||
84
tests/smoke/ai-chat-history.spec.ts
Normal file
84
tests/smoke/ai-chat-history.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { sendShortcut } from './helpers'
|
||||
|
||||
test.describe('AI chat conversation history', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Block vault API so mock entries are used
|
||||
await page.route('**/api/vault/ping', route => route.fulfill({ status: 503 }))
|
||||
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Select a note so the AI panel has context
|
||||
const noteItem = page.locator('.app__note-list .cursor-pointer').first()
|
||||
await noteItem.click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open AI Chat with Ctrl+I
|
||||
await sendShortcut(page, 'i', ['Control'])
|
||||
await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 3000 })
|
||||
})
|
||||
|
||||
test('first message has no conversation history marker', async ({ page }) => {
|
||||
// Find the input and send a message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
await input.fill('Hello')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for mock response to appear
|
||||
const response = page.getByTestId('ai-message').last()
|
||||
await expect(response).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// First message should have [mock-no-history] since there's no prior conversation
|
||||
await expect(response).toContainText('[mock-no-history]')
|
||||
})
|
||||
|
||||
test('second message includes conversation history from first exchange', async ({ page }) => {
|
||||
// Send first message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
await input.fill('What is 2+2?')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for first response to appear
|
||||
const firstResponse = page.getByTestId('ai-message').last()
|
||||
await expect(firstResponse).toBeVisible({ timeout: 5000 })
|
||||
await expect(firstResponse).toContainText('[mock-no-history]')
|
||||
|
||||
// Send second message
|
||||
await input.fill('What was my previous question?')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for second response — it should contain history marker
|
||||
await page.waitForTimeout(1000)
|
||||
const secondResponse = page.getByTestId('ai-message').last()
|
||||
await expect(secondResponse).toContainText('[mock-with-history', { timeout: 5000 })
|
||||
// turns=2 means 2 [user] lines: original + new question
|
||||
await expect(secondResponse).toContainText('turns=2')
|
||||
})
|
||||
|
||||
test('history resets after clearing conversation', async ({ page }) => {
|
||||
// Send first message
|
||||
const input = page.locator('input[placeholder*="Ask"]')
|
||||
await input.fill('Hello')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
// Wait for response
|
||||
const firstResponse = page.getByTestId('ai-message').last()
|
||||
await expect(firstResponse).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Clear conversation (click the + button)
|
||||
await page.locator('button[title="New conversation"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Messages should be cleared
|
||||
await expect(page.getByTestId('ai-message')).toHaveCount(0)
|
||||
|
||||
// Send new message — should have no history
|
||||
await input.fill('Fresh start')
|
||||
await page.getByTestId('agent-send').click()
|
||||
|
||||
const freshResponse = page.getByTestId('ai-message').last()
|
||||
await expect(freshResponse).toBeVisible({ timeout: 5000 })
|
||||
await expect(freshResponse).toContainText('[mock-no-history]')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user