Files
LocalMind/__tests__/useAppStore.test.ts
T
evandro 541aefd7e4 feat: check the server on launch and remember the selected model
The chat showed 'Server offline' until the user opened Settings, and the selected model was forgotten on every restart. useAutoConnect fetches the models once after the persisted settings are restored; activeModelId is now persisted.
2026-10-10 04:31:21 -03:00

189 lines
7.8 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import { appStorage } from '../store/debouncedStorage';
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, the selected model 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',
availableModels: [{ id: 'model', object: 'model', owned_by: 'x' }],
editingMessageId: 'e',
});
state().addMessage({ role: 'user', content: 'persist me' });
await appStorage.flush();
const raw = await AsyncStorage.getItem('localaiapp-storage');
const persisted = JSON.parse(raw as string).state;
expect(Object.keys(persisted).sort()).toEqual(
['activeModelId', 'chatSessions', 'currentChatId', 'messages', 'serverConfig', 'theme'],
);
expect(persisted.serverConfig).toEqual({ host: 'lan.local', port: 4321, apiKey: 'k' });
expect(persisted.activeModelId).toBe('model');
expect(persisted.messages[0].content).toBe('persist me');
});
});