mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
refactor(composio): extract derive_toolkit_slug helper (#3035)
This commit is contained in:
@@ -121,12 +121,21 @@ fn prefix_class(class: ComposioErrorClass, body: &str) -> String {
|
||||
format!("[composio:error:{}] {}", class.as_str(), body)
|
||||
}
|
||||
|
||||
fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
|
||||
let toolkit = tool
|
||||
.split('_')
|
||||
/// Derive the lowercase toolkit slug from a Composio tool/trigger identifier.
|
||||
///
|
||||
/// Identifiers are upper-snake-case (e.g. `GMAIL_NEW_GMAIL_MESSAGE`); the leading
|
||||
/// segment names the toolkit, so this returns e.g. `gmail`. `str::split('_').next()`
|
||||
/// always yields `Some(_)` for `&str` input (empty input yields `Some("")`), so
|
||||
/// `unwrap_or_default()` is a safe, equivalent terminator.
|
||||
fn derive_toolkit_slug(tool: &str) -> String {
|
||||
tool.split('_')
|
||||
.next()
|
||||
.unwrap_or("integration")
|
||||
.to_ascii_lowercase();
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
|
||||
let toolkit = derive_toolkit_slug(tool);
|
||||
format!(
|
||||
"`{tool}` was rejected because the connected {toolkit} account is missing required \
|
||||
permissions ({detail}). Reconnect the integration in Settings → Connections → \
|
||||
@@ -140,11 +149,7 @@ fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
|
||||
/// [`format_insufficient_scope_message`] does (e.g. `GMAIL_NEW_GMAIL_MESSAGE`
|
||||
/// → `gmail`), so the copy is branded and points the user at reconnecting.
|
||||
fn format_trigger_permission_message(tool: &str) -> String {
|
||||
let toolkit = tool
|
||||
.split('_')
|
||||
.next()
|
||||
.unwrap_or("integration")
|
||||
.to_ascii_lowercase();
|
||||
let toolkit = derive_toolkit_slug(tool);
|
||||
format!(
|
||||
"Couldn't enable this trigger: the connected {toolkit} account doesn't have \
|
||||
permission to manage triggers. Reconnect {toolkit} in Settings → Connections → \
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
use super::{
|
||||
classify_composio_error, format_provider_error, remap_transport_error, ComposioErrorClass,
|
||||
classify_composio_error, derive_toolkit_slug, format_provider_error, remap_transport_error,
|
||||
ComposioErrorClass,
|
||||
};
|
||||
|
||||
// ── derive_toolkit_slug (issue #2913 nitpick — shared slug extraction) ──
|
||||
|
||||
#[test]
|
||||
fn derive_toolkit_slug_extracts_leading_segment_lowercased() {
|
||||
assert_eq!(derive_toolkit_slug("GMAIL_NEW_GMAIL_MESSAGE"), "gmail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_toolkit_slug_single_segment_is_lowercased() {
|
||||
assert_eq!(derive_toolkit_slug("SLACK"), "slack");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_toolkit_slug_empty_input_returns_empty_not_fallback() {
|
||||
// Behavior-parity guard: `"".split('_').next()` yields `Some("")`, so the
|
||||
// `unwrap_or("integration")` fallback does NOT apply — preserve that exactly.
|
||||
assert_eq!(derive_toolkit_slug(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_gmail_insufficient_scope() {
|
||||
let msg = "HTTP 403: Request had insufficient authentication scopes.";
|
||||
|
||||
@@ -155,8 +155,7 @@ pub fn capability_matrix() -> Vec<ComposioCapability> {
|
||||
/// Lookup key is the lowercased prefix returned by
|
||||
/// [`toolkit_from_slug`] applied to the action slug — e.g.
|
||||
/// `GOOGLECALENDAR_CREATE_EVENT` → `"googlecalendar"`. Multi-segment
|
||||
/// prefixes like `MICROSOFT_TEAMS_*` are matched via their first
|
||||
/// segment with an extra arm.
|
||||
/// prefixes like `MICROSOFT_TEAMS_*` return their known toolkit slug.
|
||||
/// Synchronous visibility check for a Composio action slug given a
|
||||
/// pre-loaded user scope preference.
|
||||
///
|
||||
@@ -200,7 +199,8 @@ pub fn catalog_for_toolkit(toolkit: &str) -> Option<&'static [CuratedTool]> {
|
||||
"googledocs" | "google_docs" => Some(catalogs::GOOGLEDOCS_CURATED),
|
||||
"googlesheets" | "google_sheets" => Some(catalogs::GOOGLESHEETS_CURATED),
|
||||
"outlook" => Some(catalogs::OUTLOOK_CURATED),
|
||||
// MICROSOFT_TEAMS_* slugs extract to "microsoft" via toolkit_from_slug.
|
||||
// Keep the legacy "microsoft" alias while toolkit_from_slug now
|
||||
// returns the precise "microsoft_teams" slug for Teams actions.
|
||||
"microsoft" | "microsoft_teams" => Some(catalogs::MICROSOFT_TEAMS_CURATED),
|
||||
"jira" => Some(catalogs::JIRA_CURATED),
|
||||
"trello" => Some(catalogs::TRELLO_CURATED),
|
||||
|
||||
@@ -125,7 +125,13 @@ impl ToolMemoryRule {
|
||||
|
||||
/// Generate a fresh, opaque rule id.
|
||||
pub fn generate_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
let mut id = String::with_capacity(33);
|
||||
id.push('r');
|
||||
for byte in uuid::Uuid::new_v4().as_bytes() {
|
||||
id.push((b'a' + (byte >> 4)) as char);
|
||||
id.push((b'a' + (byte & 0x0f)) as char);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// Storage key used inside the tool namespace.
|
||||
@@ -207,6 +213,22 @@ mod tests {
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_rule_ids_are_safe_memory_document_keys() {
|
||||
for _ in 0..128 {
|
||||
let id = ToolMemoryRule::generate_id();
|
||||
assert!(
|
||||
id.chars().all(|ch| ch.is_ascii_lowercase()),
|
||||
"generated id should avoid PII-shaped digits and separators: {id}"
|
||||
);
|
||||
let key = ToolMemoryRule::storage_key(&id);
|
||||
assert!(
|
||||
!crate::openhuman::memory_store::safety::pii::has_likely_pii(&key),
|
||||
"generated storage key should not trip PII boundary: {key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_storage_key_uses_rule_prefix() {
|
||||
assert_eq!(ToolMemoryRule::storage_key("abc"), "rule/abc");
|
||||
|
||||
Reference in New Issue
Block a user