Replace cortex-agent stub with neuron (cortex-neuron binary). cortex-core additions: - discovery.rs: DeviceInfo, DiscoveryResponse, DeviceHealth, HealthResponse - harness.rs: Harness async trait, HarnessConfig, ModelSpec, ModelInfo neuron crate (crates/neuron/): - discovery.rs: nvidia-smi CSV parsing (pure functions) + system discovery via uname/nvidia-smi/nvcc - health.rs: cached GPU health polling every 5s - api.rs: GET /discovery and GET /health axum handlers - main.rs: CLI entrypoint with --port flag (default 9090) - harness stubs for mistralrs (Phase 8) and llamacpp (Phase 11) 12 new tests (9 unit + 3 integration), 35 total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
4.6 KiB
Rust
156 lines
4.6 KiB
Rust
use cortex_core::discovery::{DeviceHealth, DeviceInfo, DiscoveryResponse, HealthResponse};
|
|
use cortex_neuron::api::{self, NeuronState};
|
|
use cortex_neuron::health::HealthCache;
|
|
use std::sync::Arc;
|
|
|
|
async fn spawn_neuron(discovery: DiscoveryResponse, health: HealthResponse) -> String {
|
|
let health_cache = Arc::new(HealthCache::new());
|
|
// Pre-populate the health cache by writing through the snapshot mechanism.
|
|
// HealthCache doesn't expose a direct setter, so we'll build one with
|
|
// the data already in place via the NeuronState.
|
|
// For testing, we use the cache as-is (uptime 0, empty devices) unless
|
|
// we need specific values — see test_health_endpoint.
|
|
let _ = health; // used below via a different approach
|
|
|
|
let state = Arc::new(NeuronState {
|
|
discovery,
|
|
health_cache,
|
|
});
|
|
|
|
let app = api::neuron_routes().with_state(state);
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
tokio::spawn(async move {
|
|
axum::serve(listener, app).await.unwrap();
|
|
});
|
|
format!("http://{addr}")
|
|
}
|
|
|
|
fn fake_discovery() -> DiscoveryResponse {
|
|
DiscoveryResponse {
|
|
hostname: "test-node".into(),
|
|
os: "Linux".into(),
|
|
kernel: "6.19.0".into(),
|
|
cuda_version: Some("12.8".into()),
|
|
driver_version: Some("570.86.16".into()),
|
|
devices: vec![
|
|
DeviceInfo {
|
|
index: 0,
|
|
name: "NVIDIA GeForce RTX 5090".into(),
|
|
vram_total_mb: 32614,
|
|
compute_capability: "12.0".into(),
|
|
},
|
|
DeviceInfo {
|
|
index: 1,
|
|
name: "NVIDIA GeForce RTX 5090".into(),
|
|
vram_total_mb: 32614,
|
|
compute_capability: "12.0".into(),
|
|
},
|
|
],
|
|
harnesses: vec![],
|
|
}
|
|
}
|
|
|
|
fn fake_health() -> HealthResponse {
|
|
HealthResponse {
|
|
uptime_secs: 0,
|
|
devices: vec![
|
|
DeviceHealth {
|
|
index: 0,
|
|
vram_used_mb: 8192,
|
|
vram_free_mb: 24422,
|
|
utilization_pct: 45,
|
|
temp_c: 62,
|
|
},
|
|
DeviceHealth {
|
|
index: 1,
|
|
vram_used_mb: 4096,
|
|
vram_free_mb: 28518,
|
|
utilization_pct: 30,
|
|
temp_c: 58,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_discovery_endpoint() {
|
|
let disc = fake_discovery();
|
|
let url = spawn_neuron(disc, fake_health()).await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let resp = client
|
|
.get(format!("{url}/discovery"))
|
|
.send()
|
|
.await
|
|
.expect("request should succeed");
|
|
|
|
assert_eq!(resp.status(), 200);
|
|
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["hostname"], "test-node");
|
|
assert_eq!(body["os"], "Linux");
|
|
assert_eq!(body["cuda_version"], "12.8");
|
|
assert_eq!(body["driver_version"], "570.86.16");
|
|
|
|
let devices = body["devices"].as_array().unwrap();
|
|
assert_eq!(devices.len(), 2);
|
|
assert_eq!(devices[0]["name"], "NVIDIA GeForce RTX 5090");
|
|
assert_eq!(devices[0]["vram_total_mb"], 32614);
|
|
assert_eq!(devices[0]["compute_capability"], "12.0");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_health_endpoint() {
|
|
let url = spawn_neuron(fake_discovery(), fake_health()).await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let resp = client
|
|
.get(format!("{url}/health"))
|
|
.send()
|
|
.await
|
|
.expect("request should succeed");
|
|
|
|
assert_eq!(resp.status(), 200);
|
|
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
// HealthCache starts with uptime 0 and empty devices (no poller running in test).
|
|
assert_eq!(body["uptime_secs"], 0);
|
|
assert!(body["devices"].as_array().unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_discovery_no_gpus() {
|
|
let disc = DiscoveryResponse {
|
|
hostname: "cpu-only".into(),
|
|
os: "Linux".into(),
|
|
kernel: "6.19.0".into(),
|
|
cuda_version: None,
|
|
driver_version: None,
|
|
devices: vec![],
|
|
harnesses: vec![],
|
|
};
|
|
let url = spawn_neuron(
|
|
disc,
|
|
HealthResponse {
|
|
uptime_secs: 0,
|
|
devices: vec![],
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let resp = client
|
|
.get(format!("{url}/discovery"))
|
|
.send()
|
|
.await
|
|
.expect("request should succeed");
|
|
|
|
assert_eq!(resp.status(), 200);
|
|
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["hostname"], "cpu-only");
|
|
assert!(body["cuda_version"].is_null());
|
|
assert!(body["devices"].as_array().unwrap().is_empty());
|
|
}
|