huskies: merge 720_refactor_add_mesh_status_mcp_tool_read_only_peer_mesh_diagnostics
This commit is contained in:
@@ -130,6 +130,37 @@ pub(crate) fn tool_get_version(ctx: &AppContext) -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MCP tool: return read-only peer mesh status.
|
||||||
|
///
|
||||||
|
/// Returns a JSON object with `local_node_id` and a `peers` array. Each peer
|
||||||
|
/// has `node_id`, `pubkey` (same value — the hex-encoded Ed25519 public key),
|
||||||
|
/// `last_seen` (Unix timestamp), and `is_self` (true for the local node).
|
||||||
|
///
|
||||||
|
/// This tool is read-only and does not mutate any state.
|
||||||
|
pub(crate) fn tool_mesh_status(_args: &Value) -> Result<String, String> {
|
||||||
|
let local_id = crate::crdt_state::our_node_id().unwrap_or_default();
|
||||||
|
let nodes = crate::crdt_state::read_all_node_presence().unwrap_or_default();
|
||||||
|
|
||||||
|
let peers: Vec<serde_json::Value> = nodes
|
||||||
|
.into_iter()
|
||||||
|
.map(|n| {
|
||||||
|
let is_self = n.node_id == local_id;
|
||||||
|
json!({
|
||||||
|
"node_id": n.node_id,
|
||||||
|
"pubkey": n.node_id,
|
||||||
|
"last_seen": n.last_seen,
|
||||||
|
"is_self": is_self,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
serde_json::to_string_pretty(&json!({
|
||||||
|
"local_node_id": local_id,
|
||||||
|
"peers": peers,
|
||||||
|
}))
|
||||||
|
.map_err(|e| format!("Serialization error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
/// MCP tool: count lines in a specific file relative to the project root.
|
/// MCP tool: count lines in a specific file relative to the project root.
|
||||||
pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||||
let file_path = args
|
let file_path = args
|
||||||
@@ -148,6 +179,74 @@ pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, St
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_mesh_status_returns_expected_shape_with_local_node_flagged() {
|
||||||
|
// Initialise a test CRDT so our_node_id() and write_node_presence() work.
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
|
||||||
|
let local_id = crate::crdt_state::our_node_id()
|
||||||
|
.expect("CRDT must be initialised before calling tool_mesh_status");
|
||||||
|
|
||||||
|
// Write the local node's presence so it appears in read_all_node_presence().
|
||||||
|
crate::crdt_state::write_node_presence(
|
||||||
|
&local_id,
|
||||||
|
"ws://127.0.0.1:3001/crdt-sync",
|
||||||
|
1_700_000_000.0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = tool_mesh_status(&json!({})).expect("tool_mesh_status must not fail");
|
||||||
|
let parsed: serde_json::Value =
|
||||||
|
serde_json::from_str(&result).expect("result must be valid JSON");
|
||||||
|
|
||||||
|
// local_node_id field present
|
||||||
|
assert_eq!(
|
||||||
|
parsed["local_node_id"].as_str(),
|
||||||
|
Some(local_id.as_str()),
|
||||||
|
"local_node_id must match our_node_id"
|
||||||
|
);
|
||||||
|
|
||||||
|
// peers array present
|
||||||
|
let peers = parsed["peers"].as_array().expect("peers must be an array");
|
||||||
|
assert!(!peers.is_empty(), "peers must include the local node");
|
||||||
|
|
||||||
|
// Find the local peer and verify it is flagged is_self: true
|
||||||
|
let self_peer = peers
|
||||||
|
.iter()
|
||||||
|
.find(|p| p["node_id"].as_str() == Some(&local_id))
|
||||||
|
.expect("local node must appear in peers list");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
self_peer["is_self"].as_bool(),
|
||||||
|
Some(true),
|
||||||
|
"local node must have is_self: true"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
self_peer["pubkey"].as_str(),
|
||||||
|
Some(local_id.as_str()),
|
||||||
|
"pubkey must equal node_id"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
self_peer["last_seen"].is_number(),
|
||||||
|
"last_seen must be a number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_mesh_status_is_read_only_no_panic_without_crdt() {
|
||||||
|
// When CRDT is not initialised (different process / test order),
|
||||||
|
// the tool must return a valid (empty) result rather than panicking.
|
||||||
|
let result = tool_mesh_status(&json!({}));
|
||||||
|
// Must succeed — empty peers list is acceptable
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"tool_mesh_status must not error without CRDT"
|
||||||
|
);
|
||||||
|
let parsed: serde_json::Value =
|
||||||
|
serde_json::from_str(&result.unwrap()).expect("must be valid JSON");
|
||||||
|
assert!(parsed["peers"].is_array(), "peers field must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_get_server_logs_no_args_returns_string() {
|
fn tool_get_server_logs_no_args_returns_string() {
|
||||||
let result = tool_get_server_logs(&json!({})).unwrap();
|
let result = tool_get_server_logs(&json!({})).unwrap();
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ pub(super) async fn handle_tools_call(
|
|||||||
"purge_story" => story_tools::tool_purge_story(&args, ctx),
|
"purge_story" => story_tools::tool_purge_story(&args, ctx),
|
||||||
// Debug CRDT dump (story 515)
|
// Debug CRDT dump (story 515)
|
||||||
"dump_crdt" => diagnostics::tool_dump_crdt(&args),
|
"dump_crdt" => diagnostics::tool_dump_crdt(&args),
|
||||||
|
// Read-only peer mesh diagnostics (story 720)
|
||||||
|
"mesh_status" => diagnostics::tool_mesh_status(&args),
|
||||||
// Arbitrary pipeline movement
|
// Arbitrary pipeline movement
|
||||||
"move_story" => diagnostics::tool_move_story(&args, ctx),
|
"move_story" => diagnostics::tool_move_story(&args, ctx),
|
||||||
// Unblock story
|
// Unblock story
|
||||||
|
|||||||
@@ -1087,6 +1087,14 @@ pub(super) fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mesh_status",
|
||||||
|
"description": "Return read-only peer mesh status: the local node id and a list of known peers, each with node_id, pubkey, last_seen timestamp, and is_self flag. Does not mutate state.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
@@ -1163,7 +1171,8 @@ mod tests {
|
|||||||
assert!(names.contains(&"dump_crdt"));
|
assert!(names.contains(&"dump_crdt"));
|
||||||
assert!(names.contains(&"get_version"));
|
assert!(names.contains(&"get_version"));
|
||||||
assert!(names.contains(&"remove_criterion"));
|
assert!(names.contains(&"remove_criterion"));
|
||||||
assert_eq!(tools.len(), 66);
|
assert!(names.contains(&"mesh_status"));
|
||||||
|
assert_eq!(tools.len(), 67);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user