mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(composio): correct direct-mode v3 execute contract + classify 404/410 as action-not-found (#3219) (#3298)
This commit is contained in:
@@ -14,6 +14,13 @@ pub enum ComposioErrorClass {
|
||||
/// matches it). Distinct so the user gets reconnect guidance. See #2913.
|
||||
TriggerPermission,
|
||||
RateLimited,
|
||||
/// Composio answered the execute call with an HTTP 404/410 — the action
|
||||
/// endpoint or slug is unknown (or the endpoint is deprecated/Gone), NOT a
|
||||
/// broken connection. Distinct so the user is told their integration is
|
||||
/// still connected and is **not** advised to re-authenticate a healthy
|
||||
/// account. See #3219 (direct-mode sent the lowercase toolkit slug to the
|
||||
/// wrong execute path → 404/410 → misclassified as [`Self::ComposioPlatform`]).
|
||||
ActionNotFound,
|
||||
UpstreamProvider,
|
||||
ComposioPlatform,
|
||||
Gateway,
|
||||
@@ -27,6 +34,7 @@ impl ComposioErrorClass {
|
||||
Self::InsufficientScope => "insufficient_scope",
|
||||
Self::TriggerPermission => "trigger_permission",
|
||||
Self::RateLimited => "rate_limited",
|
||||
Self::ActionNotFound => "action_not_found",
|
||||
Self::UpstreamProvider => "upstream_provider",
|
||||
Self::ComposioPlatform => "composio_platform",
|
||||
Self::Gateway => "gateway",
|
||||
@@ -37,7 +45,15 @@ impl ComposioErrorClass {
|
||||
|
||||
pub fn classify_composio_error(tool: &str, message: &str) -> ComposioErrorClass {
|
||||
let lower = message.to_ascii_lowercase();
|
||||
let class = if is_validation_shape(&lower) {
|
||||
// EARLY status arm (#3219): a Composio HTTP 404/410 means the action
|
||||
// endpoint/slug is unknown or deprecated — NOT a broken connection. It must
|
||||
// win over the substring arms below, because the 404 body frequently carries
|
||||
// the phrase "connection error, try to authenticate" that
|
||||
// `is_composio_platform_shape` would otherwise match, telling the user to
|
||||
// re-authenticate a perfectly healthy connection.
|
||||
let class = if is_action_not_found_shape(&lower) {
|
||||
ComposioErrorClass::ActionNotFound
|
||||
} else if is_validation_shape(&lower) {
|
||||
ComposioErrorClass::Validation
|
||||
} else if is_insufficient_scope_shape(&lower) {
|
||||
ComposioErrorClass::InsufficientScope
|
||||
@@ -74,6 +90,7 @@ pub fn format_provider_error(tool: &str, raw: &str) -> String {
|
||||
ComposioErrorClass::InsufficientScope => format_insufficient_scope_message(tool, detail),
|
||||
ComposioErrorClass::TriggerPermission => format_trigger_permission_message(tool),
|
||||
ComposioErrorClass::RateLimited => format_rate_limited_message(tool, detail),
|
||||
ComposioErrorClass::ActionNotFound => format_action_not_found_message(tool),
|
||||
ComposioErrorClass::UpstreamProvider => {
|
||||
format!("`{tool}` failed at the connected provider: {detail}")
|
||||
}
|
||||
@@ -101,6 +118,7 @@ pub fn remap_transport_error(tool: &str, raw: &str) -> String {
|
||||
ComposioErrorClass::InsufficientScope => format_insufficient_scope_message(tool, &detail),
|
||||
ComposioErrorClass::TriggerPermission => format_trigger_permission_message(tool),
|
||||
ComposioErrorClass::RateLimited => format_rate_limited_message(tool, &detail),
|
||||
ComposioErrorClass::ActionNotFound => format_action_not_found_message(tool),
|
||||
ComposioErrorClass::Gateway => format!(
|
||||
"Temporary gateway error while calling `{tool}`: {}",
|
||||
summarize_gateway(raw)
|
||||
@@ -157,6 +175,23 @@ fn format_trigger_permission_message(tool: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the action-not-found guidance (issue #3219).
|
||||
///
|
||||
/// Deliberately does **not** echo the raw provider `detail`: a Composio 404
|
||||
/// body often literally reads "connection error, try to authenticate", and
|
||||
/// surfacing that verbatim would re-introduce the exact misleading re-auth
|
||||
/// nudge this class exists to suppress. The raw text is still captured in the
|
||||
/// `tracing::debug!` line in [`classify_composio_error`] for diagnosis.
|
||||
fn format_action_not_found_message(tool: &str) -> String {
|
||||
let toolkit = derive_toolkit_slug(tool);
|
||||
format!(
|
||||
"`{tool}` couldn't run: Composio reported this action as not found. Your {toolkit} \
|
||||
integration is still connected and working — this is not a sign-in problem. The action \
|
||||
name is likely out of date or Composio's API changed; try again with the current action \
|
||||
name, or report this if it keeps happening."
|
||||
)
|
||||
}
|
||||
|
||||
fn format_rate_limited_message(tool: &str, detail: &str) -> String {
|
||||
format!(
|
||||
"`{tool}` hit an upstream rate limit ({detail}). Wait a minute and retry, or reduce \
|
||||
@@ -210,6 +245,17 @@ fn is_rate_limited_shape(lower: &str) -> bool {
|
||||
|| lower.contains("429")
|
||||
}
|
||||
|
||||
/// True when the message carries a Composio HTTP 404 or 410 status (#3219).
|
||||
///
|
||||
/// Both `response_error` shapes (`HTTP 404` and `HTTP 404: <body>`) and the
|
||||
/// wrapped v3/v2 fallback string keep the literal `HTTP <code>` token, so a
|
||||
/// substring scan is reliable. Scoped to 404 (action/endpoint unknown) and 410
|
||||
/// (endpoint Gone/deprecated) on purpose — other 4xx/5xx keep their existing
|
||||
/// validation / scope / rate-limit / gateway classifications.
|
||||
fn is_action_not_found_shape(lower: &str) -> bool {
|
||||
lower.contains("http 404") || lower.contains("http 410")
|
||||
}
|
||||
|
||||
fn is_composio_platform_shape(lower: &str) -> bool {
|
||||
lower.contains("connection error, try to authenticate")
|
||||
|| lower.contains("not enabled")
|
||||
@@ -232,6 +278,7 @@ fn is_embedded_provider_failure(lower: &str) -> bool {
|
||||
|| is_insufficient_scope_shape(lower)
|
||||
|| is_trigger_permission_shape(lower)
|
||||
|| is_rate_limited_shape(lower)
|
||||
|| is_action_not_found_shape(lower)
|
||||
|| is_composio_platform_shape(lower)
|
||||
|| lower.contains("composio")
|
||||
|| lower.contains("google")
|
||||
|
||||
@@ -80,6 +80,81 @@ fn true_gateway_stays_gateway_class() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── HTTP 404/410 action-not-found vs auth (issue #3219) ────────────────
|
||||
|
||||
#[test]
|
||||
fn classifies_http_404_with_auth_phrase_as_action_not_found_not_platform() {
|
||||
// The exact #3219 shape: a 404 whose body carries the misleading
|
||||
// "connection error, try to authenticate" phrase. Status must win.
|
||||
let msg = "HTTP 404: connection error, try to authenticate";
|
||||
assert_eq!(
|
||||
classify_composio_error("GMAIL_SEND_EMAIL", msg),
|
||||
ComposioErrorClass::ActionNotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_http_410_gone_as_action_not_found() {
|
||||
let msg = "HTTP 410: This endpoint is deprecated";
|
||||
assert_eq!(
|
||||
classify_composio_error("GOOGLEDOCS_UPDATE_DOCUMENT", msg),
|
||||
ComposioErrorClass::ActionNotFound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_not_found_message_does_not_recommend_reauth() {
|
||||
let mapped = format_provider_error(
|
||||
"GMAIL_SEND_EMAIL",
|
||||
"HTTP 404: connection error, try to authenticate",
|
||||
);
|
||||
let lower = mapped.to_lowercase();
|
||||
assert!(mapped.contains("[composio:error:action_not_found]"));
|
||||
assert!(lower.contains("still connected"));
|
||||
// Must NOT echo the misleading provider phrase or nudge re-auth/reconnect.
|
||||
assert!(
|
||||
!lower.contains("authenticate"),
|
||||
"must not surface the re-auth phrase: {mapped}"
|
||||
);
|
||||
assert!(
|
||||
!lower.contains("reconnect"),
|
||||
"must not tell the user to reconnect a healthy connection: {mapped}"
|
||||
);
|
||||
assert!(
|
||||
!mapped.contains("Settings → Connections"),
|
||||
"must not show the re-auth CTA: {mapped}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genuine_auth_phrase_without_http_status_stays_platform() {
|
||||
// Same phrase, but with NO 4xx status — a real platform/connection issue
|
||||
// must still classify as ComposioPlatform.
|
||||
let msg = "connection error, try to authenticate";
|
||||
assert_eq!(
|
||||
classify_composio_error("GMAIL_SEND_EMAIL", msg),
|
||||
ComposioErrorClass::ComposioPlatform
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_v3_v2_404_fallback_classifies_as_action_not_found() {
|
||||
// The real transport string after both v3 and v2 fail — the wrapped form
|
||||
// produced by `execute_action`, fed through `remap_transport_error`.
|
||||
let raw = "Composio execute failed on v3 (Composio v3 action execution failed: \
|
||||
HTTP 404: connection error, try to authenticate) and v2 fallback \
|
||||
(Composio v2 action execution failed: HTTP 410: Gone)";
|
||||
let mapped = remap_transport_error("GMAIL_SEND_EMAIL", raw);
|
||||
assert!(
|
||||
mapped.contains("[composio:error:action_not_found]"),
|
||||
"wrapped 404/410 must map to action_not_found, got: {mapped}"
|
||||
);
|
||||
assert!(
|
||||
!mapped.to_lowercase().contains("authenticate"),
|
||||
"wrapped path must not surface the re-auth phrase: {mapped}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Trigger-permission denial (issue #2913) ───────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -362,10 +362,22 @@ impl ComposioTool {
|
||||
entity_id: Option<&str>,
|
||||
connected_account_ref: Option<&str>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let tool_slug = normalize_tool_slug(action_name);
|
||||
// The Composio v3 action-execute contract keys off the UPPERCASE_SNAKE
|
||||
// *action* slug (e.g. `GMAIL_SEND_EMAIL`) at `/tools/execute/{slug}`.
|
||||
// The previous code lowercased + dashed it into the *toolkit* slug
|
||||
// (`gmail-send-email`) and posted to the wrong `/tools/{slug}/execute`
|
||||
// path, so every direct-mode execute 404'd (issue #3219). Pass the
|
||||
// action slug through verbatim (trimmed only); the v2 fallback already
|
||||
// used the same untransformed name.
|
||||
let action_slug = action_name.trim();
|
||||
|
||||
match self
|
||||
.execute_action_v3(&tool_slug, params.clone(), entity_id, connected_account_ref)
|
||||
.execute_action_v3(
|
||||
action_slug,
|
||||
params.clone(),
|
||||
entity_id,
|
||||
connected_account_ref,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
@@ -379,12 +391,15 @@ impl ComposioTool {
|
||||
}
|
||||
|
||||
fn build_execute_action_v3_request(
|
||||
tool_slug: &str,
|
||||
action_slug: &str,
|
||||
params: serde_json::Value,
|
||||
entity_id: Option<&str>,
|
||||
connected_account_ref: Option<&str>,
|
||||
) -> (String, serde_json::Value) {
|
||||
let url = format!("{COMPOSIO_API_BASE_V3}/tools/{tool_slug}/execute");
|
||||
// POST /api/v3/tools/execute/{ACTION_SLUG} — the action slug stays
|
||||
// UPPERCASE_SNAKE (see `execute_action`). Path is `/tools/execute/{slug}`,
|
||||
// NOT `/tools/{slug}/execute` (issue #3219).
|
||||
let url = format!("{COMPOSIO_API_BASE_V3}/tools/execute/{action_slug}");
|
||||
let account_ref = connected_account_ref.and_then(|candidate| {
|
||||
let trimmed_candidate = candidate.trim();
|
||||
(!trimmed_candidate.is_empty()).then_some(trimmed_candidate)
|
||||
@@ -406,18 +421,18 @@ impl ComposioTool {
|
||||
|
||||
async fn execute_action_v3(
|
||||
&self,
|
||||
tool_slug: &str,
|
||||
action_slug: &str,
|
||||
params: serde_json::Value,
|
||||
entity_id: Option<&str>,
|
||||
connected_account_ref: Option<&str>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let (_default_url, body) = Self::build_execute_action_v3_request(
|
||||
tool_slug,
|
||||
action_slug,
|
||||
params,
|
||||
entity_id,
|
||||
connected_account_ref,
|
||||
);
|
||||
let url = format!("{}/tools/{tool_slug}/execute", self.base_v3);
|
||||
let url = format!("{}/tools/execute/{action_slug}", self.base_v3);
|
||||
|
||||
self.ensure_request_url(&url)?;
|
||||
|
||||
@@ -885,10 +900,6 @@ fn normalize_entity_id(entity_id: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_tool_slug(action_name: &str) -> String {
|
||||
action_name.trim().replace('_', "-").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn map_v3_tools_to_actions(items: Vec<ComposioV3Tool>) -> Vec<ComposioAction> {
|
||||
items
|
||||
.into_iter()
|
||||
|
||||
@@ -184,18 +184,6 @@ fn normalize_entity_id_falls_back_to_default_when_blank() {
|
||||
assert_eq!(normalize_entity_id("workspace-user"), "workspace-user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_tool_slug_supports_legacy_action_name() {
|
||||
assert_eq!(
|
||||
normalize_tool_slug("GMAIL_FETCH_EMAILS"),
|
||||
"gmail-fetch-emails"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_tool_slug(" github-list-repos "),
|
||||
"github-list-repos"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_redirect_url_supports_v2_and_v3_shapes() {
|
||||
let v2 = json!({"redirectUrl": "https://app.composio.dev/connect-v2"});
|
||||
@@ -313,19 +301,24 @@ fn composio_api_base_url_is_v3() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_execute_action_v3_request_uses_fixed_endpoint_and_body_account_id() {
|
||||
fn build_execute_action_v3_request_uses_execute_path_and_uppercase_action_slug() {
|
||||
// #3219: v3 action execute is POST /tools/execute/{ACTION_SLUG} with the
|
||||
// UPPERCASE_SNAKE action slug — NOT /tools/{lowercase-dashed}/execute.
|
||||
let (url, body) = ComposioTool::build_execute_action_v3_request(
|
||||
"gmail-send-email",
|
||||
json!({"to": "test@example.com"}),
|
||||
"GMAIL_SEND_EMAIL",
|
||||
json!({"recipient_email": "test@example.com"}),
|
||||
Some("workspace-user"),
|
||||
Some("account-42"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://backend.composio.dev/api/v3/tools/gmail-send-email/execute"
|
||||
"https://backend.composio.dev/api/v3/tools/execute/GMAIL_SEND_EMAIL"
|
||||
);
|
||||
assert_eq!(
|
||||
body["arguments"]["recipient_email"],
|
||||
json!("test@example.com")
|
||||
);
|
||||
assert_eq!(body["arguments"]["to"], json!("test@example.com"));
|
||||
assert_eq!(body["user_id"], json!("workspace-user"));
|
||||
assert_eq!(body["connected_account_id"], json!("account-42"));
|
||||
}
|
||||
@@ -333,7 +326,7 @@ fn build_execute_action_v3_request_uses_fixed_endpoint_and_body_account_id() {
|
||||
#[test]
|
||||
fn build_execute_action_v3_request_drops_blank_optional_fields() {
|
||||
let (url, body) = ComposioTool::build_execute_action_v3_request(
|
||||
"github-list-repos",
|
||||
"GITHUB_LIST_REPOSITORIES",
|
||||
json!({}),
|
||||
None,
|
||||
Some(" "),
|
||||
@@ -341,7 +334,7 @@ fn build_execute_action_v3_request_drops_blank_optional_fields() {
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://backend.composio.dev/api/v3/tools/github-list-repos/execute"
|
||||
"https://backend.composio.dev/api/v3/tools/execute/GITHUB_LIST_REPOSITORIES"
|
||||
);
|
||||
assert_eq!(body["arguments"], json!({}));
|
||||
assert!(body.get("connected_account_id").is_none());
|
||||
@@ -488,6 +481,61 @@ async fn list_tool_schemas_v3_sends_repeated_tags_to_v3_tools_endpoint() {
|
||||
assert_eq!(items[0].toolkit_slug.as_deref(), Some("github"));
|
||||
}
|
||||
|
||||
// ── execute_action over HTTP (correct v3 path/slug/body reach the wire) ────
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_action_v3_posts_uppercase_slug_to_execute_path() {
|
||||
use axum::{extract::Path, routing::post, Json, Router};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Capture the path slug + body the server actually received so we assert on
|
||||
// the WIRE shape, not just the pure builder. Regression guard for #3219.
|
||||
let captured: Arc<Mutex<Option<(String, serde_json::Value)>>> = Arc::new(Mutex::new(None));
|
||||
let sink = captured.clone();
|
||||
let app = Router::new().route(
|
||||
"/tools/execute/{slug}",
|
||||
post(
|
||||
move |Path(slug): Path<String>, Json(body): Json<serde_json::Value>| {
|
||||
let sink = sink.clone();
|
||||
async move {
|
||||
*sink.lock().unwrap() = Some((slug, body));
|
||||
Json(json!({ "successful": true, "data": { "id": "msg_1" } }))
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
let base = start_mock_backend(app).await;
|
||||
|
||||
let tool = ComposioTool::new_with_v3_base("ck_test_direct", None, test_security(), base);
|
||||
let result = tool
|
||||
.execute_action(
|
||||
"GMAIL_SEND_EMAIL",
|
||||
json!({ "recipient_email": "a@b.com" }),
|
||||
Some("workspace-user"),
|
||||
Some("ca_42"),
|
||||
)
|
||||
.await
|
||||
.expect("v3 execute should succeed against the mock");
|
||||
|
||||
assert_eq!(result["successful"], json!(true));
|
||||
|
||||
let (slug, body) = captured
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.expect("mock server should have observed the execute request");
|
||||
|
||||
// The action slug must reach the URL UPPERCASE_SNAKE — the toolkit-slug
|
||||
// transform (gmail-send-email) was the root cause of the 404 in #3219.
|
||||
assert_eq!(
|
||||
slug, "GMAIL_SEND_EMAIL",
|
||||
"must POST the uppercase action slug"
|
||||
);
|
||||
assert_eq!(body["arguments"]["recipient_email"], json!("a@b.com"));
|
||||
assert_eq!(body["user_id"], json!("workspace-user"));
|
||||
assert_eq!(body["connected_account_id"], json!("ca_42"));
|
||||
}
|
||||
|
||||
// ── ensure_https ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -164,7 +164,7 @@ async fn direct_composio_tool_uses_loopback_for_list_execute_connect_and_account
|
||||
let execute_result = tool
|
||||
.execute(json!({
|
||||
"action": "execute",
|
||||
"tool_slug": "gmail-fetch-emails",
|
||||
"tool_slug": "GMAIL_FETCH_EMAILS",
|
||||
"params": { "query": "newer_than:1d" },
|
||||
"connected_account_id": "acct-gmail"
|
||||
}))
|
||||
@@ -204,7 +204,7 @@ async fn direct_composio_tool_uses_loopback_for_list_execute_connect_and_account
|
||||
}));
|
||||
assert!(requests.iter().any(|request| {
|
||||
request.method == "POST"
|
||||
&& request.path == "/api/v3/tools/gmail-fetch-emails/execute"
|
||||
&& request.path == "/api/v3/tools/execute/GMAIL_FETCH_EMAILS"
|
||||
&& request.body.pointer("/user_id") == Some(&json!(" entity-override "))
|
||||
&& request.body.pointer("/connected_account_id") == Some(&json!("acct-gmail"))
|
||||
}));
|
||||
@@ -319,14 +319,14 @@ async fn composio_direct_handler(State(state): State<MockState>, request: Reques
|
||||
]
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/gmail-fetch-emails/execute") => Json(json!({
|
||||
(Method::POST, "/api/v3/tools/execute/GMAIL_FETCH_EMAILS") => Json(json!({
|
||||
"successful": true,
|
||||
"data": {
|
||||
"messages": [{ "id": "msg-direct", "subject": "hello" }]
|
||||
}
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/fallback-action/execute") => (
|
||||
(Method::POST, "/api/v3/tools/execute/FALLBACK_ACTION") => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(json!({
|
||||
"error": { "message": "temporary v3 outage" }
|
||||
@@ -338,7 +338,7 @@ async fn composio_direct_handler(State(state): State<MockState>, request: Reques
|
||||
"input": body.get("input").cloned().unwrap_or_else(|| json!({}))
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/broken-action/execute") => (
|
||||
(Method::POST, "/api/v3/tools/execute/BROKEN_ACTION") => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": {
|
||||
|
||||
@@ -282,7 +282,7 @@ async fn round25_direct_mode_ops_use_loopback_factory_for_tools_connections_and_
|
||||
}));
|
||||
assert!(requests.iter().any(|request| {
|
||||
request.method == Method::POST
|
||||
&& request.path == "/api/v3/tools/gmail-fetch-emails/execute"
|
||||
&& request.path == "/api/v3/tools/execute/GMAIL_FETCH_EMAILS"
|
||||
&& request.body["arguments"]["query"] == "label:INBOX"
|
||||
&& request.body["user_id"] == "entity-round25"
|
||||
}));
|
||||
@@ -402,7 +402,7 @@ async fn composio_direct_handler(State(state): State<MockState>, request: Reques
|
||||
}
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/gmail-fetch-emails/execute") => Json(json!({
|
||||
(Method::POST, "/api/v3/tools/execute/GMAIL_FETCH_EMAILS") => Json(json!({
|
||||
"successful": true,
|
||||
"data": {
|
||||
"messages": [
|
||||
@@ -411,7 +411,7 @@ async fn composio_direct_handler(State(state): State<MockState>, request: Reques
|
||||
}
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/gmail-send-email/execute") => Json(json!({
|
||||
(Method::POST, "/api/v3/tools/execute/GMAIL_SEND_EMAIL") => Json(json!({
|
||||
"successful": false,
|
||||
"error": "provider rejected send",
|
||||
"data": {
|
||||
|
||||
@@ -454,7 +454,7 @@ async fn round20_direct_composio_tool_covers_fallback_sanitizing_and_account_edg
|
||||
}));
|
||||
assert!(requests.iter().any(|request| {
|
||||
request.method == Method::POST
|
||||
&& request.path == "/api/v3/tools/gmail-fetch-emails/execute"
|
||||
&& request.path == "/api/v3/tools/execute/GMAIL_FETCH_EMAILS"
|
||||
&& request.body.pointer("/connected_account_id") == Some(&json!("acct-gmail"))
|
||||
}));
|
||||
}
|
||||
@@ -780,13 +780,13 @@ async fn composio_direct_handler(State(state): State<MockState>, request: Reques
|
||||
StatusCode::BAD_REQUEST,
|
||||
"v2 broken list mentions connected_account_id and user_id",
|
||||
),
|
||||
(Method::POST, "/api/v3/tools/gmail-fetch-emails/execute") => Json(json!({
|
||||
(Method::POST, "/api/v3/tools/execute/GMAIL_FETCH_EMAILS") => Json(json!({
|
||||
"successful": true,
|
||||
"data": { "messages": [{ "id": "msg-round20" }] },
|
||||
"error": null
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/broken-action/execute") => message_fail(
|
||||
(Method::POST, "/api/v3/tools/execute/BROKEN_ACTION") => message_fail(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"bad execute connected_account_id user_id entity_id",
|
||||
),
|
||||
|
||||
@@ -91,7 +91,7 @@ async fn round24_composio_direct_covers_v3_v2_fallbacks_and_account_shapes() {
|
||||
.iter()
|
||||
.find(|request| {
|
||||
request.method == Method::POST
|
||||
&& request.path == "/api/v3/tools/gmail-send-email/execute"
|
||||
&& request.path == "/api/v3/tools/execute/GMAIL_SEND_EMAIL"
|
||||
})
|
||||
.expect("v3 execute request");
|
||||
assert_eq!(v3_execute.body["connected_account_id"], "conn-secret");
|
||||
@@ -267,7 +267,7 @@ async fn composio_handler(State(state): State<MockState>, request: Request) -> R
|
||||
]
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/gmail-send-email/execute") => (
|
||||
(Method::POST, "/api/v3/tools/execute/GMAIL_SEND_EMAIL") => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": {
|
||||
|
||||
@@ -320,7 +320,7 @@ async fn round26_composio_direct_tool_covers_connect_execute_and_error_fallbacks
|
||||
let v3_execute = requests
|
||||
.iter()
|
||||
.find(|request| {
|
||||
request.method == Method::POST && request.path == "/api/v3/tools/round26-action/execute"
|
||||
request.method == Method::POST && request.path == "/api/v3/tools/execute/ROUND26_ACTION"
|
||||
})
|
||||
.expect("v3 execute request");
|
||||
assert_eq!(v3_execute.body["user_id"], "entity-round26");
|
||||
@@ -463,12 +463,12 @@ async fn composio_handler(State(state): State<MockState>, request: Request) -> R
|
||||
]
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/round26-action/execute") => Json(json!({
|
||||
(Method::POST, "/api/v3/tools/execute/ROUND26_ACTION") => Json(json!({
|
||||
"successful": true,
|
||||
"data": { "message": "v3-execute-round26" }
|
||||
}))
|
||||
.into_response(),
|
||||
(Method::POST, "/api/v3/tools/round26-v2-only/execute") => (
|
||||
(Method::POST, "/api/v3/tools/execute/ROUND26_V2_ONLY") => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "message": "v3 execute unavailable" })),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user