From 217d41d680faee3987b3653890b43282752aaa58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B8rn=20Lindahl?= Date: Sun, 15 Feb 2026 23:57:48 +0100 Subject: [PATCH] test: strengthen Cypress e2e tests with real API assertions - Remove blanket uncaught:exception suppressor (API-only tests) - Trim smoke test to single infra-verification assertion - Rewrite health test with strict status/field assertions, no failOnStatusCode - Add session CRUD tests (create, get, list, delete, 404 cases, cleanup) - Use Cypress.env('API_URL') instead of baseUrl to avoid blocking smoke tests - Remove unused main and type fields from package.json --- cypress/e2e/api/health.cy.js | 39 ++++++-------- cypress/e2e/api/sessions.cy.js | 97 ++++++++++++++++++++++++++++++++++ cypress/e2e/smoke.cy.js | 51 ------------------ cypress/support/e2e.js | 7 +-- package.json | 2 - 5 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 cypress/e2e/api/sessions.cy.js diff --git a/cypress/e2e/api/health.cy.js b/cypress/e2e/api/health.cy.js index 318cd8d..e6d7e3a 100644 --- a/cypress/e2e/api/health.cy.js +++ b/cypress/e2e/api/health.cy.js @@ -1,30 +1,21 @@ /// -describe('Session Manager API', () => { - describe('Health Check', () => { - it('should respond to the health endpoint', () => { - cy.request({ - method: 'GET', - url: '/api/health', - failOnStatusCode: false, - }).then((response) => { - // The endpoint should exist and return a response - expect(response.status).to.be.oneOf([200, 503]); - expect(response.body).to.be.an('object'); - }); - }); - }); +const api = () => Cypress.env('API_URL'); - describe('Sessions API', () => { - it('should list sessions', () => { - cy.request({ - method: 'GET', - url: '/api/sessions', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.body).to.be.an('object'); - }); +describe('Health API', () => { + it('GET /api/health returns status and required fields', () => { + cy.request(`${api()}/api/health`).then((response) => { + expect(response.status).to.eq(200); + expect(response.body).to.have.property('status'); + expect(response.body.status).to.be.oneOf([ + 'healthy', + 'degraded', + 'unhealthy', + ]); + expect(response.body).to.have.property('docker'); + expect(response.body).to.have.property('active_sessions'); + expect(response.body).to.have.property('timestamp'); + expect(response.body).to.have.property('resource_limits'); }); }); }); diff --git a/cypress/e2e/api/sessions.cy.js b/cypress/e2e/api/sessions.cy.js new file mode 100644 index 0000000..9c6a06b --- /dev/null +++ b/cypress/e2e/api/sessions.cy.js @@ -0,0 +1,97 @@ +/// + +const api = () => Cypress.env('API_URL'); + +describe('Sessions API', () => { + const createdSessions = []; + + afterEach(() => { + createdSessions.splice(0).forEach((id) => { + cy.request({ + method: 'DELETE', + url: `${api()}/api/sessions/${id}`, + failOnStatusCode: false, + }); + }); + }); + + describe('GET /api/sessions', () => { + it('returns 200 with an array', () => { + cy.request(`${api()}/api/sessions`).then((response) => { + expect(response.status).to.eq(200); + expect(response.body).to.be.an('array'); + }); + }); + }); + + describe('POST /api/sessions', () => { + it('creates a session with expected fields', () => { + cy.request('POST', `${api()}/api/sessions`).then((response) => { + expect(response.status).to.be.oneOf([200, 201]); + expect(response.body).to.have.property('session_id'); + expect(response.body).to.have.property('auth_token'); + expect(response.body).to.have.property('status'); + createdSessions.push(response.body.session_id); + }); + }); + }); + + describe('GET /api/sessions/:id', () => { + it('returns the created session', () => { + cy.request('POST', `${api()}/api/sessions`).then((createRes) => { + createdSessions.push(createRes.body.session_id); + const id = createRes.body.session_id; + + cy.request(`${api()}/api/sessions/${id}`).then((response) => { + expect(response.status).to.eq(200); + expect(response.body).to.have.property('session_id', id); + }); + }); + }); + + it('returns 404 for nonexistent session', () => { + cy.request({ + url: `${api()}/api/sessions/nonexistent-id-000`, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(404); + }); + }); + }); + + describe('DELETE /api/sessions/:id', () => { + it('deletes a session', () => { + cy.request('POST', `${api()}/api/sessions`).then((createRes) => { + const id = createRes.body.session_id; + + cy.request('DELETE', `${api()}/api/sessions/${id}`).then( + (response) => { + expect(response.status).to.eq(200); + expect(response.body).to.have.property('message'); + } + ); + }); + }); + + it('returns 404 for nonexistent session', () => { + cy.request({ + method: 'DELETE', + url: `${api()}/api/sessions/nonexistent-id-000`, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(404); + }); + }); + }); + + describe('POST /api/cleanup', () => { + it('returns 200 with cleanup message', () => { + cy.request('POST', `${api()}/api/cleanup`).then((response) => { + expect(response.status).to.eq(200); + expect(response.body) + .to.have.property('message') + .that.includes('Cleanup completed'); + }); + }); + }); +}); diff --git a/cypress/e2e/smoke.cy.js b/cypress/e2e/smoke.cy.js index d756238..6ed9f48 100644 --- a/cypress/e2e/smoke.cy.js +++ b/cypress/e2e/smoke.cy.js @@ -1,58 +1,7 @@ /// -/** - * Smoke test to verify that the Cypress test infrastructure is - * correctly installed and configured. These tests do NOT require - * the lovdata-chat stack to be running. - */ describe('Cypress Infrastructure Smoke Test', () => { it('should execute a basic assertion', () => { expect(true).to.be.true; }); - - it('should handle arrays and objects', () => { - const session = { - id: 'test-session-001', - status: 'running', - model: 'opencode/kimi-k2.5-free', - }; - - expect(session).to.have.property('id'); - expect(session.status).to.eq('running'); - expect(session.model).to.include('kimi'); - }); - - it('should validate the allowed models whitelist structure', () => { - const allowedModels = [ - { - id: 'opencode/kimi-k2.5-free', - display_name: 'Kimi K2.5', - provider: 'zen', - is_reasoning: false, - is_default: true, - }, - { - id: 'anthropic/claude-sonnet-4-thinking', - display_name: 'Claude Sonnet 4 (Thinking)', - provider: 'zen', - is_reasoning: true, - thinking_budget_default: 10000, - thinking_budget_max: 50000, - }, - ]; - - expect(allowedModels).to.have.length(2); - - const defaultModel = allowedModels.find((m) => m.is_default); - expect(defaultModel).to.exist; - expect(defaultModel.id).to.eq('opencode/kimi-k2.5-free'); - - const reasoningModels = allowedModels.filter((m) => m.is_reasoning); - expect(reasoningModels).to.have.length(1); - expect(reasoningModels[0]).to.have.property('thinking_budget_default'); - expect(reasoningModels[0]).to.have.property('thinking_budget_max'); - expect(reasoningModels[0].thinking_budget_max).to.be.greaterThan( - reasoningModels[0].thinking_budget_default - ); - }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 178d479..ef408a2 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,5 +1,6 @@ // Cypress E2E support file -// Add custom commands and global hooks here -// Prevent Cypress from failing on uncaught exceptions from the app -Cypress.on('uncaught:exception', () => false); +// Base URL for API tests. Override with CYPRESS_API_URL env var. +// Not set as Cypress baseUrl to avoid server-reachability checks +// that would block offline tests (smoke). +Cypress.env('API_URL', Cypress.env('API_URL') || 'http://localhost'); diff --git a/package.json b/package.json index 2ec7d56..b612e6e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "lovdata-chat", "version": "1.0.0", "description": "A web-based chat interface that allows users to interact with Large Language Models (LLMs) equipped with Norwegian legal research tools from the Lovdata MCP server.", - "main": "index.js", "directories": { "doc": "docs" }, @@ -18,7 +17,6 @@ "keywords": [], "author": "", "license": "ISC", - "type": "commonjs", "devDependencies": { "cypress": "^15.10.0" }