Skip to content

Commit a7b1155

Browse files
committed
feat(acp-nats): add integration test suite for Bridge, handlers, and workflows
Add tests/ module with agent_handlers, builders, prefix, and workflows integration tests covering session lifecycle, cancellation, and ext methods. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 0a0d4f4 commit a7b1155

File tree

11 files changed

+724
-24
lines changed

11 files changed

+724
-24
lines changed

rsworkspace/crates/acp-nats/src/agent/authenticate.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,11 @@ mod tests {
187187
#[tokio::test]
188188
async fn authenticate_forwards_request_and_returns_response() {
189189
let (mock, bridge) = mock_bridge();
190-
let expected = AuthenticateResponse::default();
190+
let expected = AuthenticateResponse::new();
191191
set_json_response(&mock, "acp.agent.authenticate", &expected);
192192

193-
let request = AuthenticateRequest::new("test");
193+
let request = AuthenticateRequest::new("api-key");
194194
let result = bridge.authenticate(request).await;
195-
196195
assert!(result.is_ok());
197196
}
198197

rsworkspace/crates/acp-nats/src/agent/cancel.rs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub async fn handle<N: RequestClient + PublishClient + FlushClient, C: GetElapse
8282
mod tests {
8383
use super::Bridge;
8484
use crate::config::Config;
85-
use agent_client_protocol::{Agent, CancelNotification, ErrorCode};
85+
use agent_client_protocol::{Agent, CancelNotification, ErrorCode, SessionId, StopReason};
8686
use opentelemetry::Value;
8787
use opentelemetry::metrics::MeterProvider;
8888
use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData};
@@ -91,6 +91,7 @@ mod tests {
9191
};
9292
use std::time::Duration;
9393
use trogon_nats::AdvancedMockNatsClient;
94+
use trogon_std::time::MockClock;
9495

9596
fn mock_bridge() -> (
9697
AdvancedMockNatsClient,
@@ -129,6 +130,22 @@ mod tests {
129130
(mock, bridge, exporter, provider)
130131
}
131132

133+
fn mock_bridge_with_clock() -> (
134+
AdvancedMockNatsClient,
135+
MockClock,
136+
Bridge<AdvancedMockNatsClient, MockClock>,
137+
) {
138+
let mock = AdvancedMockNatsClient::new();
139+
let clock = MockClock::new();
140+
let bridge = Bridge::new(
141+
mock.clone(),
142+
clock.clone(),
143+
&opentelemetry::global::meter("acp-nats-test"),
144+
Config::for_test("acp"),
145+
);
146+
(mock, clock, bridge)
147+
}
148+
132149
fn has_request_metric(
133150
finished_metrics: &[opentelemetry_sdk::metrics::data::ResourceMetrics],
134151
method: &str,
@@ -319,4 +336,102 @@ mod tests {
319336
));
320337
provider.shutdown().unwrap();
321338
}
339+
340+
#[tokio::test]
341+
async fn cancel_marks_session_as_cancelled() {
342+
let (_mock, _clock, bridge) = mock_bridge_with_clock();
343+
let session_id = "cancel-session-001";
344+
345+
assert!(
346+
bridge
347+
.cancelled_sessions
348+
.take_if_cancelled(&session_id.into(), &bridge.clock)
349+
.is_none()
350+
);
351+
352+
let notification = CancelNotification::new(session_id);
353+
bridge.cancel(notification).await.unwrap();
354+
355+
assert!(
356+
bridge
357+
.cancelled_sessions
358+
.take_if_cancelled(&session_id.into(), &bridge.clock)
359+
.is_some()
360+
);
361+
}
362+
363+
#[tokio::test]
364+
async fn cancel_resolves_pending_prompt_waiter_with_cancelled() {
365+
let (_mock, _clock, bridge) = mock_bridge_with_clock();
366+
let session_id: SessionId = "cancel-session-002".into();
367+
368+
let (rx, _guard, _token) = bridge
369+
.pending_session_prompt_responses
370+
.register_waiter(session_id.clone())
371+
.unwrap();
372+
373+
let notification = CancelNotification::new(session_id.clone());
374+
bridge.cancel(notification).await.unwrap();
375+
376+
let response = rx
377+
.await
378+
.expect("Should receive cancelled response")
379+
.expect("Prompt waiter should receive success response");
380+
assert_eq!(response.stop_reason, StopReason::Cancelled);
381+
}
382+
383+
#[tokio::test]
384+
async fn cancel_publishes_to_nats() {
385+
let (mock, _clock, bridge) = mock_bridge_with_clock();
386+
let session_id = "cancel-session-003";
387+
388+
let notification = CancelNotification::new(session_id);
389+
bridge.cancel(notification).await.unwrap();
390+
391+
let published = mock.published_messages();
392+
assert!(
393+
published.iter().any(|s| s.contains("session.cancel")),
394+
"Expected cancel publish, got: {:?}",
395+
published
396+
);
397+
}
398+
399+
#[tokio::test]
400+
async fn cancel_session_evicts_expired_on_mark() {
401+
let (_mock, clock, bridge) = mock_bridge_with_clock();
402+
403+
let session_old: SessionId = "old-session".into();
404+
let session_new: SessionId = "new-session".into();
405+
406+
bridge
407+
.cancelled_sessions
408+
.mark_cancelled(session_old.clone(), &bridge.clock);
409+
410+
clock.advance(Duration::from_secs(301));
411+
412+
for idx in 0..15 {
413+
let filler_session: SessionId = format!("filler-{idx}").into();
414+
bridge
415+
.cancelled_sessions
416+
.mark_cancelled(filler_session, &bridge.clock);
417+
}
418+
419+
bridge
420+
.cancelled_sessions
421+
.mark_cancelled(session_new.clone(), &bridge.clock);
422+
423+
assert!(
424+
bridge
425+
.cancelled_sessions
426+
.take_if_cancelled(&session_old, &bridge.clock)
427+
.is_none()
428+
);
429+
430+
assert!(
431+
bridge
432+
.cancelled_sessions
433+
.take_if_cancelled(&session_new, &bridge.clock)
434+
.is_some()
435+
);
436+
}
322437
}

rsworkspace/crates/acp-nats/src/agent/initialize.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,21 @@ mod tests {
317317
assert!(err.to_string().contains("Initialize request failed"));
318318
assert_eq!(err.code, ErrorCode::InternalError);
319319
}
320+
321+
#[tokio::test]
322+
async fn handlers_use_custom_prefix() {
323+
let mock = AdvancedMockNatsClient::new();
324+
let bridge = Bridge::new(
325+
mock.clone(),
326+
trogon_std::time::SystemClock,
327+
&opentelemetry::global::meter("acp-nats-test"),
328+
Config::for_test("myorg.prod"),
329+
);
330+
let expected = InitializeResponse::new(ProtocolVersion::LATEST);
331+
set_json_response(&mock, "myorg.prod.agent.initialize", &expected);
332+
333+
let request = InitializeRequest::new(ProtocolVersion::LATEST);
334+
let result = bridge.initialize(request).await;
335+
assert!(result.is_ok());
336+
}
320337
}

rsworkspace/crates/acp-nats/src/agent/load_session.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,24 @@ mod tests {
250250

251251
#[tokio::test]
252252
async fn load_session_forwards_request_and_returns_response() {
253-
let (mock, bridge) = mock_bridge();
254-
let expected = LoadSessionResponse::new();
255-
set_json_response(&mock, "acp.s1.agent.session.load", &expected);
256-
257-
let request = LoadSessionRequest::new("s1", ".");
258-
let result = bridge.load_session(request).await;
259-
260-
assert!(result.is_ok());
253+
let local = tokio::task::LocalSet::new();
254+
local
255+
.run_until(async {
256+
let mock = AdvancedMockNatsClient::new();
257+
let bridge = Bridge::new(
258+
mock.clone(),
259+
trogon_std::time::MockClock::new(),
260+
&opentelemetry::global::meter("acp-nats-test"),
261+
Config::for_test("acp"),
262+
);
263+
let expected = LoadSessionResponse::new();
264+
set_json_response(&mock, "acp.session-load-001.agent.session.load", &expected);
265+
266+
let request = LoadSessionRequest::new("session-load-001", "/tmp");
267+
let result = bridge.load_session(request).await;
268+
assert!(result.is_ok());
269+
})
270+
.await;
261271
}
262272

263273
#[tokio::test]

rsworkspace/crates/acp-nats/src/agent/new_session.rs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,61 @@ mod tests {
239239

240240
#[tokio::test]
241241
async fn new_session_forwards_request_and_returns_response() {
242-
let (mock, bridge) = mock_bridge();
243-
let session_id = SessionId::from("test-session-1");
244-
let expected = NewSessionResponse::new(session_id.clone());
245-
set_json_response(&mock, "acp.agent.session.new", &expected);
242+
let local = tokio::task::LocalSet::new();
243+
local
244+
.run_until(async {
245+
let mock = AdvancedMockNatsClient::new();
246+
let bridge = Bridge::new(
247+
mock.clone(),
248+
trogon_std::time::MockClock::new(),
249+
&opentelemetry::global::meter("acp-nats-test"),
250+
Config::for_test("acp"),
251+
);
252+
let expected = NewSessionResponse::new("session-001");
253+
set_json_response(&mock, "acp.agent.session.new", &expected);
254+
255+
let request = NewSessionRequest::new("/tmp");
256+
let result = bridge.new_session(request).await;
257+
258+
assert!(result.is_ok());
259+
let response = result.unwrap();
260+
assert_eq!(response.session_id.to_string(), "session-001");
261+
})
262+
.await;
263+
}
246264

247-
let request = NewSessionRequest::new(".");
248-
let result = bridge.new_session(request).await;
265+
#[tokio::test]
266+
async fn session_ready_publish_tasks_are_tracked_and_awaited() {
267+
let mock = AdvancedMockNatsClient::new();
268+
let expected = NewSessionResponse::new("session-ready-track");
269+
set_json_response(&mock, "acp.agent.session.new", &expected);
249270

250-
assert!(result.is_ok());
251-
let response = result.unwrap();
252-
assert_eq!(response.session_id, session_id);
271+
let local = tokio::task::LocalSet::new();
272+
local
273+
.run_until(async {
274+
let bridge = Bridge::new(
275+
mock.clone(),
276+
trogon_std::time::MockClock::new(),
277+
&opentelemetry::global::meter("acp-nats-test"),
278+
Config::for_test("acp"),
279+
);
280+
let request = NewSessionRequest::new("/tmp");
281+
let result = bridge.new_session(request).await;
282+
assert!(result.is_ok());
283+
284+
assert!(
285+
bridge.has_pending_session_ready_tasks(),
286+
"session.ready task should be tracked"
287+
);
288+
289+
bridge.await_session_ready_tasks().await;
290+
291+
assert!(
292+
!bridge.has_pending_session_ready_tasks(),
293+
"session.ready tasks should be fully awaited and cleared"
294+
);
295+
})
296+
.await;
253297
}
254298

255299
#[tokio::test]

rsworkspace/crates/acp-nats/src/agent/set_session_mode.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,14 @@ mod tests {
191191
async fn set_session_mode_forwards_request_and_returns_response() {
192192
let (mock, bridge) = mock_bridge();
193193
let expected = SetSessionModeResponse::new();
194-
set_json_response(&mock, "acp.s1.agent.session.set_mode", &expected);
194+
set_json_response(
195+
&mock,
196+
"acp.session-mode-001.agent.session.set_mode",
197+
&expected,
198+
);
195199

196-
let request = SetSessionModeRequest::new("s1", "mode-1");
200+
let request = SetSessionModeRequest::new("session-mode-001", "auto");
197201
let result = bridge.set_session_mode(request).await;
198-
199202
assert!(result.is_ok());
200203
}
201204

rsworkspace/crates/acp-nats/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub mod prompt_slot_counter;
1212
pub mod session_id;
1313
pub mod subject_token_violation;
1414
pub(crate) mod telemetry;
15+
#[cfg(test)]
16+
mod tests;
1517

1618
pub use acp_prefix::{AcpPrefix, AcpPrefixError};
1719
pub use agent::Bridge;

rsworkspace/crates/acp-nats/src/nats/subjects.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,34 @@ mod tests {
306306
assert!(agent::session_set_mode(prefix, sid).starts_with(&expected_prefix));
307307
assert!(agent::ext_session_ready(prefix, sid).starts_with(&expected_prefix));
308308
}
309+
310+
#[test]
311+
fn multi_tenant_prefixes_are_isolated() {
312+
let sid = "sess123";
313+
assert_ne!(agent::initialize("tenant1"), agent::initialize("tenant2"));
314+
assert_ne!(
315+
agent::session_prompt("tenant1", sid),
316+
agent::session_prompt("tenant2", sid),
317+
);
318+
}
319+
320+
#[test]
321+
fn environment_based_prefixes() {
322+
assert_eq!(agent::initialize("dev"), "dev.agent.initialize");
323+
assert_eq!(agent::initialize("prod"), "prod.agent.initialize");
324+
assert_eq!(agent::initialize("staging"), "staging.agent.initialize");
325+
}
326+
327+
#[test]
328+
fn special_characters_in_prefix() {
329+
assert_eq!(agent::initialize("my_app"), "my_app.agent.initialize");
330+
assert_eq!(agent::initialize("my-app"), "my-app.agent.initialize");
331+
assert_eq!(agent::initialize("app123"), "app123.agent.initialize");
332+
}
333+
334+
#[test]
335+
fn prefix_does_not_add_extra_namespace() {
336+
assert_eq!(agent::initialize("myapp"), "myapp.agent.initialize");
337+
assert_ne!(agent::initialize("myapp"), "myapp.acp.agent.initialize");
338+
}
309339
}

0 commit comments

Comments
 (0)