use tracing::{debug, info, warn};
use crate::client::{KeyComputeClient, OllamaClient};
use crate::config::NodeTokenConfig;
use crate::error::NodeTokenError;
use crate::protocol::types::{
NodeCapabilities, NodeModelCapability, NodeRegisterRequest, NodeRegisterResponse,
};
use crate::storage::{LocalStorage, SessionData};
#[allow(dead_code)] pub type Result<T> = std::result::Result<T, NodeTokenError>;
#[allow(dead_code)] pub async fn register_node(
client: &KeyComputeClient,
ollama_client: &OllamaClient,
config: &NodeTokenConfig,
storage: &LocalStorage,
) -> Result<NodeRegisterResponse> {
info!("Starting node registration...");
debug!("Scanning Ollama models...");
let models: Vec<String> = ollama_client.list_models().await?;
if models.is_empty() {
warn!("No Ollama models found on this machine");
} else {
info!("Found {} Ollama models: {:?}", models.len(), models);
}
let req = NodeRegisterRequest {
protocol_version: "node.v1".to_string(),
client_instance_id: config.client_instance_id.clone(),
display_name: config.display_name.clone(),
registration_token: config.registration_token.clone(),
capabilities: NodeCapabilities {
runtime: "ollama".to_string(),
models: models
.into_iter()
.map(|m| NodeModelCapability { model: m })
.collect(),
},
};
debug!(
"Registration request: client_instance_id={}, display_name={}, runtime=ollama, models_count={}",
req.client_instance_id,
req.display_name,
req.capabilities.models.len()
);
info!("Calling register API...");
let resp = client.register(&req).await?;
let session = SessionData {
node_id: resp.node_id,
session_id: resp.session_id,
session_token: resp.session_token.clone(),
capabilities: req.capabilities.clone(), poll_timeout_secs: resp.poll_timeout_secs, };
storage.save_session(&session)?;
info!(
"Registration successful: node_id={}, session_id={}, heartbeat_interval={}s, poll_timeout={}s",
resp.node_id,
resp.session_id,
resp.heartbeat_interval_secs,
resp.poll_timeout_secs
);
debug!("Session token saved to local storage (not logged for security)");
Ok(resp)
}
#[allow(dead_code)] pub fn try_load_session(storage: &LocalStorage) -> Result<Option<SessionData>> {
debug!("Attempting to load session from local storage...");
match storage.load_session()? {
Some(session) => {
info!(
"Loaded existing session: node_id={}, session_id={}",
session.node_id, session.session_id
);
debug!(
"Session capabilities: runtime={}, models_count={}",
session.capabilities.runtime,
session.capabilities.models.len()
);
Ok(Some(session))
}
None => {
debug!("No existing session found, will register new node");
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_try_load_session_no_existing() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(Some(temp_dir.path().to_str().unwrap())).unwrap();
let result = try_load_session(&storage).unwrap();
assert!(result.is_none());
}
#[test]
fn test_try_load_session_existing() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(Some(temp_dir.path().to_str().unwrap())).unwrap();
let session = SessionData {
node_id: uuid::Uuid::new_v4(),
session_id: uuid::Uuid::new_v4(),
session_token: "test-token".to_string(),
capabilities: NodeCapabilities {
runtime: "ollama".to_string(),
models: vec![NodeModelCapability {
model: "test-model".to_string(),
}],
},
poll_timeout_secs: 30,
};
storage.save_session(&session).unwrap();
let result = try_load_session(&storage).unwrap();
assert!(result.is_some());
let loaded = result.unwrap();
assert_eq!(loaded.node_id, session.node_id);
assert_eq!(loaded.session_id, session.session_id);
assert_eq!(loaded.capabilities.runtime, "ollama");
assert_eq!(loaded.capabilities.models.len(), 1);
}
#[test]
fn test_register_request_building() {
let config = NodeTokenConfig {
server_url: "http://localhost:3000".to_string(),
registration_token: "test-token".to_string(),
client_instance_id: "test-instance-001".to_string(),
display_name: "Test Node".to_string(),
ollama_url: "http://localhost:11434".to_string(),
heartbeat_interval_secs: 30,
excluded_poll_check_interval_secs: 30,
data_dir: None,
};
let models = vec![
"deepseek-chat:latest".to_string(),
"llama3:latest".to_string(),
];
let req = NodeRegisterRequest {
protocol_version: "node.v1".to_string(),
client_instance_id: config.client_instance_id.clone(),
display_name: config.display_name.clone(),
registration_token: config.registration_token.clone(),
capabilities: NodeCapabilities {
runtime: "ollama".to_string(),
models: models
.into_iter()
.map(|m| NodeModelCapability { model: m })
.collect(),
},
};
assert_eq!(req.protocol_version, "node.v1");
assert_eq!(req.client_instance_id, "test-instance-001");
assert_eq!(req.display_name, "Test Node");
assert_eq!(req.registration_token, "test-token");
assert_eq!(req.capabilities.runtime, "ollama");
assert_eq!(req.capabilities.models.len(), 2);
assert_eq!(req.capabilities.models[0].model, "deepseek-chat:latest");
assert_eq!(req.capabilities.models[1].model, "llama3:latest");
}
#[test]
fn test_session_persistence_after_register() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(Some(temp_dir.path().to_str().unwrap())).unwrap();
let resp = NodeRegisterResponse {
protocol_version: "node.v1".to_string(),
node_id: uuid::Uuid::new_v4(),
session_id: uuid::Uuid::new_v4(),
session_token: "new-session-token".to_string(),
heartbeat_interval_secs: 30,
poll_timeout_secs: 10,
};
let capabilities = NodeCapabilities {
runtime: "ollama".to_string(),
models: vec![NodeModelCapability {
model: "deepseek-chat:latest".to_string(),
}],
};
let session = SessionData {
node_id: resp.node_id,
session_id: resp.session_id,
session_token: resp.session_token.clone(),
capabilities: capabilities.clone(),
poll_timeout_secs: resp.poll_timeout_secs,
};
storage.save_session(&session).unwrap();
let loaded = storage.load_session().unwrap().unwrap();
assert_eq!(loaded.node_id, resp.node_id);
assert_eq!(loaded.session_id, resp.session_id);
assert_eq!(loaded.session_token, resp.session_token);
assert_eq!(loaded.capabilities.runtime, "ollama");
assert_eq!(loaded.capabilities.models.len(), 1);
assert_eq!(loaded.capabilities.models[0].model, "deepseek-chat:latest");
assert_eq!(loaded.poll_timeout_secs, 10);
}
#[test]
fn test_register_request_with_empty_models() {
let config = NodeTokenConfig {
server_url: "http://localhost:3000".to_string(),
registration_token: "test-token".to_string(),
client_instance_id: "test-instance".to_string(),
display_name: "Test Node".to_string(),
ollama_url: "http://localhost:11434".to_string(),
heartbeat_interval_secs: 30,
excluded_poll_check_interval_secs: 30,
data_dir: None,
};
let models: Vec<String> = vec![];
let req = NodeRegisterRequest {
protocol_version: "node.v1".to_string(),
client_instance_id: config.client_instance_id.clone(),
display_name: config.display_name.clone(),
registration_token: config.registration_token.clone(),
capabilities: NodeCapabilities {
runtime: "ollama".to_string(),
models: models
.into_iter()
.map(|m| NodeModelCapability { model: m })
.collect(),
},
};
assert_eq!(req.capabilities.models.len(), 0);
}
}