Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nomifun_api_types::ToolType;
|
||||
use nomifun_shell::{NoopSystemOpener, ShellService};
|
||||
|
||||
fn service() -> ShellService {
|
||||
ShellService::new(Arc::new(NoopSystemOpener))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-2: open_file — file does not exist
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh2_open_file_not_found() {
|
||||
let err = service().open_file("/nonexistent/file.txt").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("not found") || msg.contains("does not exist"),
|
||||
"expected 'not found', got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-4: show_item_in_folder — path does not exist
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh4_show_item_in_folder_not_found() {
|
||||
let err = service().show_item_in_folder("/nonexistent/path").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("not found"), "expected 'not found', got: {msg}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-6: open_external — command injection attempt
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh6_open_external_command_injection() {
|
||||
let err = service().open_external("; rm -rf /").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.to_lowercase().contains("invalid") || msg.to_lowercase().contains("url"),
|
||||
"expected 'invalid' or 'URL', got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-7: open_external — disallowed scheme (file://)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh7_open_external_file_scheme() {
|
||||
let err = service().open_external("file:///etc/passwd").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("scheme") || msg.to_lowercase().contains("not allowed"),
|
||||
"expected scheme error, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-8: check_tool_installed — terminal always true
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh8_check_tool_terminal_always_true() {
|
||||
assert!(service().check_tool_installed(ToolType::Terminal).await);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-9: check_tool_installed — explorer always true
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh9_check_tool_explorer_always_true() {
|
||||
assert!(service().check_tool_installed(ToolType::Explorer).await);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-10: check_tool_installed — vscode (environment-dependent)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh10_check_tool_vscode_returns_bool() {
|
||||
let _installed = service().check_tool_installed(ToolType::Vscode).await;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-12: open_folder_with — directory does not exist
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh12_open_folder_with_dir_not_found() {
|
||||
let err = service()
|
||||
.open_folder_with("/nonexistent/dir", ToolType::Explorer)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("not found"), "expected 'not found', got: {msg}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-13: open_file — missing filePath (tested via empty string)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh13_open_file_empty_path() {
|
||||
let err = service().open_file("").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("not found"),
|
||||
"expected 'not found' for empty path, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SH-14: open_external — empty string
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn sh14_open_external_empty_url() {
|
||||
let err = service().open_external("").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.to_lowercase().contains("invalid") || msg.to_lowercase().contains("url"),
|
||||
"expected invalid URL error, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional: open_folder_with — file path instead of directory
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn open_folder_with_file_path_rejected() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test.txt");
|
||||
std::fs::write(&file, "data").unwrap();
|
||||
let err = service()
|
||||
.open_folder_with(file.to_str().unwrap(), ToolType::Explorer)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("directory not found"),
|
||||
"expected 'directory not found', got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional: open_external — ftp scheme rejected
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn open_external_ftp_scheme_rejected() {
|
||||
let err = service().open_external("ftp://evil.com/file").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("scheme"), "expected scheme error, got: {msg}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional: open_external — javascript scheme rejected
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn open_external_javascript_scheme_rejected() {
|
||||
let err = service().open_external("javascript:alert(1)").await.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("scheme") || msg.to_lowercase().contains("invalid"),
|
||||
"expected scheme or invalid error, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error conversion: ShellError → AppError mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
#[test]
|
||||
fn shell_error_converts_to_app_error() {
|
||||
use nomifun_common::AppError;
|
||||
use nomifun_shell::ShellError;
|
||||
|
||||
let err: AppError = ShellError::FileNotFound("/tmp/x".into()).into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = ShellError::DirectoryNotFound("/tmp/y".into()).into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = ShellError::InvalidUrl("bad".into()).into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = ShellError::ToolNotInstalled("vscode".into()).into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = ShellError::CommandFailed("oops".into()).into();
|
||||
assert!(matches!(err, AppError::Internal(_)));
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
use nomifun_api_types::{
|
||||
DeepgramSpeechToTextConfig, OpenAISpeechToTextConfig, SpeechToTextConfig, SpeechToTextProvider,
|
||||
};
|
||||
use nomifun_shell::{SttError, SttService};
|
||||
use wiremock::matchers::{header, method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
fn dummy_audio() -> Vec<u8> {
|
||||
vec![0u8; 64]
|
||||
}
|
||||
|
||||
fn stt_service() -> SttService {
|
||||
SttService::new(reqwest::Client::new())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-1: OpenAI transcription — success
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st1_openai_transcribe_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/audio/transcriptions"))
|
||||
.and(header("Authorization", "Bearer sk-test-key"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "text": "hello world" })))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: Some(OpenAISpeechToTextConfig {
|
||||
api_key: "sk-test-key".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "whisper-1".into(),
|
||||
language: None,
|
||||
prompt: None,
|
||||
temperature: None,
|
||||
}),
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "hello world");
|
||||
assert_eq!(result.model, "whisper-1");
|
||||
assert_eq!(result.provider, SpeechToTextProvider::Openai);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-2: Deepgram transcription — success
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st2_deepgram_transcribe_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/listen"))
|
||||
.and(header("Authorization", "Token dg-test-key"))
|
||||
.and(header("Content-Type", "audio/wav"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"metadata": {
|
||||
"model_info": {
|
||||
"uuid-1": {
|
||||
"name": "2-general-nova",
|
||||
"version": "2024-01"
|
||||
}
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"channels": [{
|
||||
"detected_language": "en",
|
||||
"alternatives": [{
|
||||
"transcript": "hello deepgram"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
})))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: Some(DeepgramSpeechToTextConfig {
|
||||
api_key: "dg-test-key".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "nova-2".into(),
|
||||
language: None,
|
||||
detect_language: Some(true),
|
||||
punctuate: Some(true),
|
||||
smart_format: Some(true),
|
||||
}),
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "hello deepgram");
|
||||
assert_eq!(result.model, "2-general-nova");
|
||||
assert_eq!(result.provider, SpeechToTextProvider::Deepgram);
|
||||
assert_eq!(result.language.as_deref(), Some("en"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-3: STT disabled
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st3_stt_disabled() {
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: false,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(SttError::Disabled)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-4: STT config missing — treated as disabled at service layer
|
||||
// (the handler reads from ClientPrefService; if key is absent, config
|
||||
// will have enabled=false or we surface STT_DISABLED upstream)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-5: OpenAI missing API key
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st5_openai_empty_api_key() {
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: Some(OpenAISpeechToTextConfig {
|
||||
api_key: String::new(),
|
||||
base_url: None,
|
||||
model: "whisper-1".into(),
|
||||
language: None,
|
||||
prompt: None,
|
||||
temperature: None,
|
||||
}),
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(SttError::OpenaiNotConfigured)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn st5b_openai_config_section_missing() {
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(SttError::OpenaiNotConfigured)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-6: Deepgram missing API key
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st6_deepgram_empty_api_key() {
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: Some(DeepgramSpeechToTextConfig {
|
||||
api_key: String::new(),
|
||||
base_url: None,
|
||||
model: "nova-2".into(),
|
||||
language: None,
|
||||
detect_language: None,
|
||||
punctuate: None,
|
||||
smart_format: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(SttError::DeepgramNotConfigured)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn st6b_deepgram_config_section_missing() {
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(SttError::DeepgramNotConfigured)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-7: OpenAI upstream API failure (401)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st7_openai_upstream_failure() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/audio/transcriptions"))
|
||||
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
|
||||
"error": {
|
||||
"message": "Incorrect API key provided",
|
||||
"type": "invalid_request_error"
|
||||
}
|
||||
})))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: Some(OpenAISpeechToTextConfig {
|
||||
api_key: "sk-invalid".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "whisper-1".into(),
|
||||
language: None,
|
||||
prompt: None,
|
||||
temperature: None,
|
||||
}),
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(SttError::RequestFailed(msg)) => {
|
||||
assert!(msg.contains("401"), "expected 401 in error: {msg}");
|
||||
}
|
||||
other => panic!("expected RequestFailed, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-7b: Deepgram upstream API failure (403)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st7b_deepgram_upstream_failure() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/listen"))
|
||||
.respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({ "err_msg": "Invalid credentials" })))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: Some(DeepgramSpeechToTextConfig {
|
||||
api_key: "dg-invalid".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "nova-2".into(),
|
||||
language: None,
|
||||
detect_language: None,
|
||||
punctuate: None,
|
||||
smart_format: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", None, &config)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(SttError::RequestFailed(msg)) => {
|
||||
assert!(msg.contains("403"), "expected 403 in error: {msg}");
|
||||
}
|
||||
other => panic!("expected RequestFailed, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-10: languageHint passed to OpenAI
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st10_openai_language_hint_passed() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/audio/transcriptions"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "text": "你好世界" })))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: None,
|
||||
openai: Some(OpenAISpeechToTextConfig {
|
||||
api_key: "sk-test".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "whisper-1".into(),
|
||||
language: Some("en".into()),
|
||||
prompt: None,
|
||||
temperature: None,
|
||||
}),
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", Some("zh"), &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "你好世界");
|
||||
assert_eq!(result.language.as_deref(), Some("zh"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ST-10b: languageHint passed to Deepgram
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn st10b_deepgram_language_hint_passed() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/listen"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"metadata": { "model_info": {} },
|
||||
"results": {
|
||||
"channels": [{
|
||||
"detected_language": "zh",
|
||||
"alternatives": [{ "transcript": "你好" }]
|
||||
}]
|
||||
}
|
||||
})))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: Some(DeepgramSpeechToTextConfig {
|
||||
api_key: "dg-test".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "nova-2".into(),
|
||||
language: None,
|
||||
detect_language: None,
|
||||
punctuate: None,
|
||||
smart_format: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.wav", "audio/wav", Some("zh"), &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "你好");
|
||||
assert_eq!(result.language.as_deref(), Some("zh"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional: OpenAI with all optional params (prompt, temperature)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn openai_with_all_optional_params() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/audio/transcriptions"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "text": "technical terms test" })))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Openai,
|
||||
auto_send: Some(true),
|
||||
openai: Some(OpenAISpeechToTextConfig {
|
||||
api_key: "sk-full".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "whisper-1".into(),
|
||||
language: Some("en".into()),
|
||||
prompt: Some("technical terms".into()),
|
||||
temperature: Some(0.2),
|
||||
}),
|
||||
deepgram: None,
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "audio.m4a", "audio/mp4", None, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "technical terms test");
|
||||
assert_eq!(result.model, "whisper-1");
|
||||
assert_eq!(result.provider, SpeechToTextProvider::Openai);
|
||||
assert_eq!(result.language.as_deref(), Some("en"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional: Deepgram with all optional flags
|
||||
// ---------------------------------------------------------------------------
|
||||
#[tokio::test]
|
||||
async fn deepgram_with_all_optional_flags() {
|
||||
let mock_server = MockServer::start().await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/listen"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"metadata": {
|
||||
"model_info": {
|
||||
"id-1": { "name": "nova-2-general" }
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"channels": [{
|
||||
"detected_language": "fr",
|
||||
"alternatives": [{ "transcript": "bonjour" }]
|
||||
}]
|
||||
}
|
||||
})))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let config = SpeechToTextConfig {
|
||||
enabled: true,
|
||||
provider: SpeechToTextProvider::Deepgram,
|
||||
auto_send: None,
|
||||
openai: None,
|
||||
deepgram: Some(DeepgramSpeechToTextConfig {
|
||||
api_key: "dg-full".into(),
|
||||
base_url: Some(mock_server.uri()),
|
||||
model: "nova-2".into(),
|
||||
language: Some("fr".into()),
|
||||
detect_language: Some(false),
|
||||
punctuate: Some(true),
|
||||
smart_format: Some(true),
|
||||
}),
|
||||
};
|
||||
|
||||
let result = stt_service()
|
||||
.transcribe(dummy_audio(), "test.ogg", "audio/ogg", None, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.text, "bonjour");
|
||||
assert_eq!(result.model, "nova-2-general");
|
||||
assert_eq!(result.language.as_deref(), Some("fr"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SttError → AppError conversion (black-box integration test)
|
||||
// ---------------------------------------------------------------------------
|
||||
#[test]
|
||||
fn stt_error_to_app_error_mapping() {
|
||||
use nomifun_common::AppError;
|
||||
|
||||
let err: AppError = SttError::Disabled.into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = SttError::OpenaiNotConfigured.into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = SttError::DeepgramNotConfigured.into();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
|
||||
let err: AppError = SttError::RequestFailed("upstream".into()).into();
|
||||
assert!(matches!(err, AppError::BadGateway(_)));
|
||||
|
||||
let err: AppError = SttError::Unknown("bug".into()).into();
|
||||
assert!(matches!(err, AppError::Internal(_)));
|
||||
}
|
||||
Reference in New Issue
Block a user