feat: add integration test framework and health check improvements
- Add test helper library with assertion functions (scripts/lib/test-helpers.sh) - Add gateway integration test script (scripts/tests/gateway-test.sh) - Add configuration validation tool (scripts/validate-config.ts) - Add health-check.ts library with Tauri command wrappers - Add HealthStatusIndicator component to ConnectionStatus.tsx - Add E2E test specs for memory, settings, and team collaboration - Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state Key improvements: - Store architecture now properly documented as migrated - Tauri backend shown as 85-90% complete - Component integration status clarified Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,21 @@
|
||||
*
|
||||
* Displays the current Gateway connection status with visual indicators.
|
||||
* Supports automatic reconnect and manual reconnect button.
|
||||
* Includes health status indicator for OpenFang backend.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Wifi, WifiOff, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { useGatewayStore } from '../store/gatewayStore';
|
||||
import { Wifi, WifiOff, Loader2, RefreshCw, Heart, HeartPulse } from 'lucide-react';
|
||||
import { useConnectionStore } from '../store/connectionStore';
|
||||
import { getGatewayClient } from '../lib/gateway-client';
|
||||
import {
|
||||
createHealthCheckScheduler,
|
||||
getHealthStatusLabel,
|
||||
formatHealthCheckTime,
|
||||
type HealthCheckResult,
|
||||
type HealthStatus,
|
||||
} from '../lib/health-check';
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
/** Show compact version (just icon and status text) */
|
||||
@@ -75,7 +83,8 @@ export function ConnectionStatus({
|
||||
showReconnectButton = true,
|
||||
className = '',
|
||||
}: ConnectionStatusProps) {
|
||||
const { connectionState, connect } = useGatewayStore();
|
||||
const connectionState = useConnectionStore((s) => s.connectionState);
|
||||
const connect = useConnectionStore((s) => s.connect);
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [reconnectInfo, setReconnectInfo] = useState<ReconnectInfo | null>(null);
|
||||
|
||||
@@ -188,7 +197,7 @@ export function ConnectionStatus({
|
||||
* ConnectionIndicator - Minimal connection indicator for headers
|
||||
*/
|
||||
export function ConnectionIndicator({ className = '' }: { className?: string }) {
|
||||
const { connectionState } = useGatewayStore();
|
||||
const connectionState = useConnectionStore((s) => s.connectionState);
|
||||
|
||||
const isConnected = connectionState === 'connected';
|
||||
const isReconnecting = connectionState === 'reconnecting';
|
||||
@@ -221,4 +230,58 @@ export function ConnectionIndicator({ className = '' }: { className?: string })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HealthStatusIndicator - Displays OpenFang backend health status
|
||||
*/
|
||||
export function HealthStatusIndicator({
|
||||
className = '',
|
||||
showDetails = false,
|
||||
}: {
|
||||
className?: string;
|
||||
showDetails?: boolean;
|
||||
}) {
|
||||
const [healthResult, setHealthResult] = useState<HealthCheckResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Start periodic health checks
|
||||
const cleanup = createHealthCheckScheduler((result) => {
|
||||
setHealthResult(result);
|
||||
}, 30000); // Check every 30 seconds
|
||||
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
if (!healthResult) {
|
||||
return (
|
||||
<span className={`text-xs flex items-center gap-1 ${className}`}>
|
||||
<Heart className="w-3.5 h-3.5 text-gray-400" />
|
||||
<span className="text-gray-400">检查中...</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const statusColors: Record<HealthStatus, { dot: string; text: string; icon: typeof Heart }> = {
|
||||
healthy: { dot: 'bg-green-400', text: 'text-green-500', icon: Heart },
|
||||
unhealthy: { dot: 'bg-red-400', text: 'text-red-500', icon: HeartPulse },
|
||||
unknown: { dot: 'bg-gray-400', text: 'text-gray-500', icon: Heart },
|
||||
};
|
||||
|
||||
const config = statusColors[healthResult.status];
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<span className={`text-xs flex items-center gap-1 ${className}`}>
|
||||
<Icon className={`w-3.5 h-3.5 ${config.text}`} />
|
||||
<span className={config.text}>
|
||||
{getHealthStatusLabel(healthResult.status)}
|
||||
</span>
|
||||
{showDetails && healthResult.message && (
|
||||
<span className="text-gray-400 ml-1" title={healthResult.message}>
|
||||
({formatHealthCheckTime(healthResult.timestamp)})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionStatus;
|
||||
|
||||
137
desktop/src/lib/health-check.ts
Normal file
137
desktop/src/lib/health-check.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Health Check Library
|
||||
*
|
||||
* Provides Tauri health check command wrappers and utilities
|
||||
* for monitoring the health status of the OpenFang backend.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isTauriRuntime } from './tauri-gateway';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export type HealthStatus = 'healthy' | 'unhealthy' | 'unknown';
|
||||
|
||||
export interface HealthCheckResult {
|
||||
status: HealthStatus;
|
||||
message?: string;
|
||||
timestamp: number;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface OpenFangHealthResponse {
|
||||
healthy: boolean;
|
||||
message?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// === Health Check Functions ===
|
||||
|
||||
/**
|
||||
* Perform a single health check via Tauri command.
|
||||
* Returns a structured result with status, message, and timestamp.
|
||||
*/
|
||||
export async function performHealthCheck(): Promise<HealthCheckResult> {
|
||||
const timestamp = Date.now();
|
||||
|
||||
if (!isTauriRuntime()) {
|
||||
return {
|
||||
status: 'unknown',
|
||||
message: 'Not running in Tauri environment',
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await invoke<OpenFangHealthResponse>('openfang_health_check');
|
||||
|
||||
return {
|
||||
status: response.healthy ? 'healthy' : 'unhealthy',
|
||||
message: response.message,
|
||||
timestamp,
|
||||
details: response.details,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `Health check failed: ${errorMessage}`,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a periodic health check scheduler.
|
||||
* Returns cleanup function to stop the interval.
|
||||
*/
|
||||
export function createHealthCheckScheduler(
|
||||
callback: (result: HealthCheckResult) => void,
|
||||
intervalMs: number = 30000 // Default: 30 seconds
|
||||
): () => void {
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
let isChecking = false;
|
||||
|
||||
const check = async () => {
|
||||
// Prevent overlapping checks
|
||||
if (isChecking) return;
|
||||
isChecking = true;
|
||||
|
||||
try {
|
||||
const result = await performHealthCheck();
|
||||
callback(result);
|
||||
} catch (error) {
|
||||
console.error('[HealthCheck] Scheduled check failed:', error);
|
||||
callback({
|
||||
status: 'unknown',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
isChecking = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Perform initial check immediately
|
||||
check();
|
||||
|
||||
// Schedule periodic checks
|
||||
intervalId = setInterval(check, intervalMs);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === Utility Functions ===
|
||||
|
||||
/**
|
||||
* Get a human-readable label for a health status.
|
||||
*/
|
||||
export function getHealthStatusLabel(status: HealthStatus): string {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return '健康';
|
||||
case 'unhealthy':
|
||||
return '异常';
|
||||
case 'unknown':
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp for display.
|
||||
*/
|
||||
export function formatHealthCheckTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user