From 22ce90488c35ffc5d7e0ac17cd8207759c2d4d1c Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Fri, 14 Nov 2025 15:09:54 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(e2e)=20add=20first=20chat=20end-to-en?= =?UTF-8?q?d=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a very simple test, but it paves the way for more of them. --- Makefile | 4 +- compose-e2e.yml | 19 +++++ env.d/development/common.e2e.dist | 3 + src/OpenMockLLM/Dockerfile | 19 +++++ src/OpenMockLLM/README.md | 19 +++++ .../configuration/llm/default.e2e.json | 43 +++++++++++ .../__tests__/app-conversations/chat.spec.ts | 76 +++++++++++++++++++ .../__tests__/app-conversations/home.spec.ts | 8 +- 8 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 src/OpenMockLLM/Dockerfile create mode 100644 src/OpenMockLLM/README.md create mode 100644 src/backend/conversations/configuration/llm/default.e2e.json create mode 100644 src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts diff --git a/Makefile b/Makefile index 4bcf9bc..d2bbd32 100644 --- a/Makefile +++ b/Makefile @@ -126,7 +126,7 @@ build-frontend: ## build the frontend container build-e2e: cache ?= build-e2e: ## build the e2e container @$(MAKE) build-backend cache=$(cache) - @$(COMPOSE_E2E) build frontend $(cache) + @$(COMPOSE_E2E) build frontend openmockllm-mistral $(cache) .PHONY: build-e2e down: ## stop and remove containers, networks, images, and volumes @@ -158,7 +158,7 @@ create-compose-with-models: ## override the docker-compose file with models run-e2e: ## start the e2e server run-e2e: @$(MAKE) run-backend - @$(COMPOSE_E2E) up --force-recreate -d frontend + @$(COMPOSE_E2E) up --force-recreate -d frontend openmockllm-mistral .PHONY: run-e2e status: ## an alias for "docker compose ps" diff --git a/compose-e2e.yml b/compose-e2e.yml index f73e645..311d0ff 100644 --- a/compose-e2e.yml +++ b/compose-e2e.yml @@ -11,3 +11,22 @@ services: image: conversations:frontend-production ports: - "3000:3000" + + openmockllm-mistral: + user: "${DOCKER_USER:-1000}" + build: + context: . + dockerfile: ./src/OpenMockLLM/Dockerfile + image: conversations:openmockllm-mistral + command: + - openmockllm + - --host + - "0.0.0.0" + - --port + - "8000" + - --backend + - mistral + - --model-name + - mistral-mock + ports: + - "8900:8000" diff --git a/env.d/development/common.e2e.dist b/env.d/development/common.e2e.dist index aee08c2..091cb17 100644 --- a/env.d/development/common.e2e.dist +++ b/env.d/development/common.e2e.dist @@ -2,6 +2,9 @@ BURST_THROTTLE_RATES="200/minute" SUSTAINED_THROTTLE_RATES="200/hour" +# LLM +LLM_CONFIGURATION_FILE_PATH = /app/conversations/configuration/llm/default.e2e.json + # Features FEATURE_FLAG_WEB_SEARCH=ENABLED FEATURE_FLAG_DOCUMENT_UPLOAD=ENABLED diff --git a/src/OpenMockLLM/Dockerfile b/src/OpenMockLLM/Dockerfile new file mode 100644 index 0000000..550a422 --- /dev/null +++ b/src/OpenMockLLM/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.13.3-alpine + +# Upgrade pip to its latest release to speed up dependencies installation +RUN python -m pip install --upgrade pip setuptools lorem-text + +# Upgrade system packages to install security updates +RUN apk update && \ + apk upgrade + +RUN apk add --no-cache git + +# Install the package +RUN pip install git+https://github.com/etalab-ia/openmockllm.git + +# Expose the default port +EXPOSE 8000 + +# Set default command +CMD ["openmockllm", "--host", "0.0.0.0", "--port", "8000"] diff --git a/src/OpenMockLLM/README.md b/src/OpenMockLLM/README.md new file mode 100644 index 0000000..1ecb99b --- /dev/null +++ b/src/OpenMockLLM/README.md @@ -0,0 +1,19 @@ +[OpenMockLLM](https://github.com/etalab-ia/OpenMockLLM) is a FastAPI-based mock LLM API server that simulates +several Large Language Model API providers. + +This is a simple docker image to run the server for testing and development purposes (E2E tests mainly). + +It's a bit overkill to have a dedicated image for that, but it allows simple E2E stack with docker-compose since +our code is also run in Docker containers. + +## Build and Run manually + +```bash +docker build -t openmockllm . +docker run -p 8000:8000 openmockllm +``` + +## Next steps + +- Add more chat completion behaviors (specific text streaming, function calling, etc.) +- Pin a specific OpenMockLLM version in the Dockerfile diff --git a/src/backend/conversations/configuration/llm/default.e2e.json b/src/backend/conversations/configuration/llm/default.e2e.json new file mode 100644 index 0000000..a5998e0 --- /dev/null +++ b/src/backend/conversations/configuration/llm/default.e2e.json @@ -0,0 +1,43 @@ +{ + "models": [ + { + "hrid": "default-model", + "model_name": "mistral-mock", + "human_readable_name": "Default Model", + "provider_name": "default-provider", + "profile": null, + "settings": {}, + "is_active": true, + "icon": [ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAn1BMVEUALosAKoovTZjw8vb////+9/jlPUniAAz", + "iABUAGIWbpsTwq7HhAAAAI4dle7DrdX4AJohRaaboXWj7+/zn6On5//9NZaT29vfoWmVHYKDoUl/k5OUAIYddc6vpbHYCM47Y3+v53+LiFCUA", + "HIWnsckYPJHi6PL77O7jJjW3wdf1w8jre4QgQ5TZ2txwg7Pr3+I8WZ6OnsTuoamClL7tlZ5xz5y8AAAAzUlEQVR4AZ3RRQKDQBBEUSTu7h5c4", + "vc/W6Yp3KG2Dz4ynDdeEBvOmq12xx2E1u0B+4NOEocj4DgNJ1PgLAvni8WyBq5Yc71ubFJx23C2q4P7dRYejg1xzvCUgvz5guz11k7gXYKF/1", + "8oyiYuvHAYeVkhXCzolVStHcGDjiQzNmMQxsMI5rEJRdQSPZvbpE2E8aY6gC6Z+2Hg4dFA0Yb4YedNL/v4Fk8WJuwiGhrChJNXI210rnib9Fs", + "JlXRUC/HwTscPIXf/iklq/tjb/gHAdxkCUjAg2QAAAABJRU5ErkJggg==" + ], + "system_prompt": "You are a helpful AI assistant.", + "tools": [] + }, + { + "hrid": "default-summarization-model", + "model_name": "mistral-mock", + "human_readable_name": "Default Summarization Model", + "provider_name": "default-provider", + "profile": null, + "settings": {}, + "is_active": true, + "icon": null, + "system_prompt": "You are a helpful AI assistant specialized in summarization.", + "tools": [] + } + ], + "providers": [ + { + "hrid": "default-provider", + "base_url": "http://host.docker.internal:8900", + "api_key": "openmockllm-api-key", + "kind": "mistral" + } + ] +} diff --git a/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts b/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts new file mode 100644 index 0000000..2a30188 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-conversations/chat.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/home/'); +}); + +test.describe('Chat page', () => { + test('it checks the page is displayed properly', async ({ page }) => { + await page.goto('/'); + + const newChatButton = page.getByRole('button', { name: 'New chat' }); + await expect(newChatButton).toBeVisible(); + + const chatInput = page.getByRole('textbox', { + name: 'Enter your message or a', + }); + await expect(chatInput).toBeVisible(); + + const attachmentButton = page.getByRole('button', { + name: 'Add attach file', + }); + await expect(attachmentButton).toBeVisible(); + + const websearchButton = page.getByRole('button', { + name: 'Research on the web', + }); + await expect(websearchButton).toBeVisible(); + + const sendMessageButton = page.getByRole('button', { name: 'Send' }); + await expect(sendMessageButton).toBeVisible(); + }); + + test('the user can chat with LLM (simple)', async ({ page }) => { + await page.goto('/'); + + const newChatButton = page.getByRole('button', { name: 'New chat' }); + await expect(newChatButton).toBeVisible(); + + const chatInput = page.getByRole('textbox', { + name: 'Enter your message or a', + }); + await chatInput.click(); + await chatInput.fill('Hello, how are you?'); + + const sendMessageButton = page.getByRole('button', { name: 'Send' }); + await expect(sendMessageButton).toBeEnabled(); + + await page.keyboard.press('Enter'); + + // Wait for the response to appear + await page + .getByRole('button', { name: 'See more' }) + .waitFor({ timeout: 10000 }); + + const copyButton = page.getByRole('button', { name: 'Copy' }); + await expect(copyButton).toBeVisible(); + + const messageContent = page.getByText('Lorem ipsum dolor sit amet'); + await expect(messageContent).toBeVisible(); + + // Check history + const chatHistoryLink = page + .getByRole('link', { name: 'Simple chat icon Hello, how' }) + .first(); + await expect(chatHistoryLink).toBeVisible(); + + await newChatButton.click(); + + await page + .getByRole('heading', { name: 'What is on your mind?' }) + .isVisible(); + + await chatHistoryLink.click(); + await expect(messageContent).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-conversations/home.spec.ts b/src/frontend/apps/e2e/__tests__/app-conversations/home.spec.ts index d2025f1..720cc8c 100644 --- a/src/frontend/apps/e2e/__tests__/app-conversations/home.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-conversations/home.spec.ts @@ -24,9 +24,7 @@ test.describe('Home page', () => { // Check the titles const h2 = page.locator('h2'); - await expect( - h2.getByText('Your sovereign AI assistant'), - ).toBeVisible(); + await expect(h2.getByText('Your sovereign AI assistant')).toBeVisible(); await expect(footer).toBeVisible(); }); @@ -74,9 +72,7 @@ test.describe('Home page', () => { // Check the titles const h2 = page.locator('h2'); - await expect( - h2.getByText('Your sovereign AI assistant'), - ).toBeVisible(); + await expect(h2.getByText('Your sovereign AI assistant')).toBeVisible(); await expect(footer).toBeVisible(); });