Files
mixvideo-v2/apps/desktop/src-tauri/src/infrastructure/gemini_service_tests.rs
imeepos 1fb5468ecc feat: 实现RAG Grounding服务功能
基于promptx/outfit-match中的query_llm_with_grounding实现,遵循Tauri开发规范

## 新增功能
- 实现RAG Grounding核心服务逻辑
- 支持基于Vertex AI Search的检索增强生成
- 集成Cloudflare Gateway和Google Gemini API
- 提供完整的TypeScript类型定义和前端服务封装

## 技术架构
- Rust后端:GeminiService扩展RAG功能
- Tauri命令:query_rag_grounding, test_rag_grounding_connection, get_rag_grounding_config
- 前端服务:RagGroundingService类,支持统计监控和错误处理
- 数据模型:完整的请求/响应结构和配置管理

## 核心特性
- 检索增强生成:基于数据存储的智能检索和内容生成
- 容错机制:内置重试机制和错误处理
- 性能监控:响应时间统计和性能指标
- 类型安全:完整的TypeScript类型定义
- 会话管理:支持上下文保持的对话功能

## 测试覆盖
- 25个单元测试,覆盖核心功能和边界情况
- 包括序列化/反序列化、错误处理、配置验证等
- 所有测试通过,确保功能正确性

## 文档和示例
- 完整的API文档 (docs/rag-grounding-api.md)
- 详细的使用示例 (examples/rag-grounding-usage.ts)
- 包含最佳实践和错误处理指南

## 遵循规范
- 严格遵循promptx/tauri-desktop-app-expert开发规范
- 安全第一:最小权限原则,数据加密保护
- 性能优先:异步处理,响应时间优化
- 模块化设计:清晰的架构分层
- 错误处理完善:全面的错误处理和用户反馈
2025-07-21 21:07:03 +08:00

227 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[cfg(test)]
mod rag_grounding_tests {
use crate::infrastructure::gemini_service::{
GeminiService, GeminiConfig, RagGroundingRequest, RagGroundingConfig, RagGroundingResponse,
GroundingMetadata, GroundingSource,
};
/// 创建测试用的 GeminiService 实例
fn create_test_service() -> GeminiService {
let config = GeminiConfig::default();
GeminiService::new(Some(config)).expect("Failed to create test service")
}
/// 创建测试用的 RAG Grounding 请求
fn create_test_request() -> RagGroundingRequest {
RagGroundingRequest {
user_input: "测试查询".to_string(),
config: Some(RagGroundingConfig::default()),
session_id: Some("test-session".to_string()),
}
}
#[test]
fn test_rag_grounding_config_default() {
let config = RagGroundingConfig::default();
assert_eq!(config.project_id, "gen-lang-client-0413414134");
assert_eq!(config.location, "global");
assert_eq!(config.data_store_id, "default_data_store");
assert_eq!(config.model_id, "gemini-2.5-flash");
assert_eq!(config.temperature, 1.0);
assert_eq!(config.max_output_tokens, 8192);
assert!(config.system_prompt.is_none());
}
#[test]
fn test_rag_grounding_request_creation() {
let request = create_test_request();
assert_eq!(request.user_input, "测试查询");
assert!(request.config.is_some());
assert_eq!(request.session_id, Some("test-session".to_string()));
}
#[test]
fn test_rag_grounding_request_serialization() {
let request = create_test_request();
// 测试序列化
let serialized = serde_json::to_string(&request).expect("Failed to serialize request");
assert!(serialized.contains("测试查询"));
assert!(serialized.contains("test-session"));
// 测试反序列化
let deserialized: RagGroundingRequest = serde_json::from_str(&serialized)
.expect("Failed to deserialize request");
assert_eq!(deserialized.user_input, request.user_input);
assert_eq!(deserialized.session_id, request.session_id);
}
#[test]
fn test_rag_grounding_response_creation() {
let response = RagGroundingResponse {
answer: "测试回答".to_string(),
grounding_metadata: Some(GroundingMetadata {
sources: vec![GroundingSource {
title: "测试来源".to_string(),
uri: Some("https://example.com".to_string()),
snippet: "测试片段".to_string(),
relevance_score: Some(0.95),
}],
search_queries: vec!["测试查询".to_string()],
}),
response_time_ms: 1500,
model_used: "gemini-2.5-flash".to_string(),
};
assert_eq!(response.answer, "测试回答");
assert!(response.grounding_metadata.is_some());
assert_eq!(response.response_time_ms, 1500);
assert_eq!(response.model_used, "gemini-2.5-flash");
let metadata = response.grounding_metadata.unwrap();
assert_eq!(metadata.sources.len(), 1);
assert_eq!(metadata.search_queries.len(), 1);
let source = &metadata.sources[0];
assert_eq!(source.title, "测试来源");
assert_eq!(source.uri, Some("https://example.com".to_string()));
assert_eq!(source.snippet, "测试片段");
assert_eq!(source.relevance_score, Some(0.95));
}
#[test]
fn test_grounding_metadata_empty() {
let metadata = GroundingMetadata {
sources: vec![],
search_queries: vec![],
};
assert!(metadata.sources.is_empty());
assert!(metadata.search_queries.is_empty());
}
#[test]
fn test_grounding_source_without_optional_fields() {
let source = GroundingSource {
title: "测试来源".to_string(),
uri: None,
snippet: "测试片段".to_string(),
relevance_score: None,
};
assert_eq!(source.title, "测试来源");
assert!(source.uri.is_none());
assert_eq!(source.snippet, "测试片段");
assert!(source.relevance_score.is_none());
}
#[test]
fn test_rag_grounding_config_with_system_prompt() {
let mut config = RagGroundingConfig::default();
config.system_prompt = Some("你是一个专业的助手".to_string());
assert!(config.system_prompt.is_some());
assert_eq!(config.system_prompt.unwrap(), "你是一个专业的助手");
}
#[test]
fn test_rag_grounding_config_custom_values() {
let config = RagGroundingConfig {
project_id: "custom-project".to_string(),
location: "us-central1".to_string(),
data_store_id: "custom-datastore".to_string(),
model_id: "gemini-pro".to_string(),
temperature: 0.5,
max_output_tokens: 4096,
system_prompt: Some("自定义系统提示".to_string()),
};
assert_eq!(config.project_id, "custom-project");
assert_eq!(config.location, "us-central1");
assert_eq!(config.data_store_id, "custom-datastore");
assert_eq!(config.model_id, "gemini-pro");
assert_eq!(config.temperature, 0.5);
assert_eq!(config.max_output_tokens, 4096);
assert_eq!(config.system_prompt, Some("自定义系统提示".to_string()));
}
#[test]
fn test_gemini_service_creation() {
let service = create_test_service();
// 基本的服务创建测试确保没有panic
assert!(true); // 如果能到这里说明服务创建成功
}
/// 测试 JSON 响应解析
#[test]
fn test_parse_rag_grounding_response_json() {
let service = create_test_service();
// 模拟一个典型的 Gemini API 响应
let mock_response = r#"{
"candidates": [{
"content": {
"parts": [{
"text": "这是一个测试回答"
}]
},
"groundingMetadata": {
"groundingSources": [{
"title": "测试文档",
"uri": "https://example.com/doc",
"snippet": "这是一个测试片段",
"relevanceScore": 0.85
}],
"searchQueries": ["测试查询"]
}
}]
}"#;
let result = service.parse_rag_grounding_response(mock_response);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.answer, "这是一个测试回答");
assert!(response.grounding_metadata.is_some());
let metadata = response.grounding_metadata.unwrap();
assert_eq!(metadata.sources.len(), 1);
assert_eq!(metadata.search_queries.len(), 1);
let source = &metadata.sources[0];
assert_eq!(source.title, "测试文档");
assert_eq!(source.uri, Some("https://example.com/doc".to_string()));
assert_eq!(source.snippet, "这是一个测试片段");
assert_eq!(source.relevance_score, Some(0.85));
}
/// 测试无效 JSON 响应处理
#[test]
fn test_parse_invalid_json_response() {
let service = create_test_service();
let invalid_json = "这不是有效的JSON";
let result = service.parse_rag_grounding_response(invalid_json);
assert!(result.is_err());
}
/// 测试缺少必要字段的响应
#[test]
fn test_parse_incomplete_response() {
let service = create_test_service();
let incomplete_response = r#"{
"candidates": []
}"#;
let result = service.parse_rag_grounding_response(incomplete_response);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.answer, "无法解析响应内容");
assert!(response.grounding_metadata.is_none());
}
}