Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

537 lines
17 KiB
Rust

//! Integration tests for ACP session MCP injection.
//!
//! Covers test-plan items SI-1 through SI-7: capability parsing,
//! format conversion, enabled-only filtering, and builtin server injection.
use std::collections::HashMap;
use nomifun_common::McpServerStatus;
use nomifun_mcp::{
AcpMcpCapabilities, AcpSessionMcpServer, ImageGenConfig, McpServer, McpServerTransport, NameValuePair,
build_builtin_image_gen_server, build_session_mcp_servers, parse_acp_mcp_capabilities,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn make_server(name: &str, enabled: bool, transport: McpServerTransport) -> McpServer {
McpServer {
// Injection keys on `name`, never `id`; any stable value works here.
id: name.bytes().map(i64::from).sum::<i64>().max(1),
name: name.into(),
description: None,
enabled,
transport,
tools: vec![],
last_test_status: McpServerStatus::Disconnected,
last_connected: None,
original_json: None,
builtin: false,
created_at: 0,
updated_at: 0,
}
}
// ---------------------------------------------------------------------------
// SI-1: Full capabilities → all transports retained
// ---------------------------------------------------------------------------
#[test]
fn si_1_full_capabilities_retains_all_transports() {
let caps = AcpMcpCapabilities {
stdio: true,
http: true,
sse: true,
};
let servers = vec![
make_server(
"stdio-mcp",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec!["-y".into(), "test-server".into()],
env: HashMap::from([("KEY".into(), "VAL".into())]),
},
),
make_server(
"http-mcp",
true,
McpServerTransport::Http {
url: "https://example.com/mcp".into(),
headers: HashMap::from([("Authorization".into(), "Bearer tok".into())]),
},
),
make_server(
"sse-mcp",
true,
McpServerTransport::Sse {
url: "https://example.com/sse".into(),
headers: HashMap::new(),
},
),
];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 3, "all 3 transports should be retained");
// Verify each type is present
let has_stdio = result
.iter()
.any(|s| matches!(s, AcpSessionMcpServer::Stdio { name, .. } if name == "stdio-mcp"));
let has_http = result
.iter()
.any(|s| matches!(s, AcpSessionMcpServer::Http { name, .. } if name == "http-mcp"));
let has_sse = result
.iter()
.any(|s| matches!(s, AcpSessionMcpServer::Sse { name, .. } if name == "sse-mcp"));
assert!(has_stdio, "stdio server missing");
assert!(has_http, "http server missing");
assert!(has_sse, "sse server missing");
}
// ---------------------------------------------------------------------------
// SI-2: stdio-only capabilities → only stdio retained
// ---------------------------------------------------------------------------
#[test]
fn si_2_stdio_only_keeps_stdio_servers() {
let caps = AcpMcpCapabilities {
stdio: true,
http: false,
sse: false,
};
let servers = vec![
make_server(
"stdio-mcp",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec![],
env: HashMap::new(),
},
),
make_server(
"http-mcp",
true,
McpServerTransport::Http {
url: "https://example.com/mcp".into(),
headers: HashMap::new(),
},
),
make_server(
"sse-mcp",
true,
McpServerTransport::Sse {
url: "https://example.com/sse".into(),
headers: HashMap::new(),
},
),
];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 1);
assert!(matches!(&result[0], AcpSessionMcpServer::Stdio { name, .. } if name == "stdio-mcp"));
}
// ---------------------------------------------------------------------------
// SI-3: No capabilities → empty list
// ---------------------------------------------------------------------------
#[test]
fn si_3_no_capabilities_returns_empty() {
let caps = AcpMcpCapabilities {
stdio: false,
http: false,
sse: false,
};
let servers = vec![
make_server(
"s1",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec![],
env: HashMap::new(),
},
),
make_server(
"s2",
true,
McpServerTransport::Http {
url: "https://example.com".into(),
headers: HashMap::new(),
},
),
];
let result = build_session_mcp_servers(&servers, &caps);
assert!(result.is_empty(), "no capabilities → empty result");
}
// ---------------------------------------------------------------------------
// SI-4: stdio server format conversion (env Record → Vec<{name,value}>)
// ---------------------------------------------------------------------------
#[test]
fn si_4_stdio_format_conversion() {
let caps = AcpMcpCapabilities {
stdio: true,
http: false,
sse: false,
};
let servers = vec![make_server(
"test-stdio",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec!["-y".into(), "test-server".into()],
env: HashMap::from([
("NODE_ENV".into(), "production".into()),
("DEBUG".into(), "true".into()),
]),
},
)];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 1);
match &result[0] {
AcpSessionMcpServer::Stdio {
name,
command,
args,
env,
} => {
assert_eq!(name, "test-stdio");
assert_eq!(command, "npx");
assert_eq!(args, &["-y", "test-server"]);
assert_eq!(env.len(), 2);
// Sorted by name
assert_eq!(env[0].name, "DEBUG");
assert_eq!(env[0].value, "true");
assert_eq!(env[1].name, "NODE_ENV");
assert_eq!(env[1].value, "production");
}
_ => panic!("expected Stdio variant"),
}
// Verify JSON wire format
let json = serde_json::to_value(&result[0]).unwrap();
assert_eq!(json["type"], "stdio");
assert_eq!(json["name"], "test-stdio");
assert_eq!(json["command"], "npx");
assert!(json["env"].is_array());
assert_eq!(json["env"][0]["name"], "DEBUG");
assert_eq!(json["env"][0]["value"], "true");
}
// ---------------------------------------------------------------------------
// SI-5: http server format conversion (headers Record → Vec<{name,value}>)
// ---------------------------------------------------------------------------
#[test]
fn si_5_http_format_conversion() {
let caps = AcpMcpCapabilities {
stdio: false,
http: true,
sse: false,
};
let servers = vec![make_server(
"test-http",
true,
McpServerTransport::Http {
url: "https://example.com/mcp".into(),
headers: HashMap::from([
("Authorization".into(), "Bearer tok".into()),
("X-Custom".into(), "val".into()),
]),
},
)];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 1);
match &result[0] {
AcpSessionMcpServer::Http { name, url, headers } => {
assert_eq!(name, "test-http");
assert_eq!(url, "https://example.com/mcp");
assert_eq!(headers.len(), 2);
// Sorted by name
assert_eq!(headers[0].name, "Authorization");
assert_eq!(headers[0].value, "Bearer tok");
assert_eq!(headers[1].name, "X-Custom");
assert_eq!(headers[1].value, "val");
}
_ => panic!("expected Http variant"),
}
// Verify JSON wire format
let json = serde_json::to_value(&result[0]).unwrap();
assert_eq!(json["type"], "http");
assert_eq!(json["name"], "test-http");
assert_eq!(json["url"], "https://example.com/mcp");
assert!(json["headers"].is_array());
}
// ---------------------------------------------------------------------------
// SI-6: only enabled servers appear
// ---------------------------------------------------------------------------
#[test]
fn si_6_only_enabled_servers_in_result() {
let caps = AcpMcpCapabilities {
stdio: true,
http: true,
sse: true,
};
let servers = vec![
make_server(
"enabled-stdio",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec![],
env: HashMap::new(),
},
),
make_server(
"disabled-stdio",
false,
McpServerTransport::Stdio {
command: "node".into(),
args: vec![],
env: HashMap::new(),
},
),
make_server(
"enabled-http",
true,
McpServerTransport::Http {
url: "https://example.com".into(),
headers: HashMap::new(),
},
),
make_server(
"disabled-http",
false,
McpServerTransport::Http {
url: "https://other.com".into(),
headers: HashMap::new(),
},
),
];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 2, "only 2 enabled servers should appear");
let names: Vec<&str> = result
.iter()
.map(|s| match s {
AcpSessionMcpServer::Stdio { name, .. } => name.as_str(),
AcpSessionMcpServer::Http { name, .. } => name.as_str(),
AcpSessionMcpServer::Sse { name, .. } => name.as_str(),
})
.collect();
assert!(names.contains(&"enabled-stdio"));
assert!(names.contains(&"enabled-http"));
assert!(!names.contains(&"disabled-stdio"));
assert!(!names.contains(&"disabled-http"));
}
// ---------------------------------------------------------------------------
// SI-7: builtin MCP injection (image generation with env vars)
// ---------------------------------------------------------------------------
#[test]
fn si_7_builtin_image_gen_injection() {
let caps = AcpMcpCapabilities {
stdio: true,
http: true,
sse: true,
};
let img_config = ImageGenConfig {
model: Some("dall-e-3".into()),
api_url: Some("https://api.openai.com/v1".into()),
api_key: Some("sk-test-key".into()),
size: Some("1024x1024".into()),
quality: Some("hd".into()),
style: Some("natural".into()),
};
// Build user servers
let user_servers = vec![make_server(
"user-mcp",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec!["-y".into(), "user-server".into()],
env: HashMap::new(),
},
)];
let mut session_servers = build_session_mcp_servers(&user_servers, &caps);
// Inject builtin image gen server
if let Some(builtin) = build_builtin_image_gen_server(&caps, "/usr/local/bin/nomifun-img-gen", &img_config) {
session_servers.push(builtin);
}
assert_eq!(session_servers.len(), 2, "user server + builtin image gen");
// Verify the builtin server
let builtin = &session_servers[1];
match builtin {
AcpSessionMcpServer::Stdio { name, command, env, .. } => {
assert_eq!(name, "nomifun-image-generation");
assert_eq!(command, "/usr/local/bin/nomifun-img-gen");
// Verify all 6 env vars are present
assert_eq!(env.len(), 6);
let env_map: HashMap<&str, &str> = env.iter().map(|p| (p.name.as_str(), p.value.as_str())).collect();
assert_eq!(env_map["NOMIFUN_IMG_MODEL"], "dall-e-3");
assert_eq!(env_map["NOMIFUN_IMG_API_URL"], "https://api.openai.com/v1");
assert_eq!(env_map["NOMIFUN_IMG_API_KEY"], "sk-test-key");
assert_eq!(env_map["NOMIFUN_IMG_SIZE"], "1024x1024");
assert_eq!(env_map["NOMIFUN_IMG_QUALITY"], "hd");
assert_eq!(env_map["NOMIFUN_IMG_STYLE"], "natural");
}
_ => panic!("expected Stdio variant for builtin"),
}
}
// ---------------------------------------------------------------------------
// Capability parsing from various response formats
// ---------------------------------------------------------------------------
#[test]
fn parse_capabilities_from_real_response_shape() {
// Simulates a realistic ACP backend response with nested capabilities
let response = serde_json::json!({
"status": "ok",
"version": "1.2.3",
"mcp_capabilities": {
"stdio": true,
"http": true,
"sse": false
}
});
let caps = parse_acp_mcp_capabilities(&response);
assert!(caps.stdio);
assert!(caps.http);
assert!(!caps.sse);
}
#[test]
fn parse_capabilities_empty_response() {
let response = serde_json::json!({});
let caps = parse_acp_mcp_capabilities(&response);
// Default: stdio only
assert!(caps.stdio);
assert!(!caps.http);
assert!(!caps.sse);
}
// ---------------------------------------------------------------------------
// End-to-end: parse capabilities + build servers
// ---------------------------------------------------------------------------
#[test]
fn end_to_end_parse_then_build() {
let acp_response = serde_json::json!({
"mcp_capabilities": { "stdio": true, "http": true, "sse": false }
});
let caps = parse_acp_mcp_capabilities(&acp_response);
let servers = vec![
make_server(
"stdio-srv",
true,
McpServerTransport::Stdio {
command: "npx".into(),
args: vec![],
env: HashMap::new(),
},
),
make_server(
"http-srv",
true,
McpServerTransport::Http {
url: "https://example.com".into(),
headers: HashMap::new(),
},
),
make_server(
"sse-srv",
true,
McpServerTransport::Sse {
url: "https://example.com/sse".into(),
headers: HashMap::new(),
},
),
];
let result = build_session_mcp_servers(&servers, &caps);
assert_eq!(result.len(), 2, "sse should be filtered out");
let names: Vec<&str> = result
.iter()
.map(|s| match s {
AcpSessionMcpServer::Stdio { name, .. } => name.as_str(),
AcpSessionMcpServer::Http { name, .. } => name.as_str(),
AcpSessionMcpServer::Sse { name, .. } => name.as_str(),
})
.collect();
assert!(names.contains(&"stdio-srv"));
assert!(names.contains(&"http-srv"));
assert!(!names.contains(&"sse-srv"));
}
// ---------------------------------------------------------------------------
// JSON wire format verification
// ---------------------------------------------------------------------------
#[test]
fn wire_format_is_acp_compatible() {
let servers = vec![
AcpSessionMcpServer::Stdio {
name: "test-stdio".into(),
command: "npx".into(),
args: vec!["-y".into(), "server".into()],
env: vec![NameValuePair {
name: "K".into(),
value: "V".into(),
}],
},
AcpSessionMcpServer::Http {
name: "test-http".into(),
url: "https://example.com/mcp".into(),
headers: vec![NameValuePair {
name: "Auth".into(),
value: "Bearer x".into(),
}],
},
];
let json = serde_json::to_value(&servers).unwrap();
let arr = json.as_array().unwrap();
// stdio variant
assert_eq!(arr[0]["type"], "stdio");
assert_eq!(arr[0]["name"], "test-stdio");
assert_eq!(arr[0]["command"], "npx");
assert_eq!(arr[0]["args"][0], "-y");
assert_eq!(arr[0]["env"][0]["name"], "K");
assert_eq!(arr[0]["env"][0]["value"], "V");
// http variant
assert_eq!(arr[1]["type"], "http");
assert_eq!(arr[1]["name"], "test-http");
assert_eq!(arr[1]["url"], "https://example.com/mcp");
assert_eq!(arr[1]["headers"][0]["name"], "Auth");
}