文件
mynode/packages/app-desktop/app.reducer.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

180 行
5.0 KiB
TypeScript

import { ChatRole, ChatToolCall, ChatToolMessage } from '@joplin/lib/services/ai/types';
import { AiChatMessage, AppState, createAppDefaultWindowState } from './app.reducer';
import appReducer, { createAppDefaultState } from './app.reducer';
describe('app.reducer', () => {
it('should clear a deleted conversation from matching windows and keep other conversations', () => {
const messages: AiChatMessage[] = [{ id: 'question', createdTime: 1, role: 'user', text: 'Question', raw: [], noteId: '', noteTitle: '' }];
const state = createAppDefaultState({});
state.aiChatConversationId = 'deleted';
state.aiChatMessages = messages;
state.backgroundWindows = {
second: { ...createAppDefaultWindowState(), windowId: 'second', aiChatConversationId: 'deleted', aiChatMessages: messages },
third: { ...createAppDefaultWindowState(), windowId: 'third', aiChatConversationId: 'other', aiChatMessages: messages },
};
const result = appReducer(state, { type: 'AI_CHAT_DELETE', conversationId: 'deleted' });
expect(result.aiChatConversationId).toBeNull();
expect(result.aiChatMessages).toEqual([]);
expect(result.backgroundWindows.second.aiChatConversationId).toBeNull();
expect(result.backgroundWindows.second.aiChatMessages).toEqual([]);
expect(result.backgroundWindows.third.aiChatConversationId).toBe('other');
expect(result.backgroundWindows.third.aiChatMessages).toEqual(messages);
});
it('should handle DIALOG_OPEN', async () => {
const state: AppState = createAppDefaultState({});
let newState = appReducer(state, {
type: 'DIALOG_OPEN',
name: 'syncWizard',
});
expect(newState.dialogs.length).toBe(1);
expect(newState.dialogs[0].name).toBe('syncWizard');
expect(() => appReducer(newState, {
type: 'DIALOG_OPEN',
name: 'syncWizard',
})).toThrow();
newState = appReducer(newState, {
type: 'DIALOG_CLOSE',
name: 'syncWizard',
});
expect(newState.dialogs.length).toBe(0);
expect(() => appReducer(newState, {
type: 'DIALOG_CLOSE',
name: 'syncWizard',
})).toThrow();
newState = appReducer(newState, {
type: 'DIALOG_OPEN',
name: 'syncWizard',
});
newState = appReducer(newState, {
type: 'DIALOG_OPEN',
name: 'setPassword',
});
expect(newState.dialogs).toEqual([
{ name: 'syncWizard', props: {} },
{ name: 'setPassword', props: {} },
]);
});
it('aI_STATUS_UPDATE merges a partial payload without clobbering unrelated fields', () => {
const state: AppState = {
...createAppDefaultState({}),
aiStatus: { degraded: true, tokensUsed: 500, tokensBudget: 1000, lastToastShownAt: 12345 },
};
const afterUsage = appReducer(state, {
type: 'AI_STATUS_UPDATE',
payload: { tokensUsed: 600 },
});
expect(afterUsage.aiStatus).toEqual({
degraded: true,
tokensUsed: 600,
tokensBudget: 1000,
lastToastShownAt: 12345,
});
const afterToast = appReducer(afterUsage, {
type: 'AI_STATUS_UPDATE',
payload: { lastToastShownAt: 67890 },
});
expect(afterToast.aiStatus).toEqual({
degraded: true,
tokensUsed: 600,
tokensBudget: 1000,
lastToastShownAt: 67890,
});
});
it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => {
const state: AppState = {
...createAppDefaultState({}),
backgroundWindows: {
testWindow: {
...createAppDefaultWindowState(),
windowId: 'testWindow',
visibleDialogs: {
testDialog: true,
},
},
},
};
const newState = appReducer(state, {
type: 'VISIBLE_DIALOGS_ADD',
name: 'testDialog',
});
expect(newState.backgroundWindows.testWindow.visibleDialogs).toEqual({});
expect(newState.visibleDialogs).toEqual({ testDialog: true });
});
it('should build AI chat history', () => {
let state: AppState = {
...createAppDefaultState({}),
aiChatConversationId: 'chat-1',
};
let idCounter = 0;
const buildMessage = (role: ChatRole.User | ChatRole.Assistant, content: string, toolCalls: ChatToolCall[]) => ({
id: `id-${idCounter++}`,
createdTime: 1,
noteId: 'note-a',
noteTitle: 'A',
role,
text: content,
raw: [
{
role,
content: content,
...(toolCalls.length ? { toolCalls } : {}),
},
],
} satisfies AiChatMessage);
state = appReducer(state, {
type: 'AI_CHAT_APPEND',
conversationId: 'chat-1',
message: buildMessage(ChatRole.User, 'Test', []),
});
state = appReducer(state, {
type: 'AI_CHAT_APPEND',
conversationId: 'chat-1',
message: buildMessage(
ChatRole.Assistant,
'Testing',
[{ toolName: 'testTool', callId: 'call-1', arguments: { arg: 1 }, parseError: null }],
),
});
state = appReducer(state, {
type: 'AI_CHAT_ADD_TOOL_RESULT',
conversationId: 'chat-1',
toolCall: ({
role: ChatRole.Tool,
toolName: 'testTool',
toolCallId: 'call-1',
userDescription: '',
isError: false,
isEdit: true,
content: 'Result',
} satisfies ChatToolMessage),
});
expect(state.aiChatMessages).toMatchObject([
{ id: 'id-0', role: 'user', text: 'Test', raw: [{ role: 'user' }] },
{ id: 'id-1', role: 'assistant', text: 'Testing' },
]);
});
});