committed by
GitHub
Unverified
parent
87ab12849c
commit
4ff6861042
102
tests/e2e/chat-history-startup-retry.spec.ts
Normal file
102
tests/e2e/chat-history-startup-retry.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { closeElectronApp, expect, getStableWindow, installIpcMocks, test } from './fixtures/electron';
|
||||
|
||||
function stableStringify(value: unknown): string {
|
||||
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`);
|
||||
return `{${entries.join(',')}}`;
|
||||
}
|
||||
|
||||
test.describe('ClawX startup chat history recovery', () => {
|
||||
test('retries an initial chat.history timeout and eventually renders history', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await installIpcMocks(app, {
|
||||
gatewayStatus: { state: 'running', port: 18789, pid: 12345, connectedAt: Date.now() },
|
||||
gatewayRpc: {},
|
||||
hostApi: {
|
||||
[stableStringify(['/api/gateway/status', 'GET'])]: {
|
||||
ok: true,
|
||||
data: {
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: { state: 'running', port: 18789, pid: 12345, connectedAt: Date.now() },
|
||||
},
|
||||
},
|
||||
[stableStringify(['/api/agents', 'GET'])]: {
|
||||
ok: true,
|
||||
data: {
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: { success: true, agents: [{ id: 'main', name: 'main' }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.evaluate(async ({ app: _app }) => {
|
||||
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
|
||||
let chatHistoryCallCount = 0;
|
||||
|
||||
ipcMain.removeHandler('gateway:rpc');
|
||||
ipcMain.handle('gateway:rpc', async (_event: unknown, method: string, payload: unknown) => {
|
||||
const stableStringify = (value: unknown): string => {
|
||||
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`);
|
||||
return `{${entries.join(',')}}`;
|
||||
};
|
||||
|
||||
const key = stableStringify([method, payload ?? null]);
|
||||
if (key === stableStringify(['sessions.list', {}])) {
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
sessions: [{ key: 'agent:main:main', displayName: 'main' }],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (key === stableStringify(['chat.history', { sessionKey: 'agent:main:main', limit: 200 }])) {
|
||||
chatHistoryCallCount += 1;
|
||||
if (chatHistoryCallCount === 1) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'RPC timeout: chat.history',
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
messages: [
|
||||
{ role: 'user', content: 'hello', timestamp: 1000 },
|
||||
{ role: 'assistant', content: 'history restored after retry', timestamp: 1001 },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return { success: true, result: {} };
|
||||
});
|
||||
});
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
try {
|
||||
await page.reload();
|
||||
} catch (error) {
|
||||
if (!String(error).includes('ERR_FILE_NOT_FOUND')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await expect(page.getByText('history restored after retry')).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText('RPC timeout: chat.history')).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const invokeIpcMock = vi.fn();
|
||||
const hostApiFetchMock = vi.fn();
|
||||
const gatewayStoreGetStateMock = vi.fn();
|
||||
const clearHistoryPoll = vi.fn();
|
||||
const enrichWithCachedImages = vi.fn((messages) => messages);
|
||||
const enrichWithToolResultFiles = vi.fn((messages) => messages);
|
||||
@@ -30,6 +31,12 @@ vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/gateway', () => ({
|
||||
useGatewayStore: {
|
||||
getState: () => gatewayStoreGetStateMock(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/chat/helpers', () => ({
|
||||
clearHistoryPoll: (...args: unknown[]) => clearHistoryPoll(...args),
|
||||
enrichWithCachedImages: (...args: unknown[]) => enrichWithCachedImages(...args),
|
||||
@@ -83,8 +90,13 @@ function makeHarness(initial?: Partial<ChatLikeState>) {
|
||||
describe('chat history actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
vi.useRealTimers();
|
||||
invokeIpcMock.mockResolvedValue({ success: true, result: { messages: [] } });
|
||||
hostApiFetchMock.mockResolvedValue({ messages: [] });
|
||||
gatewayStoreGetStateMock.mockReturnValue({
|
||||
status: { state: 'running', port: 18789, connectedAt: Date.now() },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses cron session fallback when gateway history is empty', async () => {
|
||||
@@ -156,10 +168,158 @@ describe('chat history actions', () => {
|
||||
await actions.loadHistory();
|
||||
|
||||
expect(h.read().messages.map((message) => message.content)).toEqual(['still here']);
|
||||
expect(h.read().error).toBe('Error: Gateway unavailable');
|
||||
expect(h.read().error).toBe('Gateway unavailable');
|
||||
expect(h.read().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('retries the first foreground startup history load after a timeout and then succeeds', async () => {
|
||||
vi.useFakeTimers();
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
gatewayStoreGetStateMock.mockReturnValue({
|
||||
status: { state: 'running', port: 18789, connectedAt: Date.now() - 40_000 },
|
||||
});
|
||||
|
||||
invokeIpcMock
|
||||
.mockResolvedValueOnce({ success: false, error: 'RPC timeout: chat.history' })
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
result: {
|
||||
messages: [
|
||||
{ role: 'assistant', content: 'restored after retry', timestamp: 1000 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const loadPromise = actions.loadHistory();
|
||||
await vi.runAllTimersAsync();
|
||||
await loadPromise;
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'gateway:rpc',
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
);
|
||||
expect(invokeIpcMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'gateway:rpc',
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
);
|
||||
expect(h.read().messages.map((message) => message.content)).toEqual(['restored after retry']);
|
||||
expect(h.read().error).toBeNull();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[chat.history] startup retry scheduled',
|
||||
expect.objectContaining({
|
||||
sessionKey: 'agent:main:main',
|
||||
attempt: 1,
|
||||
errorKind: 'timeout',
|
||||
}),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('stops retrying once the load no longer belongs to the active session', async () => {
|
||||
vi.useFakeTimers();
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
|
||||
invokeIpcMock.mockImplementationOnce(async () => {
|
||||
h.set({
|
||||
currentSessionKey: 'agent:main:other',
|
||||
loading: false,
|
||||
messages: [{ role: 'assistant', content: 'other session', timestamp: 1001 }],
|
||||
});
|
||||
return { success: false, error: 'RPC timeout: chat.history' };
|
||||
});
|
||||
|
||||
await actions.loadHistory();
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenCalledTimes(1);
|
||||
expect(h.read().currentSessionKey).toBe('agent:main:other');
|
||||
expect(h.read().messages.map((message) => message.content)).toEqual(['other session']);
|
||||
expect(h.read().error).toBeNull();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('surfaces a final error only after startup retry budget is exhausted', async () => {
|
||||
vi.useFakeTimers();
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
|
||||
invokeIpcMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'RPC timeout: chat.history',
|
||||
});
|
||||
|
||||
const loadPromise = actions.loadHistory();
|
||||
await vi.runAllTimersAsync();
|
||||
await loadPromise;
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenCalledTimes(2);
|
||||
expect(h.read().messages).toEqual([]);
|
||||
expect(h.read().error).toBe('RPC timeout: chat.history');
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[chat.history] startup retry exhausted',
|
||||
expect.objectContaining({
|
||||
sessionKey: 'agent:main:main',
|
||||
}),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not retry quiet history refreshes', async () => {
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
|
||||
invokeIpcMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'RPC timeout: chat.history',
|
||||
});
|
||||
|
||||
await actions.loadHistory(true);
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenCalledTimes(1);
|
||||
expect(h.read().error).toBeNull();
|
||||
});
|
||||
|
||||
it('does not retry non-retryable startup failures', async () => {
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
});
|
||||
const actions = createHistoryActions(h.set as never, h.get as never);
|
||||
|
||||
invokeIpcMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Validation failed: bad session key',
|
||||
});
|
||||
|
||||
await actions.loadHistory();
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenCalledTimes(1);
|
||||
expect(h.read().error).toBe('Validation failed: bad session key');
|
||||
});
|
||||
|
||||
it('filters out system messages from loaded history', async () => {
|
||||
const { createHistoryActions } = await import('@/stores/chat/history-actions');
|
||||
const h = makeHarness();
|
||||
|
||||
266
tests/unit/chat-store-history-retry.test.ts
Normal file
266
tests/unit/chat-store-history-retry.test.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { gatewayRpcMock, agentsState, hostApiFetchMock } = vi.hoisted(() => ({
|
||||
gatewayRpcMock: vi.fn(),
|
||||
agentsState: {
|
||||
agents: [] as Array<Record<string, unknown>>,
|
||||
},
|
||||
hostApiFetchMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/gateway', () => ({
|
||||
useGatewayStore: {
|
||||
getState: () => ({
|
||||
status: { state: 'running', port: 18789, connectedAt: Date.now() },
|
||||
rpc: gatewayRpcMock,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/agents', () => ({
|
||||
useAgentsStore: {
|
||||
getState: () => agentsState,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||||
}));
|
||||
|
||||
describe('useChatStore startup history retry', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers();
|
||||
window.localStorage.clear();
|
||||
agentsState.agents = [];
|
||||
gatewayRpcMock.mockReset();
|
||||
hostApiFetchMock.mockReset();
|
||||
hostApiFetchMock.mockResolvedValue({ messages: [] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('uses the longer timeout only for the initial foreground history load', async () => {
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
showThinking: true,
|
||||
});
|
||||
|
||||
gatewayRpcMock
|
||||
.mockResolvedValueOnce({
|
||||
messages: [{ role: 'assistant', content: 'first load', timestamp: 1000 }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [{ role: 'assistant', content: 'quiet refresh', timestamp: 1001 }],
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadHistory(false);
|
||||
vi.advanceTimersByTime(1_000);
|
||||
await useChatStore.getState().loadHistory(true);
|
||||
|
||||
expect(gatewayRpcMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
);
|
||||
expect(gatewayRpcMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
undefined,
|
||||
);
|
||||
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 72_600);
|
||||
setTimeoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('keeps non-startup foreground loading safety timeout at 15 seconds', async () => {
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
showThinking: true,
|
||||
});
|
||||
|
||||
gatewayRpcMock
|
||||
.mockResolvedValueOnce({
|
||||
messages: [{ role: 'assistant', content: 'first load', timestamp: 1000 }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
messages: [{ role: 'assistant', content: 'second foreground load', timestamp: 1001 }],
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadHistory(false);
|
||||
setTimeoutSpy.mockClear();
|
||||
await useChatStore.getState().loadHistory(false);
|
||||
|
||||
expect(gatewayRpcMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
undefined,
|
||||
);
|
||||
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 15_000);
|
||||
setTimeoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not burn the first-load retry path when the first attempt becomes stale', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }, { key: 'agent:main:other' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
showThinking: true,
|
||||
});
|
||||
|
||||
let resolveFirstAttempt: ((value: { messages: Array<{ role: string; content: string; timestamp: number }> }) => void) | null = null;
|
||||
gatewayRpcMock
|
||||
.mockImplementationOnce(() => new Promise((resolve) => {
|
||||
resolveFirstAttempt = resolve;
|
||||
}))
|
||||
.mockRejectedValueOnce(new Error('RPC timeout: chat.history'))
|
||||
.mockResolvedValueOnce({
|
||||
messages: [{ role: 'assistant', content: 'restored after retry', timestamp: 1002 }],
|
||||
});
|
||||
|
||||
const firstLoad = useChatStore.getState().loadHistory(false);
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:other',
|
||||
messages: [{ role: 'assistant', content: 'other session', timestamp: 1001 }],
|
||||
});
|
||||
resolveFirstAttempt?.({
|
||||
messages: [{ role: 'assistant', content: 'stale original payload', timestamp: 1000 }],
|
||||
});
|
||||
await firstLoad;
|
||||
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
messages: [],
|
||||
});
|
||||
const secondLoad = useChatStore.getState().loadHistory(false);
|
||||
await vi.runAllTimersAsync();
|
||||
await secondLoad;
|
||||
|
||||
expect(gatewayRpcMock).toHaveBeenCalledTimes(3);
|
||||
expect(gatewayRpcMock.mock.calls[0]).toEqual([
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
]);
|
||||
expect(gatewayRpcMock.mock.calls[1]).toEqual([
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
]);
|
||||
expect(gatewayRpcMock.mock.calls[2]).toEqual([
|
||||
'chat.history',
|
||||
{ sessionKey: 'agent:main:main', limit: 200 },
|
||||
35_000,
|
||||
]);
|
||||
expect(useChatStore.getState().messages.map((message) => message.content)).toEqual(['restored after retry']);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[chat.history] startup retry scheduled',
|
||||
expect.objectContaining({
|
||||
sessionKey: 'agent:main:main',
|
||||
attempt: 1,
|
||||
}),
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('stops retrying once the user switches sessions mid-load', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [{ key: 'agent:main:main' }, { key: 'agent:main:other' }],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
sending: false,
|
||||
activeRunId: null,
|
||||
streamingText: '',
|
||||
streamingMessage: null,
|
||||
streamingTools: [],
|
||||
pendingFinal: false,
|
||||
lastUserMessageAt: null,
|
||||
pendingToolImages: [],
|
||||
error: null,
|
||||
loading: false,
|
||||
thinkingLevel: null,
|
||||
showThinking: true,
|
||||
});
|
||||
|
||||
gatewayRpcMock.mockImplementationOnce(async () => {
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:other',
|
||||
messages: [{ role: 'assistant', content: 'other session', timestamp: 1001 }],
|
||||
loading: false,
|
||||
});
|
||||
throw new Error('RPC timeout: chat.history');
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadHistory(false);
|
||||
|
||||
expect(gatewayRpcMock).toHaveBeenCalledTimes(1);
|
||||
expect(useChatStore.getState().currentSessionKey).toBe('agent:main:other');
|
||||
expect(useChatStore.getState().messages.map((message) => message.content)).toEqual(['other session']);
|
||||
expect(useChatStore.getState().error).toBeNull();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user