Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(gh pr:*)"
]
}
}
38 changes: 35 additions & 3 deletions ant-core/src/node/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, Sse};
use axum::response::{Html, IntoResponse};
use axum::routing::{delete, get, post};
use axum::routing::{get, post};
use axum::{Json, Router};
use tokio::sync::broadcast;
use tokio::sync::RwLock;
Expand All @@ -19,7 +19,7 @@ use crate::node::daemon::supervisor::Supervisor;
use crate::node::events::NodeEvent;
use crate::node::registry::NodeRegistry;
use crate::node::types::{
AddNodeOpts, AddNodeResult, DaemonConfig, DaemonStatus, NodeStarted, NodeStatus,
AddNodeOpts, AddNodeResult, DaemonConfig, DaemonStatus, NodeInfo, NodeStarted, NodeStatus,
NodeStatusResult, NodeStatusSummary, NodeStopped, RemoveNodeResult, ResetResult,
StartNodeResult, StopNodeResult,
};
Expand Down Expand Up @@ -104,7 +104,10 @@ fn build_router(state: Arc<AppState>) -> Router {
.route("/api/v1/events", get(get_events))
.route("/api/v1/nodes/status", get(get_nodes_status))
.route("/api/v1/nodes", post(post_nodes))
.route("/api/v1/nodes/{id}", delete(delete_node))
.route(
"/api/v1/nodes/{id}",
get(get_node_detail).delete(delete_node),
)
.route("/api/v1/nodes/{id}/start", post(post_start_node))
.route("/api/v1/nodes/start-all", post(post_start_all))
.route("/api/v1/nodes/{id}/stop", post(post_stop_node))
Expand Down Expand Up @@ -194,6 +197,35 @@ async fn get_nodes_status(State(state): State<Arc<AppState>>) -> Json<NodeStatus
})
}

/// GET /api/v1/nodes/:id — Get full detail for a single node.
async fn get_node_detail(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
) -> std::result::Result<Json<NodeInfo>, (StatusCode, Json<serde_json::Value>)> {
let registry = state.registry.read().await;
let config = match registry.get(id) {
Ok(config) => config.clone(),
Err(_) => {
return Err((
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": format!("Node not found: {id}") })),
))
}
};

let supervisor = state.supervisor.read().await;
let status = supervisor.node_status(id).unwrap_or(NodeStatus::Stopped);
let pid = supervisor.node_pid(id);
let uptime_secs = supervisor.node_uptime_secs(id);

Ok(Json(NodeInfo {
config,
status,
pid,
uptime_secs,
}))
}

/// POST /api/v1/nodes — Add one or more nodes to the registry.
async fn post_nodes(
State(state): State<Arc<AppState>>,
Expand Down
92 changes: 91 additions & 1 deletion ant-core/tests/daemon_integration.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::net::IpAddr;

use ant_core::node::binary::NoopProgress;
use ant_core::node::daemon::server;
use ant_core::node::registry::NodeRegistry;
use ant_core::node::types::{DaemonConfig, DaemonStatus};
use ant_core::node::types::{AddNodeOpts, BinarySource, DaemonConfig, DaemonStatus, NodeInfo};

fn test_config(dir: &tempfile::TempDir) -> DaemonConfig {
DaemonConfig {
Expand Down Expand Up @@ -142,3 +143,92 @@ async fn console_returns_html() {
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}

/// Create a fake binary that responds to `--version`.
fn create_fake_binary(dir: &std::path::Path) -> std::path::PathBuf {
#[cfg(unix)]
{
let binary_path = dir.join("fake-antnode");
std::fs::write(&binary_path, "#!/bin/sh\necho \"antnode 0.1.0-test\"\n").unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&binary_path, std::fs::Permissions::from_mode(0o755)).unwrap();
binary_path
}
#[cfg(windows)]
{
let binary_path = dir.join("fake-antnode.cmd");
std::fs::write(&binary_path, "@echo off\r\necho antnode 0.1.0-test\r\n").unwrap();
binary_path
}
}

#[tokio::test]
async fn get_node_detail_returns_full_config() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let reg_path = config.registry_path.clone();

// Add a node to the registry
let binary = create_fake_binary(dir.path());
let opts = AddNodeOpts {
count: 1,
rewards_address: "0x1234567890abcdef1234567890abcdef12345678".to_string(),
data_dir_path: Some(dir.path().join("data")),
log_dir_path: Some(dir.path().join("logs")),
binary_source: BinarySource::LocalPath(binary),
..Default::default()
};
ant_core::node::add_nodes(opts, &reg_path, &NoopProgress)
.await
.unwrap();

// Start the daemon
let registry = NodeRegistry::load(&reg_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();

// GET /api/v1/nodes/1 — should return full config + runtime state
let url = format!("http://{addr}/api/v1/nodes/1");
let resp = reqwest::get(&url).await.unwrap();
assert!(resp.status().is_success());

let detail: NodeInfo = resp.json().await.unwrap();
assert_eq!(detail.config.id, 1);
assert_eq!(detail.config.service_name, "node1");
assert_eq!(
detail.config.rewards_address,
"0x1234567890abcdef1234567890abcdef12345678"
);
assert!(detail.config.data_dir.exists());
assert_eq!(detail.status, ant_core::node::types::NodeStatus::Stopped);
assert!(detail.pid.is_none());
assert!(detail.uptime_secs.is_none());

// Verify JSON includes flattened config fields (serde flatten)
let raw: serde_json::Value = reqwest::get(&url).await.unwrap().json().await.unwrap();
assert!(raw.get("id").is_some(), "should have flattened id");
assert!(
raw.get("service_name").is_some(),
"should have flattened service_name"
);
assert!(
raw.get("data_dir").is_some(),
"should have flattened data_dir"
);
assert!(
raw.get("rewards_address").is_some(),
"should have flattened rewards_address"
);
assert!(raw.get("status").is_some());

// GET /api/v1/nodes/999 — should 404
let resp_404 = reqwest::get(format!("http://{addr}/api/v1/nodes/999"))
.await
.unwrap();
assert_eq!(resp_404.status(), 404);

shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Loading