一些检查失败了
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
180 行
5.0 KiB
TypeScript
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' },
|
|
]);
|
|
});
|
|
});
|