huskies: merge 515_story_add_a_debug_mcp_tool_to_dump_the_in_memory_crdt_state_for_inspection
This commit is contained in:
@@ -265,6 +265,45 @@ pub(super) fn tool_move_story(args: &Value, ctx: &AppContext) -> Result<String,
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
/// MCP tool: dump the raw in-memory CRDT state for debugging.
|
||||
///
|
||||
/// **Debug tool only** — for normal pipeline introspection use `get_pipeline_status`.
|
||||
pub(super) fn tool_dump_crdt(args: &Value) -> Result<String, String> {
|
||||
let story_id_filter = args.get("story_id").and_then(|v| v.as_str());
|
||||
let dump = crate::crdt_state::dump_crdt_state(story_id_filter);
|
||||
|
||||
let items: Vec<Value> = dump
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
json!({
|
||||
"story_id": item.story_id,
|
||||
"stage": item.stage,
|
||||
"name": item.name,
|
||||
"agent": item.agent,
|
||||
"retry_count": item.retry_count,
|
||||
"blocked": item.blocked,
|
||||
"depends_on": item.depends_on,
|
||||
"content_index": item.content_index,
|
||||
"is_deleted": item.is_deleted,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"metadata": {
|
||||
"in_memory_state_loaded": dump.in_memory_state_loaded,
|
||||
"total_items": dump.total_items,
|
||||
"total_ops_in_list": dump.total_ops_in_list,
|
||||
"max_seq_in_list": dump.max_seq_in_list,
|
||||
"persisted_ops_count": dump.persisted_ops_count,
|
||||
"pending_persist_ops_count": null,
|
||||
},
|
||||
"items": items,
|
||||
}))
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
/// MCP tool: count lines in a specific file relative to the project root.
|
||||
pub(super) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let file_path = args
|
||||
@@ -751,4 +790,49 @@ mod tests {
|
||||
.contains("not found in any pipeline stage")
|
||||
);
|
||||
}
|
||||
|
||||
// ── dump_crdt tool tests ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tool_dump_crdt_returns_valid_json() {
|
||||
let result = tool_dump_crdt(&json!({})).unwrap();
|
||||
let parsed: Value = serde_json::from_str(&result).expect("result must be valid JSON");
|
||||
assert!(parsed["metadata"].is_object(), "must have metadata object");
|
||||
assert!(parsed["items"].is_array(), "must have items array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_dump_crdt_metadata_has_required_fields() {
|
||||
let result = tool_dump_crdt(&json!({})).unwrap();
|
||||
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||
let meta = &parsed["metadata"];
|
||||
assert!(meta["in_memory_state_loaded"].is_boolean());
|
||||
assert!(meta["total_items"].is_number());
|
||||
assert!(meta["total_ops_in_list"].is_number());
|
||||
assert!(meta["max_seq_in_list"].is_number());
|
||||
assert!(meta["persisted_ops_count"].is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_dump_crdt_with_story_id_filter_returns_valid_json() {
|
||||
let result =
|
||||
tool_dump_crdt(&json!({"story_id": "9999_story_nonexistent"})).unwrap();
|
||||
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||
assert!(parsed["items"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_crdt_in_tools_list() {
|
||||
use super::super::handle_tools_list;
|
||||
let resp = handle_tools_list(Some(json!(1)));
|
||||
let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
|
||||
let tool = tools.iter().find(|t| t["name"] == "dump_crdt");
|
||||
assert!(tool.is_some(), "dump_crdt missing from tools list");
|
||||
let t = tool.unwrap();
|
||||
assert!(
|
||||
t["description"].as_str().unwrap().to_lowercase().contains("debug"),
|
||||
"description must mention this is a debug tool"
|
||||
);
|
||||
assert!(t["inputSchema"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user