mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
## What changed - Forward notification text to the session delegate without applying the previous 1,024-byte limit or appending a truncation suffix. - Update the gRPC host integration test to verify that oversized multibyte notification text is delivered unchanged. GitOrigin-RevId: 9a9e24b359a07540f70ec4e98b28524db3f7a4a0
307 lines
9.6 KiB
Rust
307 lines
9.6 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::PoisonError;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_code_mode::CellId;
|
|
use codex_code_mode::CodeModeNestedToolCall;
|
|
use codex_code_mode::CodeModeSession;
|
|
use codex_code_mode::CodeModeSessionDelegate;
|
|
use codex_code_mode::CodeModeSessionProvider;
|
|
use codex_code_mode::ExecuteRequest;
|
|
use codex_code_mode::FunctionCallOutputContentItem;
|
|
use codex_code_mode::GrpcCodeModeSessionProvider;
|
|
use codex_code_mode::NotificationFuture;
|
|
use codex_code_mode::RuntimeResponse;
|
|
use codex_code_mode::ToolInvocationFuture;
|
|
use codex_code_mode::WaitOutcome;
|
|
use codex_code_mode::WaitRequest;
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::sync::Semaphore;
|
|
use tokio::time::timeout;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
#[path = "support/host.rs"]
|
|
mod host;
|
|
#[path = "support/recording_delegate.rs"]
|
|
mod recording_delegate;
|
|
|
|
use host::HostHarness;
|
|
use recording_delegate::RecordingDelegate;
|
|
use recording_delegate::cell_id;
|
|
|
|
const TEST_TIMEOUT: Duration = Duration::from_secs(20);
|
|
|
|
struct BlockingNotificationDelegate {
|
|
started: Semaphore,
|
|
release: Semaphore,
|
|
delivered: Semaphore,
|
|
cancelled: Semaphore,
|
|
closed: Semaphore,
|
|
}
|
|
|
|
impl BlockingNotificationDelegate {
|
|
fn new() -> Self {
|
|
Self {
|
|
started: Semaphore::new(/*permits*/ 0),
|
|
release: Semaphore::new(/*permits*/ 0),
|
|
delivered: Semaphore::new(/*permits*/ 0),
|
|
cancelled: Semaphore::new(/*permits*/ 0),
|
|
closed: Semaphore::new(/*permits*/ 0),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CodeModeSessionDelegate for BlockingNotificationDelegate {
|
|
fn invoke_tool<'a>(
|
|
&'a self,
|
|
_invocation: CodeModeNestedToolCall,
|
|
_cancellation: CancellationToken,
|
|
) -> ToolInvocationFuture<'a> {
|
|
Box::pin(async { Err("unexpected tool invocation".to_string()) })
|
|
}
|
|
|
|
fn notify<'a>(
|
|
&'a self,
|
|
_call_id: String,
|
|
_cell_id: CellId,
|
|
_text: String,
|
|
cancellation: CancellationToken,
|
|
) -> NotificationFuture<'a> {
|
|
Box::pin(async move {
|
|
self.started.add_permits(/*n*/ 1);
|
|
tokio::select! {
|
|
_ = cancellation.cancelled() => {
|
|
self.cancelled.add_permits(/*n*/ 1);
|
|
Err("notification cancelled".to_string())
|
|
}
|
|
permit = self.release.acquire() => {
|
|
permit
|
|
.map_err(|_| "notification release closed".to_string())?
|
|
.forget();
|
|
self.delivered.add_permits(/*n*/ 1);
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn cell_closed(&self, _cell_id: &CellId) {
|
|
self.closed.add_permits(/*n*/ 1);
|
|
}
|
|
}
|
|
|
|
fn request(source: &str) -> ExecuteRequest {
|
|
ExecuteRequest {
|
|
tool_call_id: "call-1".to_string(),
|
|
enabled_tools: Vec::new(),
|
|
source: source.to_string(),
|
|
yield_time_ms: Some(/*value*/ 5_000),
|
|
max_output_tokens: Some(/*value*/ 1_000),
|
|
}
|
|
}
|
|
|
|
fn text_response(cell: &str, value: &str) -> RuntimeResponse {
|
|
RuntimeResponse::Result {
|
|
cell_id: cell_id(cell),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: value.to_string(),
|
|
}],
|
|
error_text: None,
|
|
}
|
|
}
|
|
|
|
async fn execute(
|
|
session: &Arc<dyn CodeModeSession>,
|
|
request: ExecuteRequest,
|
|
) -> Result<RuntimeResponse> {
|
|
timeout(TEST_TIMEOUT, async {
|
|
session
|
|
.execute(request)
|
|
.await
|
|
.map_err(anyhow::Error::msg)?
|
|
.initial_response()
|
|
.await
|
|
.map_err(anyhow::Error::msg)
|
|
})
|
|
.await
|
|
.context("timed out executing gRPC code-mode cell")?
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn completed_cells_drain_pending_notifications_before_completion() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session(delegate.clone())
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
let executing = Arc::clone(&session);
|
|
let completion = tokio::spawn(async move {
|
|
execute(&executing, request(r#"notify("notice"); text("done");"#)).await
|
|
});
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("notification did not start")??
|
|
.forget();
|
|
assert!(!completion.is_finished());
|
|
delegate.release.add_permits(/*n*/ 1);
|
|
assert_eq!(
|
|
timeout(TEST_TIMEOUT, completion)
|
|
.await
|
|
.context("completed cell did not finish after notification delivery")???,
|
|
text_response("1", "done")
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.delivered.acquire())
|
|
.await
|
|
.context("completed cell did not deliver its pending notification")??
|
|
.forget();
|
|
assert!(delegate.cancelled.try_acquire().is_err());
|
|
timeout(TEST_TIMEOUT, delegate.closed.acquire())
|
|
.await
|
|
.context("completed cell was not retired")??
|
|
.forget();
|
|
|
|
assert_eq!(
|
|
execute(&session, request(r#"text("still alive");"#)).await?,
|
|
text_response("2", "still alive")
|
|
);
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn completed_waits_drain_pending_notifications_before_returning() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session(delegate.clone())
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
let pending = request(r#"yield_control(); notify("notice"); text("done");"#);
|
|
let cell = session.execute(pending).await.map_err(anyhow::Error::msg)?;
|
|
assert_eq!(
|
|
cell.initial_response().await.map_err(anyhow::Error::msg)?,
|
|
RuntimeResponse::Yielded {
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
}
|
|
);
|
|
|
|
let waiting = Arc::clone(&session);
|
|
let completion = tokio::spawn(async move {
|
|
waiting
|
|
.wait(WaitRequest {
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 5_000,
|
|
})
|
|
.await
|
|
.map_err(anyhow::Error::msg)
|
|
});
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("wait notification did not start")??
|
|
.forget();
|
|
assert!(!completion.is_finished());
|
|
delegate.release.add_permits(/*n*/ 1);
|
|
assert_eq!(
|
|
timeout(TEST_TIMEOUT, completion)
|
|
.await
|
|
.context("wait did not finish after notification delivery")???,
|
|
WaitOutcome::LiveCell(text_response("1", "done"))
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.delivered.acquire())
|
|
.await
|
|
.context("wait did not deliver its pending notification")??
|
|
.forget();
|
|
assert!(delegate.cancelled.try_acquire().is_err());
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn termination_cancels_pending_notifications() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session(delegate.clone())
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
let mut pending = request(r#"notify("notice"); await new Promise(() => {});"#);
|
|
pending.yield_time_ms = Some(/*value*/ 1);
|
|
let cell = session.execute(pending).await.map_err(anyhow::Error::msg)?;
|
|
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("notification did not start")??
|
|
.forget();
|
|
assert_eq!(
|
|
cell.initial_response().await.map_err(anyhow::Error::msg)?,
|
|
RuntimeResponse::Yielded {
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
session
|
|
.terminate(cell_id("1"))
|
|
.await
|
|
.map_err(anyhow::Error::msg)?,
|
|
WaitOutcome::LiveCell(RuntimeResponse::Terminated {
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
})
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.cancelled.acquire())
|
|
.await
|
|
.context("termination did not cancel notification delivery")??
|
|
.forget();
|
|
timeout(TEST_TIMEOUT, delegate.closed.acquire())
|
|
.await
|
|
.context("terminated cell was not retired")??
|
|
.forget();
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn oversized_notification_text_is_delivered_unchanged() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(RecordingDelegate::default());
|
|
let session = provider
|
|
.create_session(delegate.clone())
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
assert_eq!(
|
|
execute(
|
|
&session,
|
|
request(r#"notify("🦀".repeat(512)); text("done");"#),
|
|
)
|
|
.await?,
|
|
text_response("1", "done")
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.notification_delivered.notified())
|
|
.await
|
|
.context("oversized notification was not delivered")?;
|
|
assert_eq!(
|
|
*delegate
|
|
.notifications
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner),
|
|
vec![("call-1".to_string(), cell_id("1"), "🦀".repeat(512),)]
|
|
);
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|