Files
LocalMind/__tests__/useAppStore.test.ts
T

186 lines
7.6 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import { useAppStore } from '../store/useAppStore';
const state = () => useAppStore.getState();
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true);
});
describe('messages', () => {
it('addMessage appends with a generated id and timestamp, and returns the id', () => {
const id = state().addMessage({ role: 'user', content: 'hi' });
const [msg] = state().messages;
expect(msg.id).toBe(id);
expect(msg).toMatchObject({ role: 'user', content: 'hi' });
expect(typeof msg.timestamp).toBe('number');
});
it('generates distinct ids for messages created in the same millisecond', () => {
const ids = new Set(Array.from({ length: 50 }, () => state().addMessage({ role: 'user', content: 'x' })));
expect(ids.size).toBe(50);
});
it('appendToMessage and updateMessageContent only touch the target message', () => {
const a = state().addMessage({ role: 'assistant', content: 'He' });
const b = state().addMessage({ role: 'assistant', content: 'other' });
state().appendToMessage(a, 'llo');
expect(state().messages.map((m) => m.content)).toEqual(['Hello', 'other']);
state().updateMessageContent(b, 'replaced');
expect(state().messages.map((m) => m.content)).toEqual(['Hello', 'replaced']);
});
it('editMessage replaces the content and marks it as edited', () => {
const id = state().addMessage({ role: 'user', content: 'old' });
state().editMessage(id, 'new');
expect(state().messages[0]).toMatchObject({ content: 'new', isEdited: true });
});
it('deleteMessage removes only the given message', () => {
const a = state().addMessage({ role: 'user', content: 'a' });
state().addMessage({ role: 'assistant', content: 'b' });
state().deleteMessage(a);
expect(state().messages.map((m) => m.content)).toEqual(['b']);
});
describe('deleteMessagesAfter', () => {
it('keeps the given message and drops everything after it', () => {
const a = state().addMessage({ role: 'user', content: 'a' });
state().addMessage({ role: 'assistant', content: 'b' });
state().addMessage({ role: 'user', content: 'c' });
state().deleteMessagesAfter(a);
expect(state().messages.map((m) => m.content)).toEqual(['a']);
});
it('does nothing when the id is unknown', () => {
state().addMessage({ role: 'user', content: 'a' });
state().deleteMessagesAfter('missing');
expect(state().messages).toHaveLength(1);
});
});
it('clearChat empties the conversation and forgets the current session', () => {
state().addMessage({ role: 'user', content: 'a' });
state().setCurrentChatId('abc');
state().clearChat();
expect(state().messages).toEqual([]);
expect(state().currentChatId).toBeNull();
});
});
describe('chat sessions', () => {
it('saveCurrentChat ignores an empty conversation', () => {
state().saveCurrentChat();
expect(state().chatSessions).toEqual([]);
expect(state().currentChatId).toBeNull();
});
it('creates a session titled by the first user message and makes it current', () => {
state().addMessage({ role: 'assistant', content: 'welcome' });
state().addMessage({ role: 'user', content: 'first question' });
state().saveCurrentChat();
expect(state().chatSessions).toHaveLength(1);
const [session] = state().chatSessions;
expect(session.title).toBe('first question');
expect(session.messages).toHaveLength(2);
expect(state().currentChatId).toBe(session.id);
});
it('truncates long titles to 40 characters plus an ellipsis', () => {
state().addMessage({ role: 'user', content: 'x'.repeat(60) });
state().saveCurrentChat();
expect(state().chatSessions[0].title).toBe(`${'x'.repeat(40)}...`);
});
it('falls back to "New Chat" when there is no user message', () => {
state().addMessage({ role: 'assistant', content: 'hello' });
state().saveCurrentChat();
expect(state().chatSessions[0].title).toBe('New Chat');
});
it('updates the existing session instead of creating another one', () => {
state().addMessage({ role: 'user', content: 'q' });
state().saveCurrentChat();
const firstId = state().currentChatId;
state().addMessage({ role: 'assistant', content: 'a' });
state().saveCurrentChat();
expect(state().chatSessions).toHaveLength(1);
expect(state().chatSessions[0].id).toBe(firstId);
expect(state().chatSessions[0].messages).toHaveLength(2);
});
it('only rewrites the session that matches currentChatId', () => {
useAppStore.setState({
chatSessions: [
{ id: '1', title: 'one', updatedAt: 1, messages: [] },
{ id: '2', title: 'two', updatedAt: 2, messages: [] },
],
currentChatId: '2',
messages: [{ id: 'm', role: 'user', content: 'changed', timestamp: 3 }],
});
state().saveCurrentChat();
expect(state().chatSessions[0]).toMatchObject({ id: '1', title: 'one' });
expect(state().chatSessions[1]).toMatchObject({ id: '2', title: 'changed' });
});
it('loadChat restores the messages of a session, and ignores unknown ids', () => {
const messages = [{ id: 'm', role: 'user' as const, content: 'saved', timestamp: 1 }];
useAppStore.setState({ chatSessions: [{ id: 's', title: 't', updatedAt: 1, messages }] });
state().loadChat('missing');
expect(state().messages).toEqual([]);
state().loadChat('s');
expect(state().messages).toEqual(messages);
expect(state().currentChatId).toBe('s');
});
it('deleteChatSession resets the open conversation only when it deletes the current one', () => {
const session = (id: string) => ({ id, title: id, updatedAt: 1, messages: [] });
useAppStore.setState({
chatSessions: [session('a'), session('b')],
currentChatId: 'a',
messages: [{ id: 'm', role: 'user', content: 'x', timestamp: 1 }],
});
state().deleteChatSession('b');
expect(state().chatSessions.map((s) => s.id)).toEqual(['a']);
expect(state().messages).toHaveLength(1);
state().deleteChatSession('a');
expect(state().chatSessions).toEqual([]);
expect(state().messages).toEqual([]);
expect(state().currentChatId).toBeNull();
});
});
describe('persistence', () => {
it('persists settings and chats but not transient connection or streaming state', async () => {
useAppStore.setState({
theme: 'dark',
serverConfig: { host: 'lan.local', port: 4321, apiKey: 'k' },
isConnected: true,
isStreaming: true,
activeModelId: 'model',
editingMessageId: 'e',
});
state().addMessage({ role: 'user', content: 'persist me' });
await new Promise((resolve) => setTimeout(resolve, 0));
const raw = await AsyncStorage.getItem('localaiapp-storage');
const persisted = JSON.parse(raw as string).state;
expect(Object.keys(persisted).sort()).toEqual(
['chatSessions', 'currentChatId', 'messages', 'serverConfig', 'theme'],
);
expect(persisted.serverConfig).toEqual({ host: 'lan.local', port: 4321, apiKey: 'k' });
expect(persisted.messages[0].content).toBe('persist me');
});
});