Files
Adam Perry @ OpenAI 99da697e4c [codex] Test extension API contracts (#26835)
## Why

`codex-extension-api` defines contracts shared by extension crates and
their hosts, but it had no direct test suite. Host and feature tests
cover downstream behavior, while regressions in the API crate's own
typed state, registry ordering, and capability adapters could go
unnoticed.

## What

- Add public-surface integration tests for `ExtensionData`, including
concurrent initialization and poison recovery.
- Cover contributor registration order, approval short-circuiting, event
sink retention, no-op response injection, and closure-based agent
spawning.
- Add the test-only dependencies used by the suite.

## Validation

- `just test -p codex-extension-api`
- `just argument-comment-lint -p codex-extension-api`
- `just bazel-lock-check`
2026-06-09 18:30:24 +00:00

110 lines
3.6 KiB
Rust

use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use codex_extension_api::ExtensionData;
use pretty_assertions::assert_eq;
#[test]
fn typed_values_can_be_inserted_replaced_and_removed() {
let data = ExtensionData::new("thread-1");
assert_eq!(data.insert(/*value*/ 41_u64), None);
assert_eq!(data.insert("alpha".to_string()), None);
assert_eq!(data.get::<u64>().as_deref(), Some(&41));
assert_eq!(
data.get::<String>().map(|value| value.as_str().to_string()),
Some("alpha".to_string())
);
assert_eq!(data.insert(/*value*/ 42_u64).as_deref(), Some(&41));
assert_eq!(data.get::<u64>().as_deref(), Some(&42));
assert_eq!(
data.remove::<String>()
.map(|value| value.as_str().to_string()),
Some("alpha".to_string())
);
assert_eq!(data.get::<String>(), None);
assert_eq!(data.get::<u64>().as_deref(), Some(&42));
}
#[test]
fn get_or_init_initializes_once_and_returns_shared_value() {
const CALLER_COUNT: usize = 8;
#[derive(Debug, PartialEq, Eq)]
struct SharedValue(usize);
let data = Arc::new(ExtensionData::new("session"));
let callers_started = Arc::new(AtomicUsize::new(0));
let initialization_count = Arc::new(AtomicUsize::new(0));
let handles: [_; CALLER_COUNT] = std::array::from_fn(|_| {
let data = Arc::clone(&data);
let callers_started = Arc::clone(&callers_started);
let initialization_count = Arc::clone(&initialization_count);
std::thread::spawn(move || {
callers_started.fetch_add(1, Ordering::SeqCst);
data.get_or_init(|| {
initialization_count.fetch_add(1, Ordering::SeqCst);
// Keep the first initializer active until every worker has attempted
// get_or_init, forcing callers to overlap on the same missing entry.
while callers_started.load(Ordering::SeqCst) < CALLER_COUNT {
std::thread::yield_now();
}
SharedValue(7)
})
})
});
let values = handles
.into_iter()
.map(|handle| handle.join().expect("initializer thread should succeed"))
.collect::<Vec<_>>();
assert_eq!(initialization_count.load(Ordering::SeqCst), 1);
assert_eq!(
values.iter().map(Arc::as_ref).collect::<Vec<_>>(),
vec![&SharedValue(7); CALLER_COUNT]
);
assert!(
values
.iter()
.skip(1)
.all(|value| Arc::ptr_eq(&values[0], value))
);
}
#[test]
fn stores_are_isolated_and_preserve_level_id() {
let session_data = ExtensionData::new("root-1");
let thread_data = ExtensionData::new("root-1");
session_data.insert(/*value*/ 17_u32);
thread_data.insert("thread value".to_string());
assert_eq!(session_data.level_id(), "root-1");
assert_eq!(thread_data.level_id(), "root-1");
assert_eq!(session_data.get::<u32>().as_deref(), Some(&17));
assert_eq!(session_data.get::<String>(), None);
assert_eq!(thread_data.get::<u32>(), None);
assert_eq!(
thread_data
.get::<String>()
.map(|value| value.as_str().to_string()),
Some("thread value".to_string())
);
}
#[test]
fn store_remains_usable_after_panicking_initializer() {
let data = ExtensionData::new("turn-1");
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
data.get_or_init::<u64>(|| panic!("initializer failed"));
}));
assert!(result.is_err());
assert_eq!(*data.get_or_init(|| 99_u64), 99);
}