Function bodies 699 total
run function · rust · L398-L440 (43 LOC)crates/bubbaloop/src/cli/node.rs
pub async fn run(self) -> Result<()> {
match self.action {
None => {
Self::print_help();
Ok(())
}
Some(NodeAction::Init(args)) => init_node(args),
Some(NodeAction::Validate(args)) => validate_node(args),
Some(NodeAction::List(args)) => list_nodes(args).await,
Some(NodeAction::Add(args)) => {
log::warn!("Note: 'bubbaloop node add' is deprecated. Use MCP tool 'install_node' instead.");
add_node(args).await
}
Some(NodeAction::Remove(args)) => {
log::warn!("Note: 'bubbaloop node remove' is deprecated. Use MCP tool 'remove_node' instead.");
remove_node(args).await
}
Some(NodeAction::Instance(args)) => create_instance(args).await,
Some(NodeAction::Install(args)) => handle_install(args).await,
Some(NodeAction::Uninstall(args)) => send_command(&aprint_help function · rust · L441-L469 (29 LOC)crates/bubbaloop/src/cli/node.rs
fn print_help() {
eprintln!("Node management commands\n");
eprintln!("Usage: bubbaloop node <command>\n");
eprintln!("Commands:");
eprintln!(" init Initialize a new node from template");
eprintln!(" validate Validate a node manifest and directory structure");
eprintln!(" list List all registered nodes");
eprintln!(" add Add a node from local path or GitHub URL");
eprintln!(" remove Remove a node from the registry");
eprintln!(" instance Create an instance of a multi-instance node");
eprintln!(
" Example: bubbaloop node instance rtsp-camera terrace -c config.yaml"
);
eprintln!(" search Search the node marketplace");
eprintln!(" discover Discover available nodes with status");
eprintln!(" install Install a node (or from marketplace by name)");
eprintln!(" uninstall Uninstall a node's get_zenoh_session function · rust · L471-L499 (29 LOC)crates/bubbaloop/src/cli/node.rs
pub(crate) async fn get_zenoh_session() -> Result<zenoh::Session> {
// Connect to local zenoh router, or custom endpoint via env var
let mut config = zenoh::Config::default();
// Run as client mode - only connect to router, don't listen
config
.insert_json5("mode", "\"client\"")
.map_err(|e| NodeError::Zenoh(e.to_string()))?;
let endpoint = std::env::var("BUBBALOOP_ZENOH_ENDPOINT")
.unwrap_or_else(|_| "tcp/127.0.0.1:7447".to_string());
config
.insert_json5("connect/endpoints", &format!("[\"{}\"]", endpoint))
.map_err(|e| NodeError::Zenoh(e.to_string()))?;
// Disable all scouting to avoid connecting to remote peers via Tailscale
config
.insert_json5("scouting/multicast/enabled", "false")
.map_err(|e| NodeError::Zenoh(e.to_string()))?;
config
.insert_json5("scouting/gossip/enabled", "false")
.map_err(|e| NodeError::Zenoh(e.to_string()))?;
let session = zenoh::open(config)
init_node function · rust · L500-L561 (62 LOC)crates/bubbaloop/src/cli/node.rs
fn init_node(args: InitArgs) -> Result<()> {
// Validate node name before creating any files
if args.name.is_empty() || args.name.len() > 64 {
return Err(NodeError::CommandFailed(format!(
"Node name must be 1-64 characters, got '{}'",
args.name
)));
}
if !args
.name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(NodeError::CommandFailed(format!(
"Node name '{}' contains invalid characters (only alphanumeric, hyphens, and underscores allowed)",
args.name
)));
}
// Determine output directory (default: ./<name> in current directory)
let output_dir = args
.output
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(".").join(&args.name));
// Use shared template module
let output_dir = templates::create_node_at(
&args.name,
&args.node_type,
&args.author,
&args.devalidate_node function · rust · L562-L615 (54 LOC)crates/bubbaloop/src/cli/node.rs
fn validate_node(args: ValidateArgs) -> Result<()> {
let path = PathBuf::from(&args.path);
// 1. Check node.yaml exists
let manifest_path = path.join("node.yaml");
if !manifest_path.exists() {
println!("FAIL: node.yaml not found at {}", manifest_path.display());
return Err(NodeError::NotFound("node.yaml".into()));
}
println!("OK: node.yaml found");
// 2. Parse manifest
let content = std::fs::read_to_string(&manifest_path)?;
let manifest: serde_yaml::Value = serde_yaml::from_str(&content)
.map_err(|e| NodeError::CommandFailed(format!("Invalid YAML: {}", e)))?;
println!("OK: node.yaml parses correctly");
// 3. Check required fields
let required = ["name", "version", "type"];
for field in required {
if manifest.get(field).is_none() {
println!("FAIL: Missing required field: {}", field);
return Err(NodeError::CommandFailed(format!(
"Missing field: {}",
list_nodes function · rust · L616-L727 (112 LOC)crates/bubbaloop/src/cli/node.rs
async fn list_nodes(args: ListArgs) -> Result<()> {
if args.base && args.instances {
return Err(NodeError::InvalidArgs(
"Cannot use --base and --instances together".into(),
));
}
let session = get_zenoh_session().await?;
// Retry up to 3 times with 1 second delay between retries
let mut best_data: Option<NodeListResponse> = None;
for attempt in 1..=3 {
let replies_result = session
.get("bubbaloop/daemon/api/nodes")
.target(QueryTarget::BestMatching)
.timeout(std::time::Duration::from_secs(30))
.await;
if let Ok(replies) = replies_result {
for reply in replies {
if let Ok(sample) = reply.into_result() {
if let Ok(data) =
serde_json::from_slice::<NodeListResponse>(&sample.payload().to_bytes())
{
if !data.nodes.is_empty() || best_data.is_none() {
discover_nodes_in_subdirs function · rust · L731-L767 (37 LOC)crates/bubbaloop/src/cli/node.rs
fn discover_nodes_in_subdirs(base_path: &Path) -> Vec<(String, String, String)> {
let manifest_field = |manifest: &serde_yaml::Value, key: &str| -> String {
manifest
.get(key)
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string()
};
let mut nodes: Vec<_> = std::fs::read_dir(base_path)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().is_dir())
.filter_map(|entry| {
let yaml_path = entry.path().join("node.yaml");
let content = std::fs::read_to_string(&yaml_path).ok()?;
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(manifest) => {
let subdir = entry.file_name().to_string_lossy().to_string();
Some((
manifest_field(&manifest, "name"),
subdir,
manifest_field(&manifest, "type"),
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
resolve_node_path function · rust · L770-L824 (55 LOC)crates/bubbaloop/src/cli/node.rs
fn resolve_node_path(base_path: &str, subdir: Option<&str>) -> Result<String> {
let base = Path::new(base_path);
if let Some(sub) = subdir {
// Validate subdir to prevent path traversal
if sub.is_empty()
|| sub.contains("..")
|| sub.contains('/')
|| sub.contains('\\')
|| sub.starts_with('.')
{
return Err(NodeError::InvalidArgs(
"subdir must be a simple directory name (no paths, no '..')".into(),
));
}
let node_path = base.join(sub);
let manifest = node_path.join("node.yaml");
if !manifest.exists() {
return Err(NodeError::NotFound(format!(
"No node.yaml found at {}/{}",
base_path, sub
)));
}
return Ok(node_path.to_string_lossy().to_string());
}
// Check for node.yaml at root
let manifest = base.join("node.yaml");
if manifest.exists() {
returadd_node function · rust · L825-L914 (90 LOC)crates/bubbaloop/src/cli/node.rs
async fn add_node(args: AddArgs) -> Result<()> {
// Normalize source URL
let source = normalize_git_url(&args.source);
let base_path = if is_git_url(&source) {
// Clone from GitHub
clone_from_github(&source, args.output.as_deref(), &args.branch)?
} else {
// Local path
let path = Path::new(&args.source);
if !path.exists() {
return Err(NodeError::NotFound(args.source));
}
path.canonicalize()?.to_string_lossy().to_string()
};
// Resolve the actual node path (handles --subdir and multi-node discovery)
let node_path = resolve_node_path(&base_path, args.subdir.as_deref())?;
// Add to daemon via Zenoh
let session = get_zenoh_session().await?;
let payload = serde_json::to_string(&serde_json::json!({
"command": "add",
"node_path": node_path,
"name": args.name,
"config": args.config,
}))?;
let mut node_name: Option<String> = None;
for attcreate_instance function · rust · L933-L1103 (171 LOC)crates/bubbaloop/src/cli/node.rs
async fn create_instance(args: InstanceArgs) -> Result<()> {
// Validate suffix (same rules as node names: alphanumeric, hyphens, underscores)
if args.suffix.is_empty() || args.suffix.len() > 64 {
return Err(NodeError::InvalidArgs(
"Instance suffix must be 1-64 characters".into(),
));
}
if !args
.suffix
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(NodeError::InvalidArgs(
"Instance suffix can only contain alphanumeric characters, hyphens, and underscores"
.into(),
));
}
if args.suffix.starts_with('-') || args.suffix.starts_with('_') {
return Err(NodeError::InvalidArgs(
"Instance suffix cannot start with hyphen or underscore".into(),
));
}
// Build the full instance name: base-suffix
let instance_name = format!("{}-{}", args.base_node, args.suffix);
// First, find the base node to get its pfind_example_config function · rust · L1106-L1126 (21 LOC)crates/bubbaloop/src/cli/node.rs
fn find_example_config(configs_dir: &Path) -> Result<PathBuf> {
let entries: Vec<_> = std::fs::read_dir(configs_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.map(|ext| ext == "yaml" || ext == "yml")
.unwrap_or(false)
})
.collect();
if entries.is_empty() {
return Err(NodeError::NotFound(format!(
"No .yaml config files found in {}",
configs_dir.display()
)));
}
// Return the first one found
Ok(entries[0].path())
}normalize_git_url function · rust · L1127-L1137 (11 LOC)crates/bubbaloop/src/cli/node.rs
fn normalize_git_url(source: &str) -> String {
// If it's an existing local path, return it unchanged
if std::path::Path::new(source).exists() {
return source.to_string();
}
if source.starts_with("https://") || source.starts_with("git@") {
source.to_string()
} else if source.starts_with("github.com/") {
format!("https://{}", source)
} else if source.contains('/')extract_node_name function · rust · L1154-L1170 (17 LOC)crates/bubbaloop/src/cli/node.rs
fn extract_node_name(path: &str) -> Result<String> {
let node_yaml = Path::new(path).join("node.yaml");
if node_yaml.exists() {
let content = std::fs::read_to_string(&node_yaml)?;
let manifest: serde_yaml::Value =
serde_yaml::from_str(&content).map_err(|e| NodeError::CommandFailed(e.to_string()))?;
if let Some(name) = manifest.get("name").and_then(|v| v.as_str()) {
return Ok(name.to_string());
}
}
// Fallback to directory name
Path::new(path)
.file_name()
.map(|s| s.to_string_lossy().to_string())
.ok_or_else(|| NodeError::CommandFailed("Cannot extract node name".into()))
}clone_from_github function · rust · L1171-L1248 (78 LOC)crates/bubbaloop/src/cli/node.rs
fn clone_from_github(url: &str, output: Option<&str>, branch: &str) -> Result<String> {
// Prevent argument injection via branch or URL starting with '-'
if branch.starts_with('-') {
return Err(NodeError::InvalidUrl(format!(
"Invalid branch name: {}",
branch
)));
}
if url.starts_with('-') {
return Err(NodeError::InvalidUrl(format!("Invalid URL: {}", url)));
}
// Extract repo name from URL
let repo_name = url
.trim_end_matches('/')
.trim_end_matches(".git")
.rsplit('/')
.next()
.ok_or_else(|| NodeError::InvalidUrl(url.to_string()))?;
// Determine target directory
let target_dir = if let Some(out) = output {
PathBuf::from(out)
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home)
.join(".bubbaloop")
.join("nodes")
.join(repo_name)
};
if target_remove_node function · rust · L1249-L1289 (41 LOC)crates/bubbaloop/src/cli/node.rs
async fn remove_node(args: RemoveArgs) -> Result<()> {
let session = get_zenoh_session().await?;
let payload = serde_json::to_string(&serde_json::json!({
"command": "remove"
}))?;
let key = format!("bubbaloop/daemon/api/nodes/{}/command", args.name);
let replies: Vec<_> = session
.get(&key)
.payload(payload)
.target(QueryTarget::BestMatching)
.timeout(std::time::Duration::from_secs(30))
.await
.map_err(|e| NodeError::Zenoh(e.to_string()))?
.into_iter()
.collect();
for reply in replies {
if let Ok(sample) = reply.into_result() {
let data: CommandResponse = serde_json::from_slice(&sample.payload().to_bytes())?;
if data.success {
println!("Removed node: {}", args.name);
} else {
return Err(NodeError::CommandFailed(data.message));
}
}
}
if args.delete_files {
// Get node path firsIf a scraper extracted this row, it came from Repobility (https://repobility.com)
send_command function · rust · L1290-L1336 (47 LOC)crates/bubbaloop/src/cli/node.rs
pub(crate) async fn send_command(name: &str, command: &str) -> Result<()> {
let session = get_zenoh_session().await?;
let payload = serde_json::to_string(&serde_json::json!({"command": command}))?;
let key = format!("bubbaloop/daemon/api/nodes/{}/command", name);
let mut last_error = None;
for attempt in 1..=3 {
if let Ok(replies) = session
.get(&key)
.payload(payload.clone())
.target(QueryTarget::BestMatching)
.timeout(std::time::Duration::from_secs(30))
.await
{
for reply in replies {
if let Ok(sample) = reply.into_result() {
let data: CommandResponse =
serde_json::from_slice(&sample.payload().to_bytes())?;
if data.success {
println!("{}", data.message);
if !data.output.is_empty() {
println!("{}", data.output);
search_nodes function · rust · L1612-L1668 (57 LOC)crates/bubbaloop/src/cli/node.rs
fn search_nodes(args: SearchArgs) -> Result<()> {
log::info!(
"node search: query={:?} category={:?} tag={:?}",
args.query,
args.category,
args.tag
);
println!("Refreshing marketplace registry...");
if let Err(e) = registry::refresh_cache() {
log::warn!("registry refresh failed: {}", e);
eprintln!("Warning: could not refresh registry (using cache): {}", e);
}
let all_nodes = registry::load_cached_registry();
if all_nodes.is_empty() {
println!("No nodes found in marketplace registry.");
println!("The registry cache may not have been fetched yet.");
return Ok(());
}
let results = registry::search_registry(
&all_nodes,
&args.query,
args.category.as_deref(),
args.tag.as_deref(),
);
if results.is_empty() {
println!("No nodes matching your search.");
if !args.query.is_empty() || args.category.is_some() || args.tag.is_some() {
discover_nodes function · rust · L1669-L1782 (114 LOC)crates/bubbaloop/src/cli/node.rs
async fn discover_nodes(args: DiscoverArgs) -> Result<()> {
// Refresh marketplace cache
if let Err(e) = registry::refresh_cache() {
log::warn!("registry refresh failed: {}", e);
eprintln!("Warning: could not refresh registry (using cache): {}", e);
}
let all_marketplace = registry::load_cached_registry();
// Query daemon for registered nodes
let registered: Vec<NodeState> = match get_zenoh_session().await {
Ok(session) => {
let result = session
.get("bubbaloop/daemon/api/nodes")
.target(QueryTarget::BestMatching)
.timeout(std::time::Duration::from_secs(5))
.await;
let mut nodes = Vec::new();
if let Ok(replies) = result {
for reply in replies {
if let Ok(sample) = reply.into_result() {
if let Ok(data) =
serde_json::from_slice::<NodeListResponse>(&samplview_logs function · rust · L1783-L1831 (49 LOC)crates/bubbaloop/src/cli/node.rs
async fn view_logs(args: LogsArgs) -> Result<()> {
if args.follow {
// Use journalctl directly for follow mode
let service = format!("bubbaloop-{}.service", args.name);
let status = Command::new("journalctl")
.args(["--user", "-u", &service, "-f", "--no-pager"])
.status()?;
if !status.success() {
// Fallback to systemctl status
let _ = Command::new("systemctl")
.args(["--user", "status", "-l", "--no-pager", &service])
.status();
}
return Ok(());
}
let session = get_zenoh_session().await?;
let key = format!("bubbaloop/daemon/api/nodes/{}/logs", args.name);
let replies: Vec<_> = session
.get(&key)
.target(QueryTarget::BestMatching)
.timeout(std::time::Duration::from_secs(30))
.await
.map_err(|e| NodeError::Zenoh(e.to_string()))?
.into_iter()
.collect();
for reply in replies {
truncate function · rust · L1832-L1848 (17 LOC)crates/bubbaloop/src/cli/node.rs
fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
// Find the last char that *ends* at or before byte position max-3.
let target = max.saturating_sub(3);
let mut end = 0;
for (i, c) in s.char_indices() {
let char_end = i + c.len_utf8();
if char_end > target {
break;
}
end = char_end;
}
format!("{}...", &s[..end])
}test_normalize_git_url_full_https function · rust · L1855-L1860 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_normalize_git_url_full_https() {
assert_eq!(
normalize_git_url("https://github.com/user/repo"),
"https://github.com/user/repo"
);
}test_normalize_git_url_ssh function · rust · L1863-L1868 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_normalize_git_url_ssh() {
assert_eq!(
normalize_git_url("[email protected]:user/repo.git"),
"[email protected]:user/repo.git"
);
}test_normalize_git_url_with_github_prefix function · rust · L1871-L1884 (14 LOC)crates/bubbaloop/src/cli/node.rs
fn test_normalize_git_url_with_github_prefix() {
assert_eq!(
normalize_git_url("github.com/user/repo"),
"https://github.com/user/repo"
);
}
#[test]
fn test_normalize_git_url_shorthand() {
assert_eq!(
normalize_git_url("user/repo"),
"https://github.com/user/repo"
);
}Repobility (the analyzer behind this table) · https://repobility.com
test_normalize_git_url_shorthand function · rust · L1879-L1902 (24 LOC)crates/bubbaloop/src/cli/node.rs
fn test_normalize_git_url_shorthand() {
assert_eq!(
normalize_git_url("user/repo"),
"https://github.com/user/repo"
);
}
#[test]
fn test_normalize_git_url_local_path() {
assert_eq!(normalize_git_url("/path/to/node"), "/path/to/node");
}
#[test]
fn test_normalize_git_url_relative_path() {
// Relative paths starting with . should be preserved as local paths
assert_eq!(normalize_git_url("./node"), "./node");
assert_eq!(normalize_git_url("../my-node"), "../my-node");
assert_eq!(normalize_git_url("./path/to/node"), "./path/to/node");
}
#[test]
fn test_is_git_url_https() {
assert!(is_git_url("https://github.com/user/repo"));
}test_normalize_git_url_relative_path function · rust · L1892-L1897 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_normalize_git_url_relative_path() {
// Relative paths starting with . should be preserved as local paths
assert_eq!(normalize_git_url("./node"), "./node");
assert_eq!(normalize_git_url("../my-node"), "../my-node");
assert_eq!(normalize_git_url("./path/to/node"), "./path/to/node");
}test_is_git_url_https function · rust · L1900-L1975 (76 LOC)crates/bubbaloop/src/cli/node.rs
fn test_is_git_url_https() {
assert!(is_git_url("https://github.com/user/repo"));
}
#[test]
fn test_is_git_url_ssh() {
assert!(is_git_url("[email protected]:user/repo.git"));
}
#[test]
fn test_is_git_url_with_prefix() {
assert!(is_git_url("github.com/user/repo"));
}
#[test]
fn test_is_git_url_local_path() {
assert!(!is_git_url("/path/to/node"));
}
#[test]
fn test_is_git_url_relative_path() {
assert!(!is_git_url("./node"));
}
#[test]
fn test_truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn test_truncate_exact_length() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn test_truncate_long_string() {
assert_eq!(truncate("hello world", 8), "hello...");
}
#[test]
fn test_truncate_very_long_string() {
let long = "This is a very long description that exceeds the maximum length";
test_truncate_very_long_string function · rust · L1940-L1945 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_truncate_very_long_string() {
let long = "This is a very long description that exceeds the maximum length";
// Function takes first (max - 3) chars and adds "..."
assert_eq!(truncate(long, 30), "This is a very long descrip...");
assert_eq!(truncate(long, 30).len(), 30);
}test_truncate_multibyte_utf8 function · rust · L1948-L1962 (15 LOC)crates/bubbaloop/src/cli/node.rs
fn test_truncate_multibyte_utf8() {
// Should not panic when truncation would fall inside a multi-byte char
let s = "cafe\u{0301} is great"; // "café is great" with combining accent
let result = truncate(s, 8);
assert!(result.ends_with("..."));
// Result must be valid UTF-8 and not exceed max bytes
assert!(result.len() <= 8);
// Pure multi-byte: each snowman is 3 bytes, 5 snowmen = 15 bytes
let s = "\u{2603}\u{2603}\u{2603}\u{2603}\u{2603}";
let result = truncate(s, 10);
assert!(result.ends_with("..."));
// 10 - 3 = 7 target bytes, fits 2 snowmen (6 bytes) + "..." = 9
assert_eq!(result, "\u{2603}\u{2603}...");
}test_command_request_serialization function · rust · L1965-L1975 (11 LOC)crates/bubbaloop/src/cli/node.rs
fn test_command_request_serialization() {
let req = serde_json::json!({
"command": "start",
"node_path": "/path/to/node"
});
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"command\""));
assert!(json.contains("\"start\""));
assert!(json.contains("\"node_path\""));
}test_command_response_deserialization function · rust · L1978-L1983 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_command_response_deserialization() {
let json = r#"{"success": true, "message": "Node started", "output": ""}"#;
let response: CommandResponse = serde_json::from_str(json).unwrap();
assert!(response.success);
assert_eq!(response.message, "Node started");
}test_command_response_with_output function · rust · L1986-L1991 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_command_response_with_output() {
let json = r#"{"success": true, "message": "Built", "output": "Compiling..."}"#;
let response: CommandResponse = serde_json::from_str(json).unwrap();
assert!(response.success);
assert_eq!(response.output, "Compiling...");
}Want this analysis on your repo? https://repobility.com/scan/
test_node_state_serialization function · rust · L1994-L2011 (18 LOC)crates/bubbaloop/src/cli/node.rs
fn test_node_state_serialization() {
let node = NodeState {
name: "test-node".to_string(),
path: "/path/to/node".to_string(),
status: "running".to_string(),
installed: true,
autostart_enabled: false,
version: "1.0.0".to_string(),
description: "Test node".to_string(),
node_type: "rust".to_string(),
is_built: true,
base_node: String::new(),
};
let json = serde_json::to_string(&node).unwrap();
assert!(json.contains("test-node"));
assert!(json.contains("running"));
}test_node_list_response_deserialization function · rust · L2014-L2021 (8 LOC)crates/bubbaloop/src/cli/node.rs
fn test_node_list_response_deserialization() {
let json = r#"{"nodes": [{"name": "node1", "path": "/path", "status": "running",
"installed": true, "autostart_enabled": false, "version": "1.0.0",
"description": "Test", "node_type": "rust", "is_built": true}]}"#;
let response: NodeListResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.nodes.len(), 1);
assert_eq!(response.nodes[0].name, "node1");
}test_logs_response_deserialization function · rust · L2024-L2030 (7 LOC)crates/bubbaloop/src/cli/node.rs
fn test_logs_response_deserialization() {
let json = r#"{"lines": ["line1", "line2"], "success": true}"#;
let response: LogsResponse = serde_json::from_str(json).unwrap();
assert!(response.success);
assert_eq!(response.lines.len(), 2);
assert_eq!(response.lines[0], "line1");
}test_logs_response_with_error function · rust · L2033-L2038 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_logs_response_with_error() {
let json = r#"{"lines": [], "success": false, "error": "Node not found"}"#;
let response: LogsResponse = serde_json::from_str(json).unwrap();
assert!(!response.success);
assert_eq!(response.error, Some("Node not found".to_string()));
}test_clone_rejects_branch_argument_injection function · rust · L2041-L2073 (33 LOC)crates/bubbaloop/src/cli/node.rs
fn test_clone_rejects_branch_argument_injection() {
// Branch starting with '-' could be interpreted as a git flag
let result = clone_from_github("https://github.com/user/repo", None, "--upload-pack=evil");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Invalid branch name"));
}
#[test]
fn test_clone_rejects_url_argument_injection() {
let result = clone_from_github("--upload-pack=evil", None, "main");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Invalid URL"));
}
#[test]
fn test_clone_accepts_valid_branch() {
// This will fail at the git clone step (no network), but should not
// fail at the argument validation step. We check by verifying the error
// is NOT about an invalid branch/URL.
let result = clone_from_github(
"https://github.com/user/repo",
test_clone_rejects_url_argument_injection function · rust · L2050-L2055 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_clone_rejects_url_argument_injection() {
let result = clone_from_github("--upload-pack=evil", None, "main");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Invalid URL"));
}test_clone_accepts_valid_branch function · rust · L2058-L2129 (72 LOC)crates/bubbaloop/src/cli/node.rs
fn test_clone_accepts_valid_branch() {
// This will fail at the git clone step (no network), but should not
// fail at the argument validation step. We check by verifying the error
// is NOT about an invalid branch/URL.
let result = clone_from_github(
"https://github.com/user/repo",
Some("/tmp/bubbaloop-test-nonexistent"),
"main",
);
// Either succeeds or fails for a reason other than argument injection
if let Err(e) = result {
let msg = e.to_string();
assert!(!msg.contains("Invalid branch name"));
assert!(!msg.contains("Invalid URL"));
}
}
/// Validate node name checking logic (mirrors submit_create_node_form validation)
fn is_valid_node_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !name.starts_with(is_valid_node_name function · rust · L2076-L2083 (8 LOC)crates/bubbaloop/src/cli/node.rs
fn is_valid_node_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !name.starts_with('-')
&& !name.starts_with('.')
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
test_valid_node_names function · rust · L2086-L2091 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_valid_node_names() {
assert!(is_valid_node_name("my-node"));
assert!(is_valid_node_name("my_node"));
assert!(is_valid_node_name("sensor1"));
assert!(is_valid_node_name("MyNode"));
}test_invalid_node_names_special_chars function · rust · L2101-L2106 (6 LOC)crates/bubbaloop/src/cli/node.rs
fn test_invalid_node_names_special_chars() {
assert!(!is_valid_node_name("node;evil"));
assert!(!is_valid_node_name("node name"));
assert!(!is_valid_node_name("node&evil"));
assert!(!is_valid_node_name("$HOME"));
}test_resolve_node_path_single_node_at_root function · rust · L2118-L2129 (12 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_single_node_at_root() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("node.yaml"),
"name: test-node\nversion: \"0.1.0\"\ntype: rust",
)
.unwrap();
let result = resolve_node_path(dir.path().to_str().unwrap(), None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), dir.path().to_str().unwrap());
}test_resolve_node_path_with_subdir function · rust · L2132-L2145 (14 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_with_subdir() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("my-node");
std::fs::create_dir(&subdir).unwrap();
std::fs::write(
subdir.join("node.yaml"),
"name: my-node\nversion: \"0.1.0\"\ntype: rust",
)
.unwrap();
let result = resolve_node_path(dir.path().to_str().unwrap(), Some("my-node"));
assert!(result.is_ok());
assert!(result.unwrap().ends_with("my-node"));
}test_resolve_node_path_subdir_missing_manifest function · rust · L2148-L2157 (10 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_subdir_missing_manifest() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("empty-dir");
std::fs::create_dir(&subdir).unwrap();
let result = resolve_node_path(dir.path().to_str().unwrap(), Some("empty-dir"));
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("No node.yaml found"));
}test_resolve_node_path_multi_node_discovery function · rust · L2160-L2182 (23 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_multi_node_discovery() {
let dir = tempfile::tempdir().unwrap();
// Create two node subdirectories
for name in &["camera", "weather"] {
let subdir = dir.path().join(name);
std::fs::create_dir(&subdir).unwrap();
std::fs::write(
subdir.join("node.yaml"),
format!("name: {}\nversion: \"0.1.0\"\ntype: rust", name),
)
.unwrap();
}
// No node.yaml at root, no --subdir -> should discover and error
let result = resolve_node_path(dir.path().to_str().unwrap(), None);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Found 2 node(s)"));
assert!(err.contains("camera"));
assert!(err.contains("weather"));
assert!(err.contains("--subdir"));
}test_resolve_node_path_no_nodes_found function · rust · L2185-L2192 (8 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_no_nodes_found() {
let dir = tempfile::tempdir().unwrap();
// Empty directory, no node.yaml anywhere
let result = resolve_node_path(dir.path().to_str().unwrap(), None);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("No node.yaml found"));
}test_resolve_node_path_rejects_path_traversal function · rust · L2195-L2216 (22 LOC)crates/bubbaloop/src/cli/node.rs
fn test_resolve_node_path_rejects_path_traversal() {
let dir = tempfile::tempdir().unwrap();
// ".." traversal
let result = resolve_node_path(dir.path().to_str().unwrap(), Some("../etc"));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("simple directory name"));
// Slash in subdir
let result = resolve_node_path(dir.path().to_str().unwrap(), Some("foo/bar"));
assert!(result.is_err());
// Hidden directory
let result = resolve_node_path(dir.path().to_str().unwrap(), Some(".hidden"));
assert!(result.is_err());
// Empty string
let result = resolve_node_path(dir.path().to_str().unwrap(), Some(""));
assert!(result.is_err());
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
test_discover_nodes_in_subdirs function · rust · L2219-L2239 (21 LOC)crates/bubbaloop/src/cli/node.rs
fn test_discover_nodes_in_subdirs() {
let dir = tempfile::tempdir().unwrap();
for (name, node_type) in &[("sensor", "rust"), ("bridge", "python")] {
let subdir = dir.path().join(name);
std::fs::create_dir(&subdir).unwrap();
std::fs::write(
subdir.join("node.yaml"),
format!("name: {}\nversion: \"0.1.0\"\ntype: {}", name, node_type),
)
.unwrap();
}
let nodes = discover_nodes_in_subdirs(dir.path());
assert_eq!(nodes.len(), 2);
// Sorted by name
assert_eq!(nodes[0].0, "bridge");
assert_eq!(nodes[0].2, "python");
assert_eq!(nodes[1].0, "sensor");
assert_eq!(nodes[1].2, "rust");
}test_node_state_base_node_deserialization function · rust · L2242-L2249 (8 LOC)crates/bubbaloop/src/cli/node.rs
fn test_node_state_base_node_deserialization() {
let json = r#"{"name": "rtsp-camera-terrace", "path": "/path", "status": "running",
"installed": true, "autostart_enabled": false, "version": "1.0.0",
"description": "Test", "node_type": "rust", "is_built": true,
"base_node": "rtsp-camera"}"#;
let node: NodeState = serde_json::from_str(json).unwrap();
assert_eq!(node.base_node, "rtsp-camera");
}test_node_state_base_node_defaults_empty function · rust · L2252-L2258 (7 LOC)crates/bubbaloop/src/cli/node.rs
fn test_node_state_base_node_defaults_empty() {
let json = r#"{"name": "openmeteo", "path": "/path", "status": "stopped",
"installed": true, "autostart_enabled": false, "version": "1.0.0",
"description": "Weather", "node_type": "rust", "is_built": true}"#;
let node: NodeState = serde_json::from_str(json).unwrap();
assert_eq!(node.base_node, "");
}