diff --git a/archon-ui-main/package-lock.json b/archon-ui-main/package-lock.json index 831b1a928c..7986b794b9 100644 --- a/archon-ui-main/package-lock.json +++ b/archon-ui-main/package-lock.json @@ -26,6 +26,7 @@ "react-router-dom": "^6.26.2", "socket.io-client": "^4.8.1", "tailwind-merge": "latest", + "uuid": "^11.1.0", "zod": "^3.25.46" }, "devDependencies": { @@ -35,6 +36,7 @@ "@types/node": "^20.19.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.1", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", "@vitejs/plugin-react": "^4.2.1", @@ -2977,6 +2979,13 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -10025,6 +10034,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/archon-ui-main/package.json b/archon-ui-main/package.json index fc6a1d1a60..8e70ed9c90 100644 --- a/archon-ui-main/package.json +++ b/archon-ui-main/package.json @@ -8,7 +8,7 @@ "build": "npx vite build", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "preview": "npx vite preview", - "test": "vitest", + "test": "vitest run --coverage", "test:ui": "vitest --ui", "test:coverage": "npm run test:coverage:run && npm run test:coverage:summary", "test:coverage:run": "vitest run --coverage --reporter=dot --reporter=json", @@ -36,6 +36,7 @@ "react-router-dom": "^6.26.2", "socket.io-client": "^4.8.1", "tailwind-merge": "latest", + "uuid": "^11.1.0", "zod": "^3.25.46" }, "devDependencies": { @@ -45,6 +46,7 @@ "@types/node": "^20.19.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.1", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", "@vitejs/plugin-react": "^4.2.1", diff --git a/archon-ui-main/src/components/ui/TestResultDashboard.tsx b/archon-ui-main/src/components/ui/TestResultDashboard.tsx index a804a83c10..abfb279925 100644 --- a/archon-ui-main/src/components/ui/TestResultDashboard.tsx +++ b/archon-ui-main/src/components/ui/TestResultDashboard.tsx @@ -72,7 +72,9 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) ); } - if (!results) { + const summary = results?.summary; + + if (!summary) { return (
@@ -88,11 +90,18 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) ); } - const { summary } = results; - const successRate = summary.total > 0 ? (summary.passed / summary.total) * 100 : 0; + const safeSummary = { + total: summary.total || 0, + passed: summary.passed || 0, + failed: summary.failed || 0, + skipped: summary.skipped || 0, + duration: summary.duration || 0, + }; + + const successRate = safeSummary.total > 0 ? (safeSummary.passed / safeSummary.total) * 100 : 0; const getHealthStatus = () => { - if (summary.failed === 0 && summary.passed > 0) return { text: 'All Tests Passing', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' }; + if (safeSummary.failed === 0 && safeSummary.passed > 0) return { text: 'All Tests Passing', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' }; if (successRate >= 80) return { text: 'Mostly Passing', color: 'text-yellow-600 dark:text-yellow-400', bg: 'bg-yellow-50 dark:bg-yellow-900/20' }; return { text: 'Tests Failing', color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-900/20' }; }; @@ -127,7 +136,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) className="text-center p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg" >
- {summary.total} + {safeSummary.total}
Total Tests
@@ -140,7 +149,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) >
- {summary.passed} + {safeSummary.passed}
Passed
@@ -153,7 +162,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) >
- {summary.failed} + {safeSummary.failed}
Failed
@@ -166,7 +175,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) >
- {summary.skipped} + {safeSummary.skipped}
Skipped
@@ -195,7 +204,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading })
- Duration: {(summary.duration / 1000).toFixed(2)}s + Duration: {(safeSummary.duration / 1000).toFixed(2)}s
{results.timestamp && (
@@ -206,7 +215,7 @@ const TestSummaryCard: React.FC = ({ results, isLoading })
{/* Failed Tests Alert */} - {summary.failed > 0 && ( + {safeSummary.failed > 0 && ( = ({ results, isLoading })
- {summary.failed} test{summary.failed > 1 ? 's' : ''} failing - review errors below + {safeSummary.failed} test{safeSummary.failed > 1 ? 's' : ''} failing - review errors below
@@ -225,7 +234,10 @@ const TestSummaryCard: React.FC = ({ results, isLoading }) }; const FailedTestsList: React.FC<{ results: TestResults }> = ({ results }) => { - const failedSuites = results.suites.filter(suite => suite.failed > 0); + if (!results || !Array.isArray(results.suites) || !results.summary) { + return null; + } + const failedSuites = results.suites.filter(suite => suite && typeof suite.failed === 'number' && suite.failed > 0); if (failedSuites.length === 0) { return null; @@ -240,7 +252,7 @@ const FailedTestsList: React.FC<{ results: TestResults }> = ({ results }) => {

- Failed Tests ({results.summary.failed}) + Failed Tests ({results?.summary?.failed || 0})

@@ -381,9 +393,9 @@ export const TestResultDashboard: React.FC = ({ )} {/* Main content */} -
+
{/* Test Summary */} -
+
@@ -400,7 +412,7 @@ export const TestResultDashboard: React.FC = ({
{/* Failed Tests */} - {results && results.summary.failed > 0 && ( + {results?.summary?.failed > 0 && results?.suites && ( )}
diff --git a/archon-ui-main/src/config/api.ts b/archon-ui-main/src/config/api.ts index ec3eb2510c..db60df39f3 100644 --- a/archon-ui-main/src/config/api.ts +++ b/archon-ui-main/src/config/api.ts @@ -7,27 +7,28 @@ // Get the API URL from environment or construct it export function getApiUrl(): string { - // Check if VITE_API_URL is provided (set by docker-compose) + // 1. Priority: VITE_API_URL from environment (e.g., Docker) if (import.meta.env.VITE_API_URL) { - return import.meta.env.VITE_API_URL; + return import.meta.env.VITE_API_URL } - // For relative URLs in production (goes through proxy) + // 2. Production mode: Use relative path if (import.meta.env.PROD) { - return ''; + return '' } - // For development, construct from window location - const protocol = window.location.protocol; - const host = window.location.hostname; - // Use configured port or default to 8181 - const port = import.meta.env.VITE_ARCHON_SERVER_PORT || '8181'; - - if (!import.meta.env.VITE_ARCHON_SERVER_PORT) { - console.info('[Archon] Using default ARCHON_SERVER_PORT: 8181'); - } - - return `${protocol}//${host}:${port}`; + // 3. Development mode: Construct URL from port + const port = import.meta.env.ARCHON_SERVER_PORT + if (!port) { + throw new Error( + 'ARCHON_SERVER_PORT environment variable is required. ' + + 'Please set it in your .env file. ' + + 'Default value: 8181', + ) + + const protocol = window.location.protocol + const hostname = window.location.hostname + return `${protocol}//${hostname}:${port}` } // Get the base path for API endpoints diff --git a/archon-ui-main/src/services/mcpClientService.ts b/archon-ui-main/src/services/mcpClientService.ts index 2010c9bfec..1c73d6683a 100644 --- a/archon-ui-main/src/services/mcpClientService.ts +++ b/archon-ui-main/src/services/mcpClientService.ts @@ -89,13 +89,11 @@ const MCPToolSchema = z.object({ export type MCPTool = z.infer; export type MCPParameter = z.infer; -import { getApiUrl } from '../config/api'; - /** * MCP Client Service - Universal MCP client that connects to any MCP servers * This service communicates with the standalone Python MCP client service */ -class MCPClientService { +export class MCPClientService { private baseUrl = getApiUrl(); // ======================================== diff --git a/archon-ui-main/src/services/testService.ts b/archon-ui-main/src/services/testService.ts index cf4b15d1c4..247130bccf 100644 --- a/archon-ui-main/src/services/testService.ts +++ b/archon-ui-main/src/services/testService.ts @@ -40,6 +40,7 @@ export interface TestStatus { } import { getApiUrl, getWebSocketUrl } from '../config/api'; +import { v4 as uuidv4 } from 'uuid'; // Use unified API configuration const API_BASE_URL = getApiUrl(); @@ -159,7 +160,7 @@ class TestService { onError?: (error: Error) => void, onComplete?: () => void ): Promise { - const execution_id = crypto.randomUUID(); + const execution_id = uuidv4(); try { // Send initial status @@ -247,44 +248,47 @@ class TestService { * Get coverage data for Test Results Modal from new API endpoints with fallback */ async getCoverageData(): Promise { - try { - // Try new API endpoint first - const response = await callAPI('/api/coverage/combined-summary'); - return response; - } catch (apiError) { - // Fallback to static files for backward compatibility - try { - const response = await fetch('/test-results/coverage/coverage-summary.json'); - if (!response.ok) { - throw new Error('Coverage data not available'); - } - return await response.json(); - } catch (staticError) { - throw new Error(`Failed to load coverage data: ${apiError instanceof Error ? apiError.message : 'API and static files unavailable'}`); - } + const response = await fetch('/test-results/coverage/coverage-summary.json'); + if (!response.ok) { + throw new Error('Coverage data not available'); } + return await response.json(); } /** * Get test results for Test Results Modal from new API endpoints with fallback */ async getTestResults(): Promise { - try { - // Try new API endpoint first - const response = await callAPI('/api/tests/latest-results'); - return response; - } catch (apiError) { - // Fallback to static files for backward compatibility - try { - const response = await fetch('/test-results/test-results.json'); - if (!response.ok) { - throw new Error('Test results not available'); - } - return await response.json(); - } catch (staticError) { - throw new Error(`Failed to load test results: ${apiError instanceof Error ? apiError.message : 'API and static files unavailable'}`); - } + const response = await fetch('/test-results/test-results.json'); + if (!response.ok) { + throw new Error('Test results not available'); } + const data = await response.json(); + + // Transform the data to match the expected TestResults interface + const transformedData = { + summary: { + total: data.numTotalTests || 0, + passed: data.numPassedTests || 0, + failed: data.numFailedTests || 0, + skipped: data.numPendingTests || 0, + duration: data.testResults.reduce((acc: number, suite: any) => acc + (suite.endTime - suite.startTime), 0), + }, + suites: data.testResults.map((suite: any) => ({ + name: suite.name, + tests: suite.assertionResults.length, + passed: suite.assertionResults.filter((r: any) => r.status === 'passed').length, + failed: suite.assertionResults.filter((r: any) => r.status === 'failed').length, + skipped: suite.assertionResults.filter((r: any) => r.status === 'pending' || r.status === 'todo').length, + duration: suite.endTime - suite.startTime, + failedTests: suite.assertionResults + .filter((r: any) => r.status === 'failed') + .map((r: any) => ({ name: r.title, error: r.failureMessages.join('\\n') })) + })), + timestamp: new Date(data.startTime).toISOString() + }; + + return transformedData; } /** diff --git a/archon-ui-main/src/utils/onboarding.ts b/archon-ui-main/src/utils/onboarding.ts index 743566f2b5..93f082cc0b 100644 --- a/archon-ui-main/src/utils/onboarding.ts +++ b/archon-ui-main/src/utils/onboarding.ts @@ -41,10 +41,11 @@ export function isLmConfigured( // Helper function to check if a credential has a valid value const hasValidCredential = (cred: NormalizedCredential | undefined): boolean => { if (!cred) return false; - return !!( - (cred.value && cred.value !== 'null' && cred.value !== null && cred.value.trim() !== '') || - (cred.is_encrypted && cred.encrypted_value && cred.encrypted_value !== 'null' && cred.encrypted_value !== null) - ); + // If is_encrypted is true, we consider it valid even without a value, + // as the value is stored on the server. + if (cred.is_encrypted) return true; + + return !!(cred.value && cred.value !== 'null' && cred.value !== null && cred.value.trim() !== ''); }; // Find API keys diff --git a/archon-ui-main/test/config/api.test.ts b/archon-ui-main/test/config/api.test.ts index bbf58f5b92..9084c051ac 100644 --- a/archon-ui-main/test/config/api.test.ts +++ b/archon-ui-main/test/config/api.test.ts @@ -49,18 +49,11 @@ describe('API Configuration', () => { delete (import.meta.env as any).VITE_PORT; delete (import.meta.env as any).ARCHON_SERVER_PORT; - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - hostname: 'localhost' - }, - writable: true - }); - - const { getApiUrl } = await import('../../src/config/api'); - - expect(getApiUrl()).toBe('http://localhost:8181'); + // The import() promise should reject because a top-level statement calls getApiUrl() + // which will throw an error when ARCHON_SERVER_PORT is not set. + await expect(import('../../src/config/api')).rejects.toThrow( + /ARCHON_SERVER_PORT environment variable is required.*Default value: 8181/ + ); }); it('should use VITE_ARCHON_SERVER_PORT when set in development', async () => { diff --git a/archon-ui-main/test/errors.test.tsx b/archon-ui-main/test/errors.test.tsx index 3971f4af7f..3d955331a7 100644 --- a/archon-ui-main/test/errors.test.tsx +++ b/archon-ui-main/test/errors.test.tsx @@ -1,5 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, act } from '@testing-library/react' +import { describe, test, expect, vi } from 'vitest' import React from 'react' import { credentialsService } from '../src/services/credentialsService' @@ -36,7 +36,7 @@ describe('Error Handling Tests', () => { expect(screen.getByRole('alert')).toHaveTextContent('Failed to load data') }) - test('timeout error simulation', () => { + test('timeout error simulation', async () => { const MockTimeoutComponent = () => { const [status, setStatus] = React.useState('idle') @@ -61,10 +61,8 @@ describe('Error Handling Tests', () => { fireEvent.click(screen.getByText('Start Request')) expect(screen.getByText('Loading...')).toBeInTheDocument() - // Wait for timeout - setTimeout(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Request timed out') - }, 150) + const alert = await screen.findByRole('alert', {}, { timeout: 500 }) + expect(alert).toHaveTextContent('Request timed out') }) test('form validation errors', () => { diff --git a/archon-ui-main/vite.config.ts b/archon-ui-main/vite.config.ts index f6b7563f22..4256ccbceb 100644 --- a/archon-ui-main/vite.config.ts +++ b/archon-ui-main/vite.config.ts @@ -145,43 +145,34 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { mkdirSync(testResultsDir, { recursive: true }); } - const testProcess = exec('npm run test:coverage:stream', { + const testProcess = exec('npx vitest run --coverage --reporter=dot --reporter=json', { cwd: process.cwd(), - env: { - ...process.env, - FORCE_COLOR: '1', + env: { + ...process.env, + FORCE_COLOR: '1', CI: 'true', - NODE_ENV: 'test' - } // Enable color output and CI mode for cleaner output + NODE_ENV: 'test', + } }); - testProcess.stdout?.on('data', (data) => { - const text = data.toString(); - // Split by newlines but preserve empty lines for better formatting - const lines = text.split('\n'); - - lines.forEach((line: string) => { - // Strip ANSI escape codes to get clean text - const cleanLine = line.replace(/\\x1b\[[0-9;]*m/g, ''); + const handleStream = (stream: any, res: any) => { + stream.on('data', (data: any) => { + const text = data.toString(); + const lines = text.split('\n'); - // Send all lines for verbose reporter output - res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`); - }); - - // Flush the response to ensure immediate delivery - if (res.flushHeaders) { - res.flushHeaders(); - } - }); + lines.forEach((line: string) => { + const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ''); + res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`); + }); - testProcess.stderr?.on('data', (data) => { - const lines = data.toString().split('\n').filter((line: string) => line.trim()); - lines.forEach((line: string) => { - // Strip ANSI escape codes - const cleanLine = line.replace(/\\x1b\[[0-9;]*m/g, ''); - res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`); + if (res.flushHeaders) { + res.flushHeaders(); + } }); - }); + }; + + handleStream(testProcess.stdout, res); + handleStream(testProcess.stderr, res); testProcess.on('close', (code) => { res.write(`data: ${JSON.stringify({ diff --git a/archon-ui-main/vitest.config.ts b/archon-ui-main/vitest.config.ts index 7677c9c05a..7c3b6a284f 100644 --- a/archon-ui-main/vitest.config.ts +++ b/archon-ui-main/vitest.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ outputFile: { json: './public/test-results/test-results.json' }, + bail: 0, testTimeout: 10000, // 10 seconds timeout hookTimeout: 10000, // 10 seconds for setup/teardown coverage: { @@ -63,4 +64,4 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, -}) \ No newline at end of file +})