fix(mobile): KI-AGENT: Neu angelegte Chats vor veralteten Listen schützen

This commit is contained in:
2026-10-19 11:44:07 +02:00
parent da84e98b8a
commit e0a94f5e29
2 changed files with 55 additions and 0 deletions

View File

@@ -92,6 +92,8 @@ function ChatScreen({ token }: { token: string }) {
setBusy(room.key); setBusy(room.key);
const ready = await chatApi.provision(token, room); const ready = await chatApi.provision(token, room);
if (!alive.current || version !== selectionRevision.current || directoryRef.current?.hiddenRoomKeys.includes(room.key)) return; if (!alive.current || version !== selectionRevision.current || directoryRef.current?.hiddenRoomKeys.includes(room.key)) return;
// A list requested before provisioning must not replace the new room with its placeholder.
++directoryRevision.current;
setDirectory(previous => previous && ({ ...previous, rooms: previous.rooms.map(item => item.key === room.key ? ready : item) })); setDirectory(previous => previous && ({ ...previous, rooms: previous.rooms.map(item => item.key === room.key ? ready : item) }));
setActiveKey(ready.key); setActiveKey(ready.key);
} catch (err) { if (alive.current && version === selectionRevision.current) setError(err instanceof Error ? err.message : 'Chat konnte nicht geöffnet werden.'); } } catch (err) { if (alive.current && version === selectionRevision.current) setError(err instanceof Error ? err.message : 'Chat konnte nicht geöffnet werden.'); }

View File

@@ -0,0 +1,53 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { test } from 'node:test';
import React from 'react';
import { act, create } from 'react-test-renderer';
import ts from 'typescript';
import * as chatModel from '../src/lib/chat.ts';
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const require = createRequire(import.meta.url);
const deferred = () => { let resolve; const promise = new Promise(yes => { resolve = yes; }); return { promise, resolve }; };
test('Eine verspätete Raumliste verdrängt keinen gerade angelegten Projektchat', async () => {
const project = { key: 'project_42', name: 'Projekt', source: 'internal', type: 'project', exists: false, projectId: 42 };
const directory = { rooms: [{ key: 'allgemein', name: 'Allgemein', source: 'internal', exists: true }, project], hiddenRoomKeys: [], invitations: [], projects: [] };
const provision = deferred(), list = deferred(), timers = new Map();
let directoryCalls = 0;
const api = { directory: () => ++directoryCalls === 1 ? Promise.resolve(directory) : list.promise, provision: () => provision.promise };
const output = ts.transpileModule(readFileSync(new URL('../app/(tabs)/communication.tsx', import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2022 },
}).outputText;
const exports = {};
new Function('require', 'exports', 'setInterval', 'clearInterval', output)(name => {
if (name === 'react-native') return {
...Object.fromEntries(['ActivityIndicator', 'FlatList', 'KeyboardAvoidingView', 'Modal', 'Pressable', 'ScrollView', 'Text', 'TextInput', 'View'].map(key => [key, key])),
StyleSheet: { create: value => value }, Platform: { OS: 'ios' }, DeviceEventEmitter: { emit() {} },
AppState: { currentState: 'active', addEventListener: () => ({ remove() {} }) },
};
if (name === '@expo/vector-icons') return { Ionicons: 'Icon' };
if (name === '@react-navigation/native') return { useIsFocused: () => true };
if (name === 'expo-crypto') return { randomUUID: () => 'draft-id' };
if (name === '@/components/chat-conversation') return { ChatConversation: 'Conversation' };
if (name === '@/src/lib/api') return {};
if (name === '@/src/lib/chat-api') return { chatApi: api };
if (name === '@/src/lib/chat') return chatModel;
if (name === '@/src/lib/server-config') return { getApiBaseUrlSync: () => 'https://test.invalid' };
if (name === '@/src/providers/auth-provider') return { useAuth: () => ({ token: 'test', activeTenantId: 1 }) };
return require(name);
}, exports, callback => { const id = Symbol(); timers.set(id, callback); return id; }, id => timers.delete(id));
let root;
try {
await act(async () => { root = create(React.createElement(exports.default)); });
const roomList = root.root.findAllByType('FlatList').find(node => node.props.horizontal);
await act(async () => roomList.props.renderItem({ item: project }).props.onPress());
await act(async () => { for (const callback of timers.values()) callback(); });
assert.equal(directoryCalls, 2);
await act(async () => provision.resolve({ ...project, exists: true }));
assert.equal(root.root.findByType('Conversation').props.room.key, 'project_42');
await act(async () => list.resolve(directory));
assert.equal(root.root.findByType('Conversation').props.room.key, 'project_42');
} finally { await act(async () => root?.unmount()); }
});