mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-19 05:14:09 +08:00
# Conflicts: # frontend/src/App.tsx # frontend/src/components/AISettingsModal.tsx # frontend/src/components/ConnectionModal.edit-password.test.tsx # frontend/src/components/ConnectionModal.tsx # frontend/src/components/DataSyncModal.i18n.test.ts # frontend/src/components/DataSyncModal.tsx # frontend/src/components/QueryEditor.external-sql-save.test.tsx # frontend/src/components/QueryEditor.tsx # frontend/src/components/Sidebar.locate-toolbar.test.tsx # frontend/src/components/Sidebar.tsx # frontend/src/components/SnippetSettingsModal.tsx # frontend/src/components/TableOverview.tsx # frontend/src/components/ai/AIChatHeader.test.tsx # frontend/src/components/ai/AISettingsProvidersSection.tsx # frontend/src/components/ai/aiChatPayloadDispatch.ts # frontend/src/components/ai/aiChatReadiness.ts # frontend/src/components/ai/aiSettingsModalConfig.tsx # frontend/src/components/ai/messageBubble/AIMessageCodeBlock.tsx # frontend/src/components/sidebarV2Utils.ts # frontend/src/i18n/catalog.test.ts # frontend/src/utils/connectionTypeCatalog.test.ts # frontend/src/utils/connectionTypeCatalog.ts # frontend/src/utils/tabDisplay.ts # internal/ai/provider/custom.go # internal/ai/service/service.go # internal/app/methods_driver.go # internal/app/methods_file.go # internal/db/custom_impl.go # internal/db/iris_impl.go # internal/db/mariadb_impl.go # internal/db/sqlserver_impl.go # shared/i18n/de-DE.json # shared/i18n/en-US.json # shared/i18n/ja-JP.json # shared/i18n/ru-RU.json # shared/i18n/zh-CN.json # shared/i18n/zh-TW.json
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { t as translateCatalog } from '../../i18n';
|
|
import { dispatchAIChatPayload } from './aiChatPayloadDispatch';
|
|
|
|
describe('aiChatPayloadDispatch', () => {
|
|
const originalWindow = (globalThis as any).window;
|
|
|
|
afterEach(() => {
|
|
(globalThis as any).window = originalWindow;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('prefers streaming when AIChatStream is available', async () => {
|
|
const AIChatStream = vi.fn().mockResolvedValue(undefined);
|
|
const addAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatStream },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-stream',
|
|
});
|
|
|
|
expect(result).toBe('stream');
|
|
expect(AIChatStream).toHaveBeenCalledWith('session-1', [{ role: 'user', content: 'hello' }], []);
|
|
expect(addAIChatMessage).not.toHaveBeenCalled();
|
|
expect(setSending).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('appends a non-stream assistant message when session-aware send is available', async () => {
|
|
const AIChatSendInSession = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
content: 'done',
|
|
reasoning_content: 'thinking',
|
|
});
|
|
const addAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
const onNonStreamSuccess = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatSendInSession },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-send',
|
|
onNonStreamSuccess,
|
|
});
|
|
|
|
expect(result).toBe('send');
|
|
expect(AIChatSendInSession).toHaveBeenCalledWith('session-1', [{ role: 'user', content: 'hello' }], []);
|
|
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
id: 'msg-send',
|
|
role: 'assistant',
|
|
content: 'done',
|
|
thinking: 'thinking',
|
|
reasoning_content: 'thinking',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
expect(onNonStreamSuccess).toHaveBeenCalled();
|
|
});
|
|
|
|
it('settles the pending assistant message when falling back to non-stream send', async () => {
|
|
const AIChatSendInSession = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
content: 'done',
|
|
reasoning_content: 'thinking',
|
|
});
|
|
const addAIChatMessage = vi.fn();
|
|
const updateAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatSendInSession },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
updateAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-send',
|
|
pendingAssistantMessageId: 'assistant-connecting',
|
|
});
|
|
|
|
expect(result).toBe('send');
|
|
expect(AIChatSendInSession).toHaveBeenCalledWith('session-1', [{ role: 'user', content: 'hello' }], []);
|
|
expect(addAIChatMessage).not.toHaveBeenCalled();
|
|
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
|
|
content: 'done',
|
|
thinking: 'thinking',
|
|
reasoning_content: 'thinking',
|
|
loading: false,
|
|
phase: 'idle',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('falls back to stateless AIChatSend when session-aware send is unavailable', async () => {
|
|
const AIChatSend = vi.fn().mockResolvedValue({
|
|
success: true,
|
|
content: 'done',
|
|
reasoning_content: 'thinking',
|
|
});
|
|
const addAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatSend },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-send',
|
|
});
|
|
|
|
expect(result).toBe('send');
|
|
expect(AIChatSend).toHaveBeenCalledWith([{ role: 'user', content: 'hello' }], []);
|
|
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
id: 'msg-send',
|
|
role: 'assistant',
|
|
content: 'done',
|
|
thinking: 'thinking',
|
|
reasoning_content: 'thinking',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('emits the unavailable message when the AI service is missing', async () => {
|
|
const addAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
const unavailableContent = translateCatalog('ai_chat.panel.message.service_not_ready', undefined, 'zh-CN');
|
|
(globalThis as any).window = {};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-unavailable',
|
|
unavailableContent,
|
|
});
|
|
|
|
expect(result).toBe('unavailable');
|
|
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
id: 'msg-unavailable',
|
|
content: unavailableContent,
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('settles the pending assistant message when the AI service is missing', async () => {
|
|
const addAIChatMessage = vi.fn();
|
|
const updateAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
(globalThis as any).window = {};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
updateAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-unavailable',
|
|
pendingAssistantMessageId: 'assistant-connecting',
|
|
});
|
|
|
|
expect(result).toBe('unavailable');
|
|
expect(addAIChatMessage).not.toHaveBeenCalled();
|
|
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
|
|
content: '❌ AI Service is not ready',
|
|
loading: false,
|
|
phase: 'idle',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('sanitizes thrown errors and preserves the raw error when the cleaned text changes', async () => {
|
|
const AIChatSend = vi.fn().mockRejectedValue(new Error('<html><title>502 Bad Gateway</title></html>'));
|
|
const addAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatSend },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-error',
|
|
});
|
|
|
|
expect(result).toBe('error');
|
|
expect(addAIChatMessage).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
id: 'msg-error',
|
|
content: '❌ Send failed: HTTP 502: 502 Bad Gateway',
|
|
rawError: '<html><title>502 Bad Gateway</title></html>',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('settles the pending assistant message when streaming startup throws', async () => {
|
|
const AIChatStream = vi.fn().mockRejectedValue(new Error('<html><title>502 Bad Gateway</title></html>'));
|
|
const addAIChatMessage = vi.fn();
|
|
const updateAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatStream },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
updateAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-error',
|
|
pendingAssistantMessageId: 'assistant-connecting',
|
|
});
|
|
|
|
expect(result).toBe('error');
|
|
expect(addAIChatMessage).not.toHaveBeenCalled();
|
|
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
|
|
content: '❌ Send failed: HTTP 502: 502 Bad Gateway',
|
|
rawError: '<html><title>502 Bad Gateway</title></html>',
|
|
loading: false,
|
|
phase: 'idle',
|
|
}));
|
|
expect(setSending).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it('localizes fallback service and dispatch error messages while preserving raw error detail', async () => {
|
|
const addAIChatMessage = vi.fn();
|
|
const updateAIChatMessage = vi.fn();
|
|
const setSending = vi.fn();
|
|
(globalThis as any).window = {};
|
|
|
|
await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
updateAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-unavailable',
|
|
pendingAssistantMessageId: 'assistant-connecting',
|
|
translate: (key, params) => translateCatalog(key, params, 'en-US'),
|
|
} as Parameters<typeof dispatchAIChatPayload>[0] & {
|
|
translate: (key: string, params?: Record<string, string | number | boolean | null | undefined>) => string;
|
|
});
|
|
|
|
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
|
|
content: '❌ AI Service is not ready',
|
|
}));
|
|
|
|
const AIChatStream = vi.fn().mockRejectedValue(new Error('<html><title>502 Bad Gateway</title></html>'));
|
|
(globalThis as any).window = {
|
|
go: {
|
|
aiservice: {
|
|
Service: { AIChatStream },
|
|
},
|
|
},
|
|
};
|
|
updateAIChatMessage.mockClear();
|
|
|
|
await dispatchAIChatPayload({
|
|
sid: 'session-1',
|
|
messages: [{ role: 'user', content: 'hello' }],
|
|
tools: [],
|
|
addAIChatMessage,
|
|
updateAIChatMessage,
|
|
setSending,
|
|
nextMessageId: () => 'msg-error',
|
|
pendingAssistantMessageId: 'assistant-connecting',
|
|
translate: (key, params) => translateCatalog(key, params, 'en-US'),
|
|
} as Parameters<typeof dispatchAIChatPayload>[0] & {
|
|
translate: (key: string, params?: Record<string, string | number | boolean | null | undefined>) => string;
|
|
});
|
|
|
|
expect(updateAIChatMessage).toHaveBeenCalledWith('session-1', 'assistant-connecting', expect.objectContaining({
|
|
content: '❌ Send failed: HTTP 502: 502 Bad Gateway',
|
|
rawError: '<html><title>502 Bad Gateway</title></html>',
|
|
}));
|
|
});
|
|
});
|