mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
feat(llm): add e2e tests for llm
This commit is contained in:
parent
7f9ad04b57
commit
bb483558b0
251
apps/server-e2e/src/ai_settings.spec.ts
Normal file
251
apps/server-e2e/src/ai_settings.spec.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
test.describe("AI Settings", () => {
|
||||||
|
test("Should access AI settings page", async ({ page, context }) => {
|
||||||
|
page.setDefaultTimeout(15_000);
|
||||||
|
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to settings
|
||||||
|
await app.goToSettings();
|
||||||
|
|
||||||
|
// Navigate to AI settings
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
// Verify we're on the AI settings page
|
||||||
|
await expect(app.currentNoteSplitTitle).toHaveValue("AI Settings");
|
||||||
|
|
||||||
|
// Check that AI settings content is visible
|
||||||
|
const aiSettingsContent = app.currentNoteSplitContent;
|
||||||
|
await aiSettingsContent.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
// Verify basic AI settings elements are present
|
||||||
|
await expect(aiSettingsContent).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should toggle AI features", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
// Look for AI enable/disable toggle
|
||||||
|
const aiToggle = app.currentNoteSplitContent.locator('input[type="checkbox"]').first();
|
||||||
|
|
||||||
|
if (await aiToggle.isVisible()) {
|
||||||
|
// Get initial state
|
||||||
|
const initialState = await aiToggle.isChecked();
|
||||||
|
|
||||||
|
// Toggle the setting
|
||||||
|
await aiToggle.click();
|
||||||
|
|
||||||
|
// Wait for the change to be saved
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify the state changed
|
||||||
|
const newState = await aiToggle.isChecked();
|
||||||
|
expect(newState).toBe(!initialState);
|
||||||
|
|
||||||
|
// Toggle back to original state
|
||||||
|
await aiToggle.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify we're back to the original state
|
||||||
|
const finalState = await aiToggle.isChecked();
|
||||||
|
expect(finalState).toBe(initialState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should configure AI provider settings", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
// Look for provider configuration elements
|
||||||
|
const settingsContent = app.currentNoteSplitContent;
|
||||||
|
|
||||||
|
// Check for common AI provider setting elements
|
||||||
|
const providerSelects = settingsContent.locator('select');
|
||||||
|
const apiKeyInputs = settingsContent.locator('input[type="password"], input[type="text"]');
|
||||||
|
|
||||||
|
if (await providerSelects.count() > 0) {
|
||||||
|
// Test provider selection
|
||||||
|
const firstSelect = providerSelects.first();
|
||||||
|
await firstSelect.click();
|
||||||
|
|
||||||
|
// Verify options are available
|
||||||
|
const options = firstSelect.locator('option');
|
||||||
|
const optionCount = await options.count();
|
||||||
|
expect(optionCount).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await apiKeyInputs.count() > 0) {
|
||||||
|
// Test API key field interaction (without actually setting a key)
|
||||||
|
const firstInput = apiKeyInputs.first();
|
||||||
|
await firstInput.click();
|
||||||
|
|
||||||
|
// Verify the field is interactive
|
||||||
|
await expect(firstInput).toBeFocused();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display AI model options", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
const settingsContent = app.currentNoteSplitContent;
|
||||||
|
|
||||||
|
// Look for model selection elements
|
||||||
|
const modelSelects = settingsContent.locator('select').filter({ hasText: /model|gpt|claude|llama/i });
|
||||||
|
|
||||||
|
if (await modelSelects.count() > 0) {
|
||||||
|
const modelSelect = modelSelects.first();
|
||||||
|
await modelSelect.click();
|
||||||
|
|
||||||
|
// Verify model options are present
|
||||||
|
const options = modelSelect.locator('option');
|
||||||
|
const optionCount = await options.count();
|
||||||
|
expect(optionCount).toBeGreaterThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should save AI settings changes", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
const settingsContent = app.currentNoteSplitContent;
|
||||||
|
|
||||||
|
// Look for save button or auto-save indication
|
||||||
|
const saveButton = settingsContent.locator('button').filter({ hasText: /save|apply/i });
|
||||||
|
|
||||||
|
if (await saveButton.count() > 0) {
|
||||||
|
// Test save functionality
|
||||||
|
await saveButton.first().click();
|
||||||
|
|
||||||
|
// Wait for save to complete
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for success indication (toast, message, etc.)
|
||||||
|
const successMessage = page.locator('.toast, .notification, .success-message');
|
||||||
|
if (await successMessage.count() > 0) {
|
||||||
|
await expect(successMessage.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle invalid AI configuration", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
const settingsContent = app.currentNoteSplitContent;
|
||||||
|
|
||||||
|
// Look for API key input to test invalid configuration
|
||||||
|
const apiKeyInput = settingsContent.locator('input[type="password"], input[type="text"]').first();
|
||||||
|
|
||||||
|
if (await apiKeyInput.isVisible()) {
|
||||||
|
// Enter invalid API key
|
||||||
|
await apiKeyInput.fill("invalid-api-key-test");
|
||||||
|
|
||||||
|
// Look for test/validate button
|
||||||
|
const testButton = settingsContent.locator('button').filter({ hasText: /test|validate|check/i });
|
||||||
|
|
||||||
|
if (await testButton.count() > 0) {
|
||||||
|
await testButton.first().click();
|
||||||
|
|
||||||
|
// Wait for validation
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Look for error message
|
||||||
|
const errorMessage = page.locator('.error, .alert-danger, .text-danger');
|
||||||
|
if (await errorMessage.count() > 0) {
|
||||||
|
await expect(errorMessage.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the invalid input
|
||||||
|
await apiKeyInput.fill("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should navigate between AI setting sections", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
// Look for sub-sections or tabs in AI settings
|
||||||
|
const tabs = app.currentNoteSplitContent.locator('.nav-tabs a, .tab-header, .section-header');
|
||||||
|
|
||||||
|
if (await tabs.count() > 1) {
|
||||||
|
// Test navigation between sections
|
||||||
|
const firstTab = tabs.first();
|
||||||
|
const secondTab = tabs.nth(1);
|
||||||
|
|
||||||
|
await firstTab.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await secondTab.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify navigation worked by checking if content changed
|
||||||
|
await expect(app.currentNoteSplitContent).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display AI feature documentation", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Go to AI settings
|
||||||
|
await app.goToSettings();
|
||||||
|
await app.clickNoteOnNoteTreeByTitle("AI Settings");
|
||||||
|
|
||||||
|
const settingsContent = app.currentNoteSplitContent;
|
||||||
|
|
||||||
|
// Look for help or documentation links
|
||||||
|
const helpLinks = settingsContent.locator('a').filter({ hasText: /help|documentation|learn more|guide/i });
|
||||||
|
const helpButtons = settingsContent.locator('button, .help-icon, .info-icon').filter({ hasText: /\?|help|info/i });
|
||||||
|
|
||||||
|
if (await helpLinks.count() > 0) {
|
||||||
|
// Test help link accessibility
|
||||||
|
const firstHelpLink = helpLinks.first();
|
||||||
|
await expect(firstHelpLink).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await helpButtons.count() > 0) {
|
||||||
|
// Test help button functionality
|
||||||
|
const helpButton = helpButtons.first();
|
||||||
|
await helpButton.click();
|
||||||
|
|
||||||
|
// Wait for help content to appear
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for help modal or tooltip
|
||||||
|
const helpContent = page.locator('.modal, .tooltip, .popover, .help-content');
|
||||||
|
if (await helpContent.count() > 0) {
|
||||||
|
await expect(helpContent.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
377
apps/server-e2e/src/llm_chat.spec.ts
Normal file
377
apps/server-e2e/src/llm_chat.spec.ts
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
test.describe("LLM Chat Features", () => {
|
||||||
|
test("Should access LLM chat interface", async ({ page, context }) => {
|
||||||
|
page.setDefaultTimeout(15_000);
|
||||||
|
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Look for AI/LLM chat access points in the interface
|
||||||
|
// This could be a launcher button, menu item, or widget
|
||||||
|
const aiButtons = page.locator('[data-trigger-command*="ai"], [data-trigger-command*="llm"], [data-trigger-command*="chat"]');
|
||||||
|
const aiMenuItems = page.locator('a, button').filter({ hasText: /ai chat|llm|assistant|chat/i });
|
||||||
|
|
||||||
|
// Try the launcher bar first
|
||||||
|
const launcherAiButton = app.launcherBar.locator('.launcher-button').filter({ hasText: /ai|chat|assistant/i });
|
||||||
|
|
||||||
|
if (await launcherAiButton.count() > 0) {
|
||||||
|
await launcherAiButton.first().click();
|
||||||
|
|
||||||
|
// Wait for chat interface to load
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for chat interface elements
|
||||||
|
const chatInterface = page.locator('.llm-chat, .ai-chat, .chat-widget, .chat-panel');
|
||||||
|
if (await chatInterface.count() > 0) {
|
||||||
|
await expect(chatInterface.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
} else if (await aiButtons.count() > 0) {
|
||||||
|
await aiButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} else if (await aiMenuItems.count() > 0) {
|
||||||
|
await aiMenuItems.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify some form of AI/chat interface is accessible
|
||||||
|
const possibleChatElements = page.locator('.chat, .llm, .ai, [class*="chat"], [class*="llm"], [class*="ai"]');
|
||||||
|
const elementCount = await possibleChatElements.count();
|
||||||
|
|
||||||
|
// If no specific chat elements found, at least verify the page is responsive
|
||||||
|
if (elementCount === 0) {
|
||||||
|
await expect(app.currentNoteSplit).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should create new LLM chat session", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to trigger new chat creation
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Alternative: Look for chat creation buttons
|
||||||
|
const newChatButtons = page.locator('button, a').filter({ hasText: /new chat|create chat|start chat/i });
|
||||||
|
|
||||||
|
if (await newChatButtons.count() > 0) {
|
||||||
|
await newChatButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for chat input elements
|
||||||
|
const chatInputs = page.locator('textarea, input[type="text"]').filter({ hasText: /message|chat|type/i });
|
||||||
|
const possibleChatInputs = page.locator('textarea[placeholder*="message"], textarea[placeholder*="chat"], input[placeholder*="message"]');
|
||||||
|
|
||||||
|
if (await chatInputs.count() > 0) {
|
||||||
|
await expect(chatInputs.first()).toBeVisible();
|
||||||
|
} else if (await possibleChatInputs.count() > 0) {
|
||||||
|
await expect(possibleChatInputs.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle chat message input", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// If command doesn't exist, continue with alternative methods
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for message input areas
|
||||||
|
const messageInputs = page.locator('textarea, input[type="text"]');
|
||||||
|
const chatAreas = page.locator('[contenteditable="true"]');
|
||||||
|
|
||||||
|
// Try to find and interact with chat input
|
||||||
|
for (let i = 0; i < await messageInputs.count(); i++) {
|
||||||
|
const input = messageInputs.nth(i);
|
||||||
|
const placeholder = await input.getAttribute('placeholder') || '';
|
||||||
|
|
||||||
|
if (placeholder.toLowerCase().includes('message') ||
|
||||||
|
placeholder.toLowerCase().includes('chat') ||
|
||||||
|
placeholder.toLowerCase().includes('type')) {
|
||||||
|
|
||||||
|
// Test message input
|
||||||
|
await input.click();
|
||||||
|
await input.fill("Hello, this is a test message for the LLM chat.");
|
||||||
|
|
||||||
|
// Look for send button
|
||||||
|
const sendButtons = page.locator('button').filter({ hasText: /send|submit/i });
|
||||||
|
const enterHint = page.locator('.hint, .help-text').filter({ hasText: /enter|send/i });
|
||||||
|
|
||||||
|
if (await sendButtons.count() > 0) {
|
||||||
|
// Don't actually send to avoid API calls in tests
|
||||||
|
await expect(sendButtons.first()).toBeVisible();
|
||||||
|
} else if (await enterHint.count() > 0) {
|
||||||
|
// Test Enter key functionality indication
|
||||||
|
await expect(enterHint.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input
|
||||||
|
await input.fill("");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display chat history", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue with alternative access methods
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for chat history or previous conversations
|
||||||
|
const chatHistory = page.locator('.chat-history, .conversation-list, .message-list');
|
||||||
|
const previousChats = page.locator('.chat-item, .conversation-item');
|
||||||
|
|
||||||
|
if (await chatHistory.count() > 0) {
|
||||||
|
await expect(chatHistory.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await previousChats.count() > 0) {
|
||||||
|
// Test clicking on a previous chat
|
||||||
|
await previousChats.first().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Look for loaded conversation
|
||||||
|
const messages = page.locator('.message, .chat-message');
|
||||||
|
if (await messages.count() > 0) {
|
||||||
|
await expect(messages.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle chat settings and configuration", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for chat settings or configuration options
|
||||||
|
const settingsButtons = page.locator('button, a').filter({ hasText: /settings|config|options|preferences/i });
|
||||||
|
const gearIcons = page.locator('.fa-cog, .fa-gear, .bx-cog, .settings-icon');
|
||||||
|
|
||||||
|
if (await settingsButtons.count() > 0) {
|
||||||
|
await settingsButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for settings panel
|
||||||
|
const settingsPanel = page.locator('.settings-panel, .config-panel, .options-panel');
|
||||||
|
if (await settingsPanel.count() > 0) {
|
||||||
|
await expect(settingsPanel.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
} else if (await gearIcons.count() > 0) {
|
||||||
|
await gearIcons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for common chat settings
|
||||||
|
const temperatureSliders = page.locator('input[type="range"]');
|
||||||
|
const modelSelects = page.locator('select');
|
||||||
|
|
||||||
|
if (await temperatureSliders.count() > 0) {
|
||||||
|
// Test temperature adjustment
|
||||||
|
const slider = temperatureSliders.first();
|
||||||
|
await slider.click();
|
||||||
|
await expect(slider).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await modelSelects.count() > 0) {
|
||||||
|
// Test model selection
|
||||||
|
const select = modelSelects.first();
|
||||||
|
await select.click();
|
||||||
|
|
||||||
|
const options = select.locator('option');
|
||||||
|
if (await options.count() > 1) {
|
||||||
|
await expect(options.nth(1)).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle context and note integration", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Create or select a note first
|
||||||
|
await app.addNewTab();
|
||||||
|
|
||||||
|
// Try to access chat with note context
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChatWithContext");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Try alternative method
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error2) {
|
||||||
|
// Continue with UI-based approach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for context integration features
|
||||||
|
const contextButtons = page.locator('button, a').filter({ hasText: /context|include note|add note/i });
|
||||||
|
const atMentions = page.locator('[data-mention], .mention-button');
|
||||||
|
|
||||||
|
if (await contextButtons.count() > 0) {
|
||||||
|
await contextButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for note selection interface
|
||||||
|
const noteSelector = page.locator('.note-selector, .note-picker');
|
||||||
|
if (await noteSelector.count() > 0) {
|
||||||
|
await expect(noteSelector.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await atMentions.count() > 0) {
|
||||||
|
await atMentions.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should display AI provider status", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for AI provider status indicators
|
||||||
|
const statusIndicators = page.locator('.status-indicator, .connection-status, .provider-status');
|
||||||
|
const providerLabels = page.locator('.provider-name, .model-name');
|
||||||
|
const errorMessages = page.locator('.error-message, .alert').filter({ hasText: /api|provider|connection/i });
|
||||||
|
|
||||||
|
if (await statusIndicators.count() > 0) {
|
||||||
|
await expect(statusIndicators.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await providerLabels.count() > 0) {
|
||||||
|
const label = providerLabels.first();
|
||||||
|
await expect(label).toBeVisible();
|
||||||
|
|
||||||
|
// Verify it contains a known provider name
|
||||||
|
const text = await label.textContent();
|
||||||
|
const knownProviders = ['openai', 'anthropic', 'claude', 'gpt', 'ollama'];
|
||||||
|
const hasKnownProvider = knownProviders.some(provider =>
|
||||||
|
text?.toLowerCase().includes(provider)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Either has a known provider or at least some text
|
||||||
|
expect(text?.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await errorMessages.count() > 0) {
|
||||||
|
// If there are error messages, they should be visible
|
||||||
|
await expect(errorMessages.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle chat export and sharing", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for export or sharing features
|
||||||
|
const exportButtons = page.locator('button, a').filter({ hasText: /export|download|save|share/i });
|
||||||
|
const menuButtons = page.locator('.menu-button, .dropdown-toggle');
|
||||||
|
|
||||||
|
if (await exportButtons.count() > 0) {
|
||||||
|
await exportButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for export options
|
||||||
|
const exportOptions = page.locator('.export-options, .download-options');
|
||||||
|
if (await exportOptions.count() > 0) {
|
||||||
|
await expect(exportOptions.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await menuButtons.count() > 0) {
|
||||||
|
await menuButtons.first().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Look for menu items
|
||||||
|
const menuItems = page.locator('.dropdown-menu a, .menu-item');
|
||||||
|
if (await menuItems.count() > 0) {
|
||||||
|
const exportMenuItem = menuItems.filter({ hasText: /export|download|save/i });
|
||||||
|
if (await exportMenuItem.count() > 0) {
|
||||||
|
await expect(exportMenuItem.first()).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Should handle keyboard shortcuts in chat", async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Try to access chat interface
|
||||||
|
try {
|
||||||
|
await app.triggerCommand("openLlmChat");
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (error) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for message input to test keyboard shortcuts
|
||||||
|
const messageInputs = page.locator('textarea');
|
||||||
|
|
||||||
|
if (await messageInputs.count() > 0) {
|
||||||
|
const input = messageInputs.first();
|
||||||
|
await input.click();
|
||||||
|
|
||||||
|
// Test common keyboard shortcuts
|
||||||
|
// Ctrl+Enter or Enter for sending
|
||||||
|
await input.fill("Test message for keyboard shortcuts");
|
||||||
|
|
||||||
|
// Test Ctrl+A for select all
|
||||||
|
await input.press('Control+a');
|
||||||
|
|
||||||
|
// Test Escape for clearing/canceling
|
||||||
|
await input.press('Escape');
|
||||||
|
|
||||||
|
// Verify input is still functional
|
||||||
|
await expect(input).toBeVisible();
|
||||||
|
await expect(input).toBeFocused();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test global chat shortcuts
|
||||||
|
try {
|
||||||
|
await page.press('body', 'Control+Shift+l'); // Common LLM chat shortcut
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
} catch (error) {
|
||||||
|
// Shortcut might not exist, that's fine
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
355
apps/server/src/routes/api/llm.spec.ts
Normal file
355
apps/server/src/routes/api/llm.spec.ts
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import { Application } from "express";
|
||||||
|
import { beforeAll, describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
import supertest from "supertest";
|
||||||
|
import config from "../../services/config.js";
|
||||||
|
|
||||||
|
// Import the login utility from ETAPI tests
|
||||||
|
async function login(app: Application) {
|
||||||
|
// Obtain auth token.
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/etapi/auth/login")
|
||||||
|
.send({
|
||||||
|
"password": "demo1234"
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
const token = response.body.authToken;
|
||||||
|
expect(token).toBeTruthy();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
let app: Application;
|
||||||
|
|
||||||
|
describe("LLM API Tests", () => {
|
||||||
|
let token: string;
|
||||||
|
let createdChatId: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Enable authentication and use ETAPI auth (bypasses CSRF)
|
||||||
|
config.General.noAuthentication = false;
|
||||||
|
const buildApp = (await import("../../app.js")).default;
|
||||||
|
app = await buildApp();
|
||||||
|
token = await login(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Chat Session Management", () => {
|
||||||
|
it("should create a new chat session", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Test Chat Session",
|
||||||
|
systemPrompt: "You are a helpful assistant for testing.",
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 1000,
|
||||||
|
model: "gpt-3.5-turbo",
|
||||||
|
provider: "openai"
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
sessionId: expect.any(String),
|
||||||
|
title: "Test Chat Session",
|
||||||
|
createdAt: expect.any(String)
|
||||||
|
});
|
||||||
|
|
||||||
|
createdChatId = response.body.sessionId;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should list all chat sessions", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.get("/api/llm/chat")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('sessions');
|
||||||
|
expect(Array.isArray(response.body.sessions)).toBe(true);
|
||||||
|
|
||||||
|
if (response.body.sessions.length > 0) {
|
||||||
|
expect(response.body.sessions[0]).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
lastActive: expect.any(String),
|
||||||
|
messageCount: expect.any(Number)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retrieve a specific chat session", async () => {
|
||||||
|
if (!createdChatId) {
|
||||||
|
// Create a chat first if we don't have one
|
||||||
|
const createResponse = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Test Retrieval Chat"
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
createdChatId = createResponse.body.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await supertest(app)
|
||||||
|
.get(`/api/llm/chat/${createdChatId}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
id: createdChatId,
|
||||||
|
title: expect.any(String),
|
||||||
|
messages: expect.any(Array),
|
||||||
|
createdAt: expect.any(String)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update a chat session", async () => {
|
||||||
|
if (!createdChatId) {
|
||||||
|
// Create a chat first if we don't have one
|
||||||
|
const createResponse = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Test Update Chat"
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
createdChatId = createResponse.body.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await supertest(app)
|
||||||
|
.patch(`/api/llm/chat/${createdChatId}`)
|
||||||
|
.send({
|
||||||
|
title: "Updated Chat Title",
|
||||||
|
temperature: 0.8
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
id: createdChatId,
|
||||||
|
title: "Updated Chat Title",
|
||||||
|
updatedAt: expect.any(String)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 for non-existent chat session", async () => {
|
||||||
|
await supertest(app)
|
||||||
|
.get("/api/llm/chat/nonexistent-chat-id")
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Chat Messaging", () => {
|
||||||
|
let testChatId: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create a fresh chat for each test
|
||||||
|
const createResponse = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Message Test Chat"
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
testChatId = createResponse.body.sessionId;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle sending a message to a chat", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages`)
|
||||||
|
.send({
|
||||||
|
message: "Hello, how are you?",
|
||||||
|
options: {
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 100
|
||||||
|
},
|
||||||
|
includeContext: false,
|
||||||
|
useNoteContext: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// The response depends on whether AI is actually configured
|
||||||
|
// We should get either a successful response or an error about AI not being configured
|
||||||
|
expect([200, 400, 500]).toContain(response.status);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
response: expect.any(String),
|
||||||
|
sessionId: testChatId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// AI not configured is expected in test environment
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty message content", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages`)
|
||||||
|
.send({
|
||||||
|
message: "",
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([400, 500]).toContain(response.status);
|
||||||
|
expect(response.body).toHaveProperty('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle invalid chat ID for messaging", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat/invalid-chat-id/messages")
|
||||||
|
.send({
|
||||||
|
message: "Hello",
|
||||||
|
options: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([404, 500]).toContain(response.status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Chat Streaming", () => {
|
||||||
|
let testChatId: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create a fresh chat for each test
|
||||||
|
const createResponse = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Streaming Test Chat"
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
testChatId = createResponse.body.sessionId;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initiate streaming for a chat message", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages/stream`)
|
||||||
|
.send({
|
||||||
|
content: "Tell me a short story",
|
||||||
|
useAdvancedContext: false,
|
||||||
|
showThinking: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// The streaming endpoint should immediately return success
|
||||||
|
// indicating that streaming has been initiated
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
message: "Streaming initiated successfully"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty content for streaming", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages/stream`)
|
||||||
|
.send({
|
||||||
|
content: "",
|
||||||
|
useAdvancedContext: false,
|
||||||
|
showThinking: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
success: false,
|
||||||
|
error: "Content cannot be empty"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle whitespace-only content for streaming", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages/stream`)
|
||||||
|
.send({
|
||||||
|
content: " \n\t ",
|
||||||
|
useAdvancedContext: false,
|
||||||
|
showThinking: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
success: false,
|
||||||
|
error: "Content cannot be empty"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle invalid chat ID for streaming", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat/invalid-chat-id/messages/stream")
|
||||||
|
.send({
|
||||||
|
content: "Hello",
|
||||||
|
useAdvancedContext: false,
|
||||||
|
showThinking: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still return 200 for streaming initiation
|
||||||
|
// Errors would be communicated via WebSocket
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle streaming with note mentions", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post(`/api/llm/chat/${testChatId}/messages/stream`)
|
||||||
|
.send({
|
||||||
|
content: "Tell me about this note",
|
||||||
|
useAdvancedContext: true,
|
||||||
|
showThinking: true,
|
||||||
|
mentions: [
|
||||||
|
{
|
||||||
|
noteId: "root",
|
||||||
|
title: "Root Note"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
message: "Streaming initiated successfully"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
it("should handle malformed JSON in request body", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send('{ invalid json }');
|
||||||
|
|
||||||
|
expect([400, 500]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle missing required fields", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
// Missing required fields
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still work as title can be auto-generated
|
||||||
|
expect([200, 400, 500]).toContain(response.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle invalid parameter types", async () => {
|
||||||
|
const response = await supertest(app)
|
||||||
|
.post("/api/llm/chat")
|
||||||
|
.send({
|
||||||
|
title: "Test Chat",
|
||||||
|
temperature: "invalid", // Should be number
|
||||||
|
maxTokens: "also-invalid" // Should be number
|
||||||
|
});
|
||||||
|
|
||||||
|
// API should handle type conversion or validation
|
||||||
|
expect([200, 400, 500]).toContain(response.status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Clean up: delete any created chats
|
||||||
|
if (createdChatId) {
|
||||||
|
try {
|
||||||
|
await supertest(app)
|
||||||
|
.delete(`/api/llm/chat/${createdChatId}`);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user