diff --git a/rsworkspace/crates/acp-nats/src/client/mod.rs b/rsworkspace/crates/acp-nats/src/client/mod.rs index a666357df..d7c83bed0 100644 --- a/rsworkspace/crates/acp-nats/src/client/mod.rs +++ b/rsworkspace/crates/acp-nats/src/client/mod.rs @@ -71,7 +71,6 @@ pub async fn run< let wildcard = client::wildcards::all(bridge.config.acp_prefix()); info!("Starting client proxy - subscribing to {}", wildcard); - // TODO: change `run` to return `Result` and propagate this error once there is a caller. let mut subscriber = match nats.subscribe(wildcard).await { Ok(sub) => sub, Err(e) => { @@ -221,7 +220,7 @@ async fn dispatch_client_method< .await; } ClientMethod::SessionUpdate => { - session_update::handle(&payload, ctx.client, &parsed.session_id).await; + session_update::handle(&payload, ctx.client, reply.is_some()).await; } ClientMethod::ExtSessionPromptResponse => { ext_session_prompt_response::handle( diff --git a/rsworkspace/crates/acp-nats/src/client/session_update.rs b/rsworkspace/crates/acp-nats/src/client/session_update.rs index 068911ab4..745122aa0 100644 --- a/rsworkspace/crates/acp-nats/src/client/session_update.rs +++ b/rsworkspace/crates/acp-nats/src/client/session_update.rs @@ -1,10 +1,11 @@ -use crate::session_id::AcpSessionId; use agent_client_protocol::{Client, SessionNotification}; -use tracing::{info, instrument, warn}; +use tracing::{instrument, warn}; -#[instrument(name = "acp.client.session.update", skip(payload, client), fields(session_id = %session_id))] -pub async fn handle(payload: &[u8], client: &C, session_id: &AcpSessionId) { - info!(session_id = %session_id, "Forwarding session update to client"); +#[instrument(name = "acp.client.session.update", skip(payload, client))] +pub async fn handle(payload: &[u8], client: &C, has_reply: bool) { + if has_reply { + warn!("Unexpected reply subject on notification request"); + } match serde_json::from_slice::(payload) { Ok(notification) => { if let Err(e) = client.session_notification(notification).await { @@ -21,17 +22,12 @@ pub async fn handle(payload: &[u8], client: &C, session_id: &AcpSessi mod tests { use super::*; use agent_client_protocol::{ - ContentBlock, ContentChunk, RequestPermissionRequest, RequestPermissionResponse, - SessionUpdate, + ContentBlock, ContentChunk, RequestPermissionOutcome, RequestPermissionRequest, + RequestPermissionResponse, SessionUpdate, }; use async_trait::async_trait; use std::cell::RefCell; - fn session_id(s: &str) -> AcpSessionId { - AcpSessionId::new(s).unwrap() - } - - #[derive(Debug)] struct MockClient { notifications_received: RefCell>, should_fail: bool, @@ -62,7 +58,7 @@ mod tests { async fn session_notification( &self, notification: SessionNotification, - ) -> Result<(), agent_client_protocol::Error> { + ) -> agent_client_protocol::Result<()> { if self.should_fail { return Err(agent_client_protocol::Error::new(-1, "mock failure")); } @@ -75,16 +71,15 @@ mod tests { async fn request_permission( &self, _: RequestPermissionRequest, - ) -> Result { - Err(agent_client_protocol::Error::new( - -32603, - "not implemented in test mock", + ) -> agent_client_protocol::Result { + Ok(RequestPermissionResponse::new( + RequestPermissionOutcome::Cancelled, )) } } #[tokio::test] - async fn session_update_forwards_notification_to_client() { + async fn forwards_notification_to_client() { let client = MockClient::new(); let notification = SessionNotification::new( "session-001", @@ -92,20 +87,20 @@ mod tests { ); let payload = serde_json::to_vec(¬ification).unwrap(); - handle(&payload, &client, &session_id("session-001")).await; + handle(&payload, &client, false).await; assert_eq!(client.notification_count(), 1); } #[tokio::test] - async fn session_update_invalid_payload_does_not_panic() { + async fn invalid_payload_does_not_panic() { let client = MockClient::new(); - handle(b"not json", &client, &session_id("session-001")).await; + handle(b"not json", &client, false).await; assert_eq!(client.notification_count(), 0); } #[tokio::test] - async fn session_update_client_error_does_not_panic() { + async fn client_error_does_not_panic() { let client = MockClient::failing(); let notification = SessionNotification::new( "session-001", @@ -113,19 +108,30 @@ mod tests { ); let payload = serde_json::to_vec(¬ification).unwrap(); - handle(&payload, &client, &session_id("session-001")).await; + handle(&payload, &client, false).await; + } + + #[tokio::test] + async fn has_reply_logs_warning_but_still_forwards() { + let client = MockClient::new(); + let notification = SessionNotification::new( + "session-001", + SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from("hello"))), + ); + let payload = serde_json::to_vec(¬ification).unwrap(); + + handle(&payload, &client, true).await; + + assert_eq!(client.notification_count(), 1); } #[tokio::test] - async fn mock_client_request_permission_returns_err() { + async fn mock_client_trait_coverage() { + use agent_client_protocol::{ToolCallUpdate, ToolCallUpdateFields}; + let client = MockClient::new(); - let req: RequestPermissionRequest = serde_json::from_value(serde_json::json!({ - "sessionId": "sess-1", - "toolCall": { "toolCallId": "call-1" }, - "options": [] - })) - .unwrap(); - let result = client.request_permission(req).await; - assert!(result.is_err()); + let tool_call = ToolCallUpdate::new("call-1", ToolCallUpdateFields::new()); + let req = RequestPermissionRequest::new("sess-1", tool_call, vec![]); + assert!(client.request_permission(req).await.is_ok()); } } diff --git a/rsworkspace/crates/acp-nats/src/client/terminal_output.rs b/rsworkspace/crates/acp-nats/src/client/terminal_output.rs index b844f6c11..7e61b5fa4 100644 --- a/rsworkspace/crates/acp-nats/src/client/terminal_output.rs +++ b/rsworkspace/crates/acp-nats/src/client/terminal_output.rs @@ -1,60 +1,11 @@ use crate::client::rpc_reply; use crate::jsonrpc::extract_request_id; use crate::nats::{FlushClient, PublishClient}; -use agent_client_protocol::{ - Client, ErrorCode, Request, Response, TerminalOutputRequest, TerminalOutputResponse, -}; +use agent_client_protocol::{Client, ErrorCode, Request, Response, TerminalOutputRequest}; use bytes::Bytes; use tracing::{instrument, warn}; use trogon_std::JsonSerialize; -#[derive(Debug)] -pub enum TerminalOutputError { - MalformedJson(serde_json::Error), - InvalidParams(agent_client_protocol::Error), - ClientError(agent_client_protocol::Error), -} - -impl std::fmt::Display for TerminalOutputError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::MalformedJson(e) => write!(f, "malformed JSON: {}", e), - Self::InvalidParams(e) => write!(f, "invalid params: {}", e), - Self::ClientError(e) => write!(f, "client error: {}", e), - } - } -} - -impl std::error::Error for TerminalOutputError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::MalformedJson(e) => Some(e), - Self::InvalidParams(e) => Some(e), - Self::ClientError(e) => Some(e), - } - } -} - -fn invalid_params_error(message: impl Into) -> TerminalOutputError { - TerminalOutputError::InvalidParams(agent_client_protocol::Error::new( - i32::from(ErrorCode::InvalidParams), - message.into(), - )) -} - -pub fn error_code_and_message(e: &TerminalOutputError) -> (ErrorCode, String) { - match e { - TerminalOutputError::MalformedJson(inner) => ( - ErrorCode::ParseError, - format!("Malformed terminal/output request JSON: {}", inner), - ), - TerminalOutputError::InvalidParams(inner) => (inner.code, inner.message.clone()), - TerminalOutputError::ClientError(inner) => (inner.code, inner.message.clone()), - } -} - -/// Handles terminal/output: parses request, calls client, wraps response in JSON-RPC envelope, -/// and publishes to reply subject. Reply is required (request-reply pattern). #[instrument( name = "acp.client.terminal.output", skip(payload, client, nats, serializer) @@ -79,7 +30,30 @@ pub async fn handle }; let request_id = extract_request_id(payload); - match forward_to_client(payload, client, session_id).await { + + let request = match parse_request(payload, session_id) { + Ok(req) => req, + Err((code, message)) => { + warn!( + error = %message, + session_id = %session_id, + "Failed to handle terminal/output" + ); + let (bytes, content_type) = + rpc_reply::error_response_bytes(serializer, request_id, code, &message); + rpc_reply::publish_reply( + nats, + reply_to, + bytes, + content_type, + "terminal/output error reply", + ) + .await; + return; + } + }; + + match client.terminal_output(request).await { Ok(response) => { let (response_bytes, content_type) = serializer .to_vec(&Response::Result { @@ -101,111 +75,96 @@ pub async fn handle reply_to, response_bytes, content_type, - "terminal_output reply", + "terminal/output reply", ) .await; } Err(e) => { - let (code, message) = error_code_and_message(&e); warn!( error = %e, session_id = %session_id, "Failed to handle terminal/output" ); let (bytes, content_type) = - rpc_reply::error_response_bytes(serializer, request_id, code, &message); + rpc_reply::error_response_bytes(serializer, request_id, e.code, &e.message); rpc_reply::publish_reply( nats, reply_to, bytes, content_type, - "terminal_output error reply", + "terminal/output error reply", ) .await; } } } -async fn forward_to_client( +fn parse_request( payload: &[u8], - client: &C, expected_session_id: &str, -) -> Result { - let payload_value: serde_json::Value = - serde_json::from_slice(payload).map_err(TerminalOutputError::MalformedJson)?; - let envelope: Request = serde_json::from_value(payload_value) - .map_err(|e| invalid_params_error(format!("Invalid terminal/output request: {}", e)))?; - let request = envelope - .params - .ok_or_else(|| invalid_params_error("params is null or missing"))?; +) -> Result { + let value: serde_json::Value = serde_json::from_slice(payload) + .map_err(|e| (ErrorCode::ParseError, format!("Malformed JSON: {}", e)))?; + + let envelope: Request = serde_json::from_value(value) + .map_err(|e| (ErrorCode::InvalidParams, format!("Invalid request: {}", e)))?; + + let request = envelope.params.ok_or_else(|| { + ( + ErrorCode::InvalidParams, + "params is null or missing".to_string(), + ) + })?; + let params_session_id = request.session_id.to_string(); if params_session_id != expected_session_id { - return Err(invalid_params_error(format!( - "params.sessionId ({}) does not match subject session id ({})", - params_session_id, expected_session_id - ))); + return Err(( + ErrorCode::InvalidParams, + format!( + "params.sessionId ({}) does not match subject session id ({})", + params_session_id, expected_session_id + ), + )); } - client - .terminal_output(request) - .await - .map_err(TerminalOutputError::ClientError) + + Ok(request) } #[cfg(test)] mod tests { use super::*; use agent_client_protocol::{ - ContentBlock, ContentChunk, Request, RequestId, RequestPermissionRequest, - RequestPermissionResponse, SessionNotification, SessionUpdate, TerminalOutputRequest, - TerminalOutputResponse, + RequestId, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, + SessionNotification, TerminalOutputResponse, }; use async_trait::async_trait; - use std::error::Error; - use trogon_nats::{AdvancedMockNatsClient, MockNatsClient}; + use std::sync::Arc; + use trogon_nats::MockNatsClient; use trogon_std::{FailNextSerialize, StdJsonSerialize}; - struct MockClient; - - impl MockClient { - fn new() -> Self { - Self - } + struct MockClient { + terminal_output_result: agent_client_protocol::Result, } - #[async_trait(?Send)] - impl Client for MockClient { - async fn session_notification( - &self, - _: SessionNotification, - ) -> agent_client_protocol::Result<()> { - Ok(()) - } - - async fn request_permission( - &self, - _: RequestPermissionRequest, - ) -> agent_client_protocol::Result { - Err(agent_client_protocol::Error::new( - -32603, - "not implemented in test mock", - )) + impl MockClient { + fn success() -> Self { + Self { + terminal_output_result: Ok(TerminalOutputResponse::new("output", false)), + } } - async fn terminal_output( - &self, - _: TerminalOutputRequest, - ) -> agent_client_protocol::Result { - Ok(TerminalOutputResponse::new( - "output data".to_string(), - false, - )) + fn failing() -> Self { + Self { + terminal_output_result: Err(agent_client_protocol::Error::new( + -32603, + "mock failure", + )), + } } } - struct FailingClient; - #[async_trait(?Send)] - impl Client for FailingClient { + impl Client for MockClient { async fn session_notification( &self, _: SessionNotification, @@ -217,9 +176,8 @@ mod tests { &self, _: RequestPermissionRequest, ) -> agent_client_protocol::Result { - Err(agent_client_protocol::Error::new( - -32603, - "not implemented in test mock", + Ok(RequestPermissionResponse::new( + RequestPermissionOutcome::Cancelled, )) } @@ -227,24 +185,25 @@ mod tests { &self, _: TerminalOutputRequest, ) -> agent_client_protocol::Result { - Err(agent_client_protocol::Error::new( - -32603, - "mock terminal_output failure", - )) + self.terminal_output_result.clone() } } - #[tokio::test] - async fn handle_success_publishes_response_to_reply_subject() { - let nats = MockNatsClient::new(); - let client = MockClient::new(); + fn envelope_payload() -> Vec { let request = TerminalOutputRequest::new("sess-1", "term-001"); let envelope = Request { id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), + method: Arc::from("terminal/output"), params: Some(request), }; - let payload = serde_json::to_vec(&envelope).unwrap(); + serde_json::to_vec(&envelope).unwrap() + } + + #[tokio::test] + async fn success_publishes_response_to_reply_subject() { + let nats = MockNatsClient::new(); + let client = MockClient::success(); + let payload = envelope_payload(); handle( &payload, @@ -260,16 +219,10 @@ mod tests { } #[tokio::test] - async fn handle_no_reply_does_not_publish() { + async fn no_reply_does_not_publish() { let nats = MockNatsClient::new(); - let client = MockClient::new(); - let request = TerminalOutputRequest::new("sess-1", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); + let client = MockClient::success(); + let payload = envelope_payload(); handle(&payload, &client, None, &nats, "sess-1", &StdJsonSerialize).await; @@ -277,9 +230,9 @@ mod tests { } #[tokio::test] - async fn handle_invalid_payload_publishes_error_reply() { + async fn malformed_json_publishes_parse_error() { let nats = MockNatsClient::new(); - let client = MockClient::new(); + let client = MockClient::success(); handle( b"not json", @@ -297,14 +250,13 @@ mod tests { assert_eq!( response.get("error").and_then(|e| e.get("code")), Some(&serde_json::Value::from(-32700)), - "malformed JSON should return ParseError (-32700)" ); } #[tokio::test] - async fn handle_invalid_params_publishes_invalid_params_error() { + async fn invalid_params_publishes_error() { let nats = MockNatsClient::new(); - let client = MockClient::new(); + let client = MockClient::success(); let payload = br#"{"id":1,"method":"terminal/output","params":{}}"#; handle( @@ -323,24 +275,17 @@ mod tests { assert_eq!( response.get("error").and_then(|e| e.get("code")), Some(&serde_json::Value::from(-32602)), - "valid JSON with invalid params should return InvalidParams (-32602)" ); } #[tokio::test] - async fn handle_client_error_publishes_error_reply() { + async fn null_params_publishes_error() { let nats = MockNatsClient::new(); - let client = FailingClient; - let request = TerminalOutputRequest::new("sess-1", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); + let client = MockClient::success(); + let payload = br#"{"id":1,"method":"terminal/output","params":null}"#; handle( - &payload, + payload.as_slice(), &client, Some("_INBOX.err"), &nats, @@ -350,97 +295,71 @@ mod tests { .await; assert_eq!(nats.published_messages(), vec!["_INBOX.err"]); + let payloads = nats.published_payloads(); + let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap(); + assert_eq!( + response.get("error").and_then(|e| e.get("code")), + Some(&serde_json::Value::from(-32602)), + ); + let message = response["error"]["message"].as_str().unwrap(); + assert!(message.contains("params is null")); } #[tokio::test] - async fn handle_session_id_mismatch_publishes_error_reply() { + async fn session_id_mismatch_publishes_error() { let nats = MockNatsClient::new(); - let client = MockClient::new(); - let request = TerminalOutputRequest::new("sess-b", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); + let client = MockClient::success(); + let payload = envelope_payload(); handle( &payload, &client, Some("_INBOX.err"), &nats, - "sess-a", + "different-session", &StdJsonSerialize, ) .await; assert_eq!(nats.published_messages(), vec!["_INBOX.err"]); + let payloads = nats.published_payloads(); + let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap(); + assert_eq!( + response.get("error").and_then(|e| e.get("code")), + Some(&serde_json::Value::from(-32602)), + ); + let message = response["error"]["message"].as_str().unwrap(); + assert!(message.contains("does not match")); } #[tokio::test] - async fn handle_success_serialization_fallback_sends_error_reply() { + async fn client_error_publishes_error_reply() { let nats = MockNatsClient::new(); - let client = MockClient::new(); - let serializer = FailNextSerialize::new(1); - let request = TerminalOutputRequest::new("sess-1", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); - - handle( - &payload, - &client, - Some("_INBOX.reply"), - &nats, - "sess-1", - &serializer, - ) - .await; - - assert_eq!(nats.published_messages(), vec!["_INBOX.reply"]); - } - - #[tokio::test] - async fn handle_success_publish_failure_exercises_error_path() { - let nats = AdvancedMockNatsClient::new(); - nats.fail_next_publish(); - let client = MockClient::new(); - let request = TerminalOutputRequest::new("sess-1", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); + let client = MockClient::failing(); + let payload = envelope_payload(); handle( &payload, &client, - Some("_INBOX.reply"), + Some("_INBOX.err"), &nats, "sess-1", &StdJsonSerialize, ) .await; - assert!(nats.published_messages().is_empty()); + assert_eq!(nats.published_messages(), vec!["_INBOX.err"]); + let payloads = nats.published_payloads(); + let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap(); + assert!(response.get("error").is_some()); } #[tokio::test] - async fn handle_success_flush_failure_exercises_warn_path() { - let nats = AdvancedMockNatsClient::new(); - nats.fail_next_flush(); - let client = MockClient::new(); - let request = TerminalOutputRequest::new("sess-1", "term-001"); - let envelope = Request { - id: RequestId::Number(1), - method: std::sync::Arc::from("terminal/output"), - params: Some(request), - }; - let payload = serde_json::to_vec(&envelope).unwrap(); + async fn serialization_failure_sends_fallback_error() { + let nats = MockNatsClient::new(); + let client = MockClient::success(); + let serializer = FailNextSerialize::new(1); + let payload = envelope_payload(); handle( &payload, @@ -448,127 +367,28 @@ mod tests { Some("_INBOX.reply"), &nats, "sess-1", - &StdJsonSerialize, + &serializer, ) .await; assert_eq!(nats.published_messages(), vec!["_INBOX.reply"]); } - #[test] - fn error_code_and_message_malformed_json_returns_parse_error() { - let err = serde_json::from_slice::(b"not json").unwrap_err(); - let to_err = TerminalOutputError::MalformedJson(err); - let (code, message) = error_code_and_message(&to_err); - assert_eq!(code, ErrorCode::ParseError); - assert!(message.contains("Malformed terminal/output request JSON")); - } - - #[test] - fn error_code_and_message_invalid_params_preserves_code_and_message() { - let inner = - agent_client_protocol::Error::new(ErrorCode::InvalidParams.into(), "params is null"); - let to_err = TerminalOutputError::InvalidParams(inner); - let (code, message) = error_code_and_message(&to_err); - assert_eq!(code, ErrorCode::InvalidParams); - assert_eq!(message, "params is null"); - } - - #[test] - fn error_code_and_message_client_error_preserves_client_code() { - let client_err = - agent_client_protocol::Error::new(ErrorCode::InvalidParams.into(), "denied"); - let to_err = TerminalOutputError::ClientError(client_err); - let (code, message) = error_code_and_message(&to_err); - assert_eq!(code, ErrorCode::InvalidParams); - assert_eq!(message, "denied"); - } - - #[test] - fn terminal_output_error_display() { - let malformed = TerminalOutputError::MalformedJson( - serde_json::from_slice::(b"not json").unwrap_err(), - ); - assert!(malformed.to_string().contains("malformed JSON")); - - let invalid_params = TerminalOutputError::InvalidParams(agent_client_protocol::Error::new( - ErrorCode::InvalidParams.into(), - "bad params", - )); - assert!(invalid_params.to_string().contains("invalid params")); - - let client_err = TerminalOutputError::ClientError(agent_client_protocol::Error::new( - ErrorCode::InvalidParams.into(), - "client fail", - )); - assert!(client_err.to_string().contains("client error")); - } - - #[test] - fn terminal_output_error_source() { - let malformed = TerminalOutputError::MalformedJson( - serde_json::from_slice::(b"not json").unwrap_err(), - ); - assert!(malformed.source().is_some()); - - let invalid_params = TerminalOutputError::InvalidParams(agent_client_protocol::Error::new( - ErrorCode::InvalidParams.into(), - "bad params", - )); - assert!(invalid_params.source().is_some()); - - let client_err = TerminalOutputError::ClientError(agent_client_protocol::Error::new( - ErrorCode::InvalidParams.into(), - "client fail", - )); - assert!(client_err.source().is_some()); - } - #[tokio::test] - async fn mock_client_session_notification_returns_ok() { - let client = MockClient::new(); - let notification = SessionNotification::new( - "sess-1", - SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from("hi"))), - ); - let result = client.session_notification(notification).await; - assert!(result.is_ok()); - } + async fn mock_client_trait_methods_exercise_coverage() { + use agent_client_protocol::{ + ContentBlock, ContentChunk, SessionUpdate, ToolCallUpdate, ToolCallUpdateFields, + }; - #[tokio::test] - async fn failing_client_session_notification_returns_ok() { - let client = FailingClient; + let client = MockClient::success(); let notification = SessionNotification::new( "sess-1", SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from("hi"))), ); - let result = client.session_notification(notification).await; - assert!(result.is_ok()); - } + assert!(client.session_notification(notification).await.is_ok()); - #[tokio::test] - async fn mock_client_request_permission_returns_err() { - let client = MockClient::new(); - let req: RequestPermissionRequest = serde_json::from_value(serde_json::json!({ - "sessionId": "sess-1", - "toolCall": { "toolCallId": "call-1" }, - "options": [] - })) - .unwrap(); - let result = client.request_permission(req).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn failing_client_request_permission_returns_err() { - let client = FailingClient; - let req: RequestPermissionRequest = serde_json::from_value(serde_json::json!({ - "sessionId": "sess-1", - "toolCall": { "toolCallId": "call-1" }, - "options": [] - })) - .unwrap(); - let result = client.request_permission(req).await; - assert!(result.is_err()); + let tool_call = ToolCallUpdate::new("call-1", ToolCallUpdateFields::new()); + let req = RequestPermissionRequest::new("sess-1", tool_call, vec![]); + assert!(client.request_permission(req).await.is_ok()); } } diff --git a/rsworkspace/crates/trogon-std/src/env/in_memory.rs b/rsworkspace/crates/trogon-std/src/env/in_memory.rs index 458c268e6..28826cf15 100644 --- a/rsworkspace/crates/trogon-std/src/env/in_memory.rs +++ b/rsworkspace/crates/trogon-std/src/env/in_memory.rs @@ -115,6 +115,13 @@ mod tests { assert!(!env.contains("TEST_VAR_2")); } + #[test] + fn test_in_memory_env_default() { + let env = InMemoryEnv::default(); + env.set("KEY", "val"); + assert_eq!(env.var("KEY").unwrap(), "val"); + } + #[test] fn test_in_memory_env_overwrite() { let env = InMemoryEnv::new();