story-kit: merge 129_story_test_coverage_http_mcp_rs
This commit is contained in:
@@ -2394,4 +2394,626 @@ mod tests {
|
|||||||
assert!(parsed.get("gate_output").is_some());
|
assert!(parsed.get("gate_output").is_some());
|
||||||
assert!(parsed.get("message").is_some());
|
assert!(parsed.get("message").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── HTTP handler tests (TestClient) ───────────────────────────
|
||||||
|
|
||||||
|
fn test_mcp_app(ctx: std::sync::Arc<AppContext>) -> impl poem::Endpoint {
|
||||||
|
use poem::EndpointExt;
|
||||||
|
poem::Route::new()
|
||||||
|
.at("/mcp", poem::post(mcp_post_handler).get(mcp_get_handler))
|
||||||
|
.data(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_body_json(resp: poem::test::TestResponse) -> Value {
|
||||||
|
let body = resp.0.into_body().into_string().await.unwrap();
|
||||||
|
serde_json::from_str(&body).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_json_mcp<E: poem::Endpoint>(
|
||||||
|
cli: &poem::test::TestClient<E>,
|
||||||
|
payload: &str,
|
||||||
|
) -> Value {
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(payload.to_string())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
read_body_json(resp).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_get_handler_returns_405() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli.get("/mcp").send().await;
|
||||||
|
assert_eq!(resp.0.status(), poem::http::StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_invalid_content_type_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "text/plain")
|
||||||
|
.body("{}")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
let body = read_body_json(resp).await;
|
||||||
|
assert!(body.get("error").is_some(), "expected error field: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_invalid_json_returns_parse_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body("not-valid-json")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
let body = read_body_json(resp).await;
|
||||||
|
assert!(body.get("error").is_some(), "expected error field: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_wrong_jsonrpc_version_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let body = post_json_mcp(
|
||||||
|
&cli,
|
||||||
|
r#"{"jsonrpc":"1.0","id":1,"method":"initialize","params":{}}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
body["error"]["message"].as_str().unwrap_or("").contains("version"),
|
||||||
|
"expected version error: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_notification_with_null_id_returns_accepted() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.0.status(), poem::http::StatusCode::ACCEPTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_notification_with_explicit_null_id_returns_accepted() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(
|
||||||
|
r#"{"jsonrpc":"2.0","id":null,"method":"notifications/initialized","params":{}}"#,
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.0.status(), poem::http::StatusCode::ACCEPTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_missing_id_non_notification_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let body = post_json_mcp(
|
||||||
|
&cli,
|
||||||
|
r#"{"jsonrpc":"2.0","method":"initialize","params":{}}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(body.get("error").is_some(), "expected error: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_unknown_method_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let body = post_json_mcp(
|
||||||
|
&cli,
|
||||||
|
r#"{"jsonrpc":"2.0","id":1,"method":"bogus/method","params":{}}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
body["error"]["message"].as_str().unwrap_or("").contains("Unknown method"),
|
||||||
|
"expected unknown method error: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_initialize_returns_capabilities() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let body = post_json_mcp(
|
||||||
|
&cli,
|
||||||
|
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(body["result"]["protocolVersion"], "2025-03-26");
|
||||||
|
assert_eq!(body["result"]["serverInfo"]["name"], "story-kit");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_tools_list_returns_tools() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let body = post_json_mcp(
|
||||||
|
&cli,
|
||||||
|
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(body["result"]["tools"].is_array());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_sse_returns_event_stream_content_type() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("accept", "text/event-stream")
|
||||||
|
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.0.headers().get("content-type").unwrap(),
|
||||||
|
"text/event-stream"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_sse_get_agent_output_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("accept", "text/event-stream")
|
||||||
|
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_output","arguments":{}}}"#)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.0.headers().get("content-type").unwrap(),
|
||||||
|
"text/event-stream",
|
||||||
|
"expected SSE content-type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_sse_get_agent_output_missing_agent_name() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("accept", "text/event-stream")
|
||||||
|
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_output","arguments":{"story_id":"1_test"}}}"#)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.0.headers().get("content-type").unwrap(),
|
||||||
|
"text/event-stream",
|
||||||
|
"expected SSE content-type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mcp_post_sse_get_agent_output_no_agent_returns_sse_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
|
||||||
|
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
|
||||||
|
let resp = cli
|
||||||
|
.post("/mcp")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("accept", "text/event-stream")
|
||||||
|
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_output","arguments":{"story_id":"99_nope","agent_name":"bot"}}}"#)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.0.headers().get("content-type").unwrap(),
|
||||||
|
"text/event-stream"
|
||||||
|
);
|
||||||
|
let body = resp.0.into_body().into_string().await.unwrap();
|
||||||
|
assert!(body.contains("data:"), "expected SSE data prefix: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_get_server_logs tests ────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_server_logs_no_args_returns_string() {
|
||||||
|
let result = tool_get_server_logs(&json!({})).unwrap();
|
||||||
|
// Returns recent log lines (possibly empty in tests) — just verify no panic
|
||||||
|
let _ = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_server_logs_with_filter_returns_matching_lines() {
|
||||||
|
let result =
|
||||||
|
tool_get_server_logs(&json!({"filter": "xyz_unlikely_match_999"})).unwrap();
|
||||||
|
assert_eq!(result, "", "filter with no matches should return empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_server_logs_with_line_limit() {
|
||||||
|
let result = tool_get_server_logs(&json!({"lines": 5})).unwrap();
|
||||||
|
assert!(result.lines().count() <= 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_server_logs_max_cap_is_1000() {
|
||||||
|
// Lines > 1000 are capped — just verify it returns without error
|
||||||
|
let result = tool_get_server_logs(&json!({"lines": 9999})).unwrap();
|
||||||
|
let _ = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_list_worktrees tests ─────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_list_worktrees_empty_dir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_list_worktrees(&ctx).unwrap();
|
||||||
|
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||||
|
assert!(parsed.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_accept_story tests ───────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_accept_story_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_accept_story(&json!({}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_accept_story_nonexistent_story_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
setup_git_repo_in(tmp.path());
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
// No story file in current/ — should fail
|
||||||
|
let result = tool_accept_story(&json!({"story_id": "99_nonexistent"}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_check_criterion tests ────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_check_criterion_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_check_criterion(&json!({"criterion_index": 0}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_check_criterion_missing_criterion_index() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_check_criterion(&json!({"story_id": "1_test"}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("criterion_index"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_check_criterion_marks_unchecked_item() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
setup_git_repo_in(tmp.path());
|
||||||
|
let current_dir = tmp.path().join(".story_kit").join("work").join("2_current");
|
||||||
|
fs::create_dir_all(¤t_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
current_dir.join("1_test.md"),
|
||||||
|
"---\nname: Test\n---\n## AC\n- [ ] First criterion\n- [x] Already done\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(["add", "."])
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(["commit", "-m", "add story"])
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_check_criterion(
|
||||||
|
&json!({"story_id": "1_test", "criterion_index": 0}),
|
||||||
|
&ctx,
|
||||||
|
);
|
||||||
|
assert!(result.is_ok(), "Expected ok: {result:?}");
|
||||||
|
assert!(result.unwrap().contains("Criterion 0 checked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_get_agent_config tests ───────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_get_agent_config_no_project_toml_returns_default_agent() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
// No project.toml → default config with one fallback agent
|
||||||
|
let result = tool_get_agent_config(&ctx).unwrap();
|
||||||
|
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||||
|
// Default config contains one agent entry with default values
|
||||||
|
assert_eq!(parsed.len(), 1, "default config should have one fallback agent");
|
||||||
|
assert!(parsed[0].get("name").is_some());
|
||||||
|
assert!(parsed[0].get("role").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_get_agent_output_poll tests ─────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_get_agent_output_poll_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_get_agent_output_poll(&json!({"agent_name": "bot"}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_get_agent_output_poll_missing_agent_name() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result =
|
||||||
|
tool_get_agent_output_poll(&json!({"story_id": "1_test"}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("agent_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_get_agent_output_poll_no_agent_falls_back_to_empty_log() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
// No agent registered, no log file → returns empty response from log fallback
|
||||||
|
let result = tool_get_agent_output_poll(
|
||||||
|
&json!({"story_id": "99_nope", "agent_name": "bot"}),
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||||
|
assert_eq!(parsed["done"], true);
|
||||||
|
assert_eq!(parsed["event_count"], 0);
|
||||||
|
assert!(
|
||||||
|
parsed["message"].as_str().unwrap_or("").contains("No agent"),
|
||||||
|
"expected 'No agent' message: {parsed}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_get_agent_output_poll_with_running_agent_returns_empty_events() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
// Inject a running agent — no events broadcast yet
|
||||||
|
ctx.agents
|
||||||
|
.inject_test_agent("10_story", "worker", crate::agents::AgentStatus::Running);
|
||||||
|
let result = tool_get_agent_output_poll(
|
||||||
|
&json!({"story_id": "10_story", "agent_name": "worker"}),
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||||
|
assert_eq!(parsed["done"], false);
|
||||||
|
assert_eq!(parsed["event_count"], 0);
|
||||||
|
assert!(parsed["events"].is_array());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Missing-arg tests for async tools ────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_stop_agent_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_stop_agent(&json!({"agent_name": "bot"}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_stop_agent_missing_agent_name() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_stop_agent(&json!({"story_id": "1_test"}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("agent_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_start_agent_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_start_agent(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_create_worktree_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_create_worktree(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_remove_worktree_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_remove_worktree(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tool_request_qa_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_request_qa(&json!({}), &ctx).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── parse_test_cases additional coverage ─────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_test_cases_null_value_returns_empty() {
|
||||||
|
let null_val = json!(null);
|
||||||
|
let result = parse_test_cases(Some(&null_val)).unwrap();
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_test_cases_non_array_returns_error() {
|
||||||
|
let obj = json!({"invalid": "input"});
|
||||||
|
let result = parse_test_cases(Some(&obj));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("Expected array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_test_cases_missing_name_returns_error() {
|
||||||
|
let input = json!([{"status": "pass"}]);
|
||||||
|
let result = parse_test_cases(Some(&input));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_test_cases_missing_status_returns_error() {
|
||||||
|
let input = json!([{"name": "test1"}]);
|
||||||
|
let result = parse_test_cases(Some(&input));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── json_rpc_error_response direct test ──────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_rpc_error_response_builds_json_response() {
|
||||||
|
let resp = json_rpc_error_response(Some(json!(42)), -32600, "test error".into());
|
||||||
|
assert_eq!(resp.status(), poem::http::StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get("content-type").unwrap(),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── request_qa in tools list ──────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_qa_in_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"] == "request_qa");
|
||||||
|
assert!(tool.is_some(), "request_qa missing from tools list");
|
||||||
|
let t = tool.unwrap();
|
||||||
|
let required = t["inputSchema"]["required"].as_array().unwrap();
|
||||||
|
let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
|
||||||
|
assert!(req_names.contains(&"story_id"));
|
||||||
|
// agent_name is optional
|
||||||
|
assert!(!req_names.contains(&"agent_name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_validate_stories with file content ───────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_validate_stories_with_valid_story() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let current_dir = tmp.path().join(".story_kit").join("work").join("2_current");
|
||||||
|
fs::create_dir_all(¤t_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
current_dir.join("1_test.md"),
|
||||||
|
"---\nname: \"Valid Story\"\n---\n## AC\n- [ ] First\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_validate_stories(&ctx).unwrap();
|
||||||
|
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||||
|
assert_eq!(parsed.len(), 1);
|
||||||
|
assert_eq!(parsed[0]["valid"], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_validate_stories_with_invalid_front_matter() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let current_dir = tmp.path().join(".story_kit").join("work").join("2_current");
|
||||||
|
fs::create_dir_all(¤t_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
current_dir.join("1_test.md"),
|
||||||
|
"## No front matter at all\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_validate_stories(&ctx).unwrap();
|
||||||
|
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||||
|
assert!(!parsed.is_empty());
|
||||||
|
assert_eq!(parsed[0]["valid"], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tool_record_tests and tool_ensure_acceptance edge cases ──
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_record_tests_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_record_tests(
|
||||||
|
&json!({"unit": [], "integration": []}),
|
||||||
|
&ctx,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_record_tests_invalid_unit_type_returns_error() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_record_tests(
|
||||||
|
&json!({
|
||||||
|
"story_id": "1_test",
|
||||||
|
"unit": "not_an_array",
|
||||||
|
"integration": []
|
||||||
|
}),
|
||||||
|
&ctx,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_ensure_acceptance_missing_story_id() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = test_ctx(tmp.path());
|
||||||
|
let result = tool_ensure_acceptance(&json!({}), &ctx);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("story_id"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user