mod common;
use common::*;
use grokbot_api::*;
use serde_json::json;
use wiremock::matchers::{body_json, body_partial_json, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn respond(server: &MockServer, method: &str, body: serde_json::Value) {
Mock::given(path(format!("/api/{method}")))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(server)
.await;
}
#[tokio::test]
async fn list_agents_response_shapes() {
let server = MockServer::start().await;
respond(
&server,
"listAgents",
json!({"agent": {"id": "solo", "notifyOnAgentUpdates": false}}),
)
.await;
let c = client(&server);
let list = c.list_agents().await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].notify_on_updates, Some(false));
assert_eq!(list[0].display_name(), "solo", "falls back to the id");
let server = MockServer::start().await;
respond(&server, "listAgents", json!({"unexpected": true})).await;
assert!(client(&server).list_agents().await.unwrap().is_empty());
}
#[test]
fn agent_parsing_details() {
let a = Agent::from_value(&json!({
"id": "g", "name": "", "title": "", "isGroup": false, "memberIds": ["x", 5],
"avatarShape": "hex", "avatarColor": "red", "notifyOnUpdatesEnabled": true,
"hiddenFromSidebar": true
}))
.unwrap();
assert!(!a.is_group, "explicit isGroup wins over members");
assert_eq!(a.member_ids, vec!["x"]);
assert_eq!(
(a.avatar_shape.as_str(), a.avatar_color.as_str()),
("hex", "red")
);
assert_eq!(
(a.notify_on_updates, a.hidden_from_sidebar),
(Some(true), Some(true))
);
assert!(Agent::from_value(&json!({"id": ""})).is_none());
}
#[tokio::test]
async fn create_agent_returns_the_created_agent() {
let server = MockServer::start().await;
Mock::given(path("/api/createAgent"))
.and(body_json(
json!({"name": "Bot", "description": "d", "title": "t",
"avatarShape": "", "avatarColor": "blue", "origin": "user"}),
))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({"agent": {"id": "new1", "name": "Bot"}})),
)
.mount(&server)
.await;
let c = client(&server);
let a = c
.create_agent(&CreateAgent {
name: "Bot".into(),
description: "d".into(),
title: "t".into(),
avatar_shape: " ".into(),
avatar_color: " blue ".into(),
})
.await
.unwrap()
.unwrap();
assert_eq!(a.id, "new1");
}
#[tokio::test]
async fn create_agent_without_echo_is_none() {
let server = MockServer::start().await;
respond(&server, "createAgent", json!({"ok": true})).await;
let r = client(&server)
.create_agent(&CreateAgent {
name: "x".into(),
..Default::default()
})
.await
.unwrap();
assert!(r.is_none());
}
#[tokio::test]
async fn update_agent_optional_fields_and_validation() {
let server = MockServer::start().await;
Mock::given(path("/api/updateAgent"))
.and(body_json(
json!({"id": "a1", "profile": {"name": "N", "description": "",
"title": "T", "avatarShape": "leaf"}}),
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"ok": true})))
.expect(1)
.mount(&server)
.await;
let c = client(&server);
let p = AgentProfile {
name: "N".into(),
title: Some("T".into()),
avatar_shape: Some("leaf".into()),
..Default::default()
};
assert_eq!(c.update_agent("a1", &p).await.unwrap(), json!({"ok": true}));
for bad in [
AgentProfile {
name: " ".into(),
..Default::default()
},
AgentProfile {
name: "n".into(),
avatar_shape: Some("cube".into()),
..Default::default()
},
AgentProfile {
name: "n".into(),
avatar_color: Some("pink".into()),
..Default::default()
},
] {
let e = c.update_agent("a1", &bad).await.unwrap_err();
assert!(matches!(e, Error::Validation(_)), "{e:?}");
assert_eq!(e.delivery(), Delivery::Rejected);
}
}
#[tokio::test]
async fn avatar_validation_lists_allowed_values() {
let server = MockServer::start().await;
let e = client(&server)
.create_agent(&CreateAgent {
name: "n".into(),
avatar_color: "pink".into(),
..Default::default()
})
.await
.unwrap_err();
let msg = e.to_string();
assert!(msg.contains("Unknown avatar color \"pink\""), "{msg}");
for c in AVATAR_COLORS {
assert!(msg.contains(c), "{msg}");
}
assert!(AVATAR_SHAPES.contains(&"hex"));
}
#[tokio::test]
async fn groups_validate_members_and_return_the_group() {
let server = MockServer::start().await;
respond(
&server,
"createGroup",
json!({"id": "g1", "memberIds": ["a", "b"]}),
)
.await;
respond(
&server,
"setGroupMembers",
json!({"agent": {"id": "g1", "memberIds": ["a"]}}),
)
.await;
let c = client(&server);
let g = c
.create_group(&CreateGroup {
name: "G".into(),
member_agent_ids: vec!["a".into(), "".into(), "b".into()],
..Default::default()
})
.await
.unwrap()
.unwrap();
assert!(g.is_group);
let g = c
.set_group_members("g1", &["a".into()])
.await
.unwrap()
.unwrap();
assert_eq!(g.member_ids, vec!["a"]);
let seven: Vec<String> = (0..=MAX_GROUP_MEMBERS).map(|i| format!("m{i}")).collect();
let e = c
.create_group(&CreateGroup {
member_agent_ids: seven,
..Default::default()
})
.await
.unwrap_err();
assert!(e.to_string().contains("at most 6"), "{e}");
let e = c.create_group(&CreateGroup::default()).await.unwrap_err();
assert!(e.to_string().contains("at least one"), "{e}");
}
#[tokio::test]
async fn notify_and_hidden_flags() {
let server = MockServer::start().await;
Mock::given(path("/api/setAgentNotifyOnUpdates"))
.and(body_json(json!({"id": "a", "isEnabled": false})))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(path("/api/setAgentHiddenFromSidebar"))
.and(body_json(json!({"id": "a", "isHidden": true})))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let c = client(&server);
c.set_agent_notify_on_updates("a", false).await.unwrap();
c.set_agent_hidden_from_sidebar("a", true).await.unwrap();
}
#[tokio::test]
async fn team_agents_parsing_and_session_routing() {
let server = MockServer::start().await;
Mock::given(path("/aiserver.v1.GrokBotService/ListGrokBotTeamAgents"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"entries": [
{"agent": {"agentId": "room1", "kind": "GROK_BOT_AGENT_KIND_ROOM",
"viewerIsOwner": false, "viewerSessionId": " vs-room ",
"ownerDisplayName": " Pat "}},
{"agent": {"agentId": "shared-no-session", "viewerIsOwner": false}},
{"agent": {"name": "missing id"}},
{"notAnAgent": {}}
]})))
.mount(&server)
.await;
Mock::given(path("/api/promptAcceptanceStatus"))
.and(body_json(
json!({"agentId": "room1", "clientNonce": "n", "sessionId": "vs-room"}),
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"outcome": "not-found"})))
.expect(1)
.mount(&server)
.await;
let c = cursor_client(&server, "gw");
let team = c.list_team_agents().await.unwrap().unwrap();
assert_eq!(team.len(), 2);
assert!(team[0].is_room && !team[0].team_visible);
assert_eq!(team[0].viewer_session_id.as_deref(), Some("vs-room"));
assert_eq!(team[0].owner_display_name.as_deref(), Some("Pat"));
assert_eq!(team[1].viewer_session_id, None);
assert_eq!(
c.prompt_acceptance_status("room1", "n").await.unwrap(),
AcceptanceStatus::NotFound
);
assert!(TeamAgent::list(&json!({})).is_empty());
}
#[tokio::test]
async fn team_agents_backend_error() {
let server = MockServer::start().await;
Mock::given(path("/aiserver.v1.GrokBotService/ListGrokBotTeamAgents"))
.respond_with(ResponseTemplate::new(403).set_body_json(json!({"message": "not on a team"})))
.mount(&server)
.await;
let e = cursor_client(&server, "gw")
.list_team_agents()
.await
.unwrap_err();
assert_eq!(
e.to_string(),
"ListGrokBotTeamAgents failed: 403 not on a team"
);
}
#[tokio::test]
async fn manual_agent_session_is_sent() {
let server = MockServer::start().await;
Mock::given(path("/api/getAgentTranscriptTail"))
.and(body_partial_json(json!({"id": "x", "sessionId": "s1"})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"entries": []})))
.expect(1)
.mount(&server)
.await;
let c = client(&server);
c.set_agent_session("x", "s1");
c.transcript_tail("x", 10, None).await.unwrap();
}
#[tokio::test]
async fn workflows_shapes_and_import_results() {
let server = MockServer::start().await;
respond(
&server,
"getAgentWorkflows",
json!([
{"id": "w1", "source": "managed", "sourceRef": "ref", "description": "d"},
{"id": true}, {"id": ""}
]),
)
.await;
Mock::given(path("/api/importAgentWorkflowText"))
.and(body_partial_json(json!({"markdown": "# one"})))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({"result": {"skipped": [{}]}})),
)
.mount(&server)
.await;
Mock::given(path("/api/importAgentWorkflowText"))
.and(body_partial_json(json!({"markdown": "# two"})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"ok": true})))
.mount(&server)
.await;
let c = client(&server);
let w = c.get_agent_workflows("a").await.unwrap();
assert_eq!(w.len(), 1);
assert_eq!(
(w[0].source.as_str(), w[0].source_ref.as_deref()),
("managed", Some("ref"))
);
let r = c.import_agent_workflow_text("a", "# one").await.unwrap();
assert_eq!(r.skipped[0].reason, "rejected", "default reason");
assert!(r.imported.is_empty() && r.workflows.is_empty());
assert_eq!(
c.import_agent_workflow_text("a", "# two").await.unwrap(),
ImportResult::default()
);
}
#[tokio::test]
async fn workflow_import_length_limit() {
let server = MockServer::start().await;
let c = client(&server);
let too_long = "x".repeat(SKILL_MAX_BODY_LENGTH + 1);
let e = c
.import_agent_workflow_text("a", &too_long)
.await
.unwrap_err();
assert!(e.to_string().contains("exceeds 100000 characters"), "{e}");
assert!(server.received_requests().await.unwrap().is_empty());
}
#[tokio::test]
async fn delete_workflow_and_resolutions() {
let server = MockServer::start().await;
Mock::given(path("/api/resolveLocalToolPermission"))
.and(body_partial_json(json!({"resolution": "deny"})))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(path("/api/resolveAutoReviewApproval"))
.and(body_partial_json(json!({"resolution": "approved"})))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let c = client(&server);
c.resolve_local_tool_permission("a", "e", "r", LocalToolResolution::Deny)
.await
.unwrap();
c.resolve_auto_review_approval("a", "e", "r", AutoReviewResolution::Approved)
.await
.unwrap();
assert_eq!(LocalToolResolution::AllowOnce.as_str(), "allow-once");
assert_eq!(AutoReviewResolution::Denied.as_str(), "denied");
}
#[tokio::test]
async fn call_raw_reaches_any_method() {
let server = MockServer::start().await;
Mock::given(path("/api/someFutureMethod"))
.and(body_json(json!({"a": [1, 2]})))
.respond_with(ResponseTemplate::new(200).set_body_string("not json"))
.mount(&server)
.await;
let v = client(&server)
.call_raw("someFutureMethod", json!({"a": [1, 2]}))
.await
.unwrap();
assert_eq!(v, json!({"raw": "not json"}));
}