Files
codex/codex-rs/message-board/tests/common/mod.rs
T
jif 6c4fa86838 Add an in-memory launcher for the message-board MCP server (#58909)
## What changed

Expose `launch_in_memory()` and `LocalServer` to run a loopback HTTP endpoint with generated bearer credentials. Runners register a shared in-memory board per session through `add_board(session_id, host)`, using `MessageBoardHost` for membership, clocks, and notifications without a SQLite registry.

The handle exposes the endpoint and authorization header for MCP configuration, including plugin endpoint overrides. Dropping it cancels HTTP and MCP sessions; `shutdown().await` also waits for completion.

## Testing

Extend the parent/child HTTP integration scenario to cover the in-memory launcher, plugin endpoint overrides, host-provided timestamps, authentication and invalid identity rejection, and operation without creating a SQLite board database.

GitOrigin-RevId: e1cae295724a35f4eded290ba8a4e19926b98c13
2026-11-02 15:29:45 +00:00

83 lines
2.7 KiB
Rust

//! Shared host fixture for board and HTTP integration tests.
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use chrono::DateTime;
use chrono::Utc;
use codex_message_board::*;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use futures::future::BoxFuture;
pub struct Host {
pub clock: AtomicI64,
pub fail_clock: AtomicBool,
pub agent_path_calls: AtomicUsize,
pub members: HashMap<ThreadId, AgentPath>,
pub active: AtomicBool,
pub fail_notifications: AtomicBool,
pub notifications: Mutex<Vec<(ThreadId, PostMetadata)>>,
}
impl MessageBoardHost for Host {
fn agent_path(&self, caller: ThreadId) -> BoxFuture<'_, Result<AgentPath>> {
Box::pin(async move {
self.agent_path_calls.fetch_add(1, Ordering::SeqCst);
self.members
.get(&caller)
.cloned()
.ok_or_else(|| CodexErr::ThreadNotFound(caller))
})
}
fn resolve_agent(&self, path: AgentPath) -> BoxFuture<'_, Result<ThreadId>> {
Box::pin(async move {
self.members
.iter()
.find_map(|(id, member)| (*member == path).then_some(*id))
.ok_or_else(|| CodexErr::InvalidRequest("unknown agent".into()))
})
}
fn current_time(&self, _caller: ThreadId) -> BoxFuture<'_, Result<DateTime<Utc>>> {
Box::pin(async move {
if self.fail_clock.load(Ordering::SeqCst) {
return Err(CodexErr::Io(std::io::Error::other("clock unavailable")));
}
Ok(DateTime::parse_from_rfc3339("2026-09-18T12:00:00Z")
.map_err(|error| CodexErr::Io(std::io::Error::other(error)))?
.with_timezone(&Utc)
+ chrono::Duration::seconds(self.clock.fetch_add(1, Ordering::SeqCst)))
})
}
fn notify(
&self,
recipient: ThreadId,
post: PostPreview,
) -> BoxFuture<'_, Result<NotificationDelivery>> {
Box::pin(async move {
if self.fail_notifications.load(Ordering::SeqCst) {
return Err(CodexErr::Io(std::io::Error::other(
"notification transport failed",
)));
}
if !self.active.load(Ordering::SeqCst) {
return Ok(NotificationDelivery::SkippedInactive);
}
self.notifications
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((recipient, post.metadata));
Ok(NotificationDelivery::Accepted)
})
}
}