mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-20 22:02:42 +00:00
feat: expand api snapshots and schema tooling
This commit is contained in:
parent
ee014b0838
commit
011ca27287
72 changed files with 29480 additions and 1081 deletions
|
|
@ -10,30 +10,30 @@ name = "sandbox-agent"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
axum = "0.7"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
sandbox-agent-error = { path = "../error" }
|
||||
sandbox-agent-agent-management = { path = "../agent-management" }
|
||||
sandbox-agent-agent-credentials = { path = "../agent-credentials" }
|
||||
sandbox-agent-universal-agent-schema = { path = "../universal-agent-schema" }
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
|
||||
dirs = "5.0"
|
||||
time = { version = "0.3", features = ["parsing", "formatting"] }
|
||||
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal", "time"] }
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||
utoipa = { version = "4.2", features = ["axum_extras"] }
|
||||
schemars = "0.8"
|
||||
tracing = "0.1"
|
||||
tracing-logfmt = "0.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
sandbox-agent-error.workspace = true
|
||||
sandbox-agent-agent-management.workspace = true
|
||||
sandbox-agent-agent-credentials.workspace = true
|
||||
sandbox-agent-universal-agent-schema.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
axum.workspace = true
|
||||
clap.workspace = true
|
||||
futures.workspace = true
|
||||
reqwest.workspace = true
|
||||
dirs.workspace = true
|
||||
time.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa.workspace = true
|
||||
schemars.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-logfmt.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
http-body-util = "0.1"
|
||||
insta = "1.41"
|
||||
tempfile = "3.10"
|
||||
tower = "0.4"
|
||||
http-body-util.workspace = true
|
||||
insta.workspace = true
|
||||
tempfile.workspace = true
|
||||
tower.workspace = true
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,11 @@ async fn require_token(
|
|||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Result<Response, ApiError> {
|
||||
let path = req.uri().path();
|
||||
if path == "/v1/health" || path == "/health" {
|
||||
return Ok(next.run(req).await);
|
||||
}
|
||||
|
||||
let expected = match &state.auth.token {
|
||||
Some(token) => token.as_str(),
|
||||
None => return Ok(next.run(req).await),
|
||||
|
|
@ -1946,7 +1951,7 @@ fn parse_agent_line(agent: AgentId, line: &str, session_id: &str) -> Option<Even
|
|||
convert_claude::event_to_universal_with_session(&value, session_id.to_string())
|
||||
}
|
||||
AgentId::Codex => match serde_json::from_value(value.clone()) {
|
||||
Ok(event) => convert_codex::event_to_universal(&event),
|
||||
Ok(notification) => convert_codex::notification_to_universal(¬ification),
|
||||
Err(err) => EventConversion::new(unparsed_message(
|
||||
&value.to_string(),
|
||||
&err.to_string(),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{Method, Request, StatusCode};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode};
|
||||
use axum::Router;
|
||||
use futures::StreamExt;
|
||||
use http_body_util::BodyExt;
|
||||
|
|
@ -13,9 +13,13 @@ use sandbox_agent_agent_management::agents::{AgentId, AgentManager};
|
|||
use sandbox_agent_agent_management::testing::{test_agents_from_env, TestAgentConfig};
|
||||
use sandbox_agent_agent_credentials::ExtractedCredentials;
|
||||
use sandbox_agent_core::router::{build_router, AppState, AuthConfig};
|
||||
use tower::ServiceExt;
|
||||
use tower::util::ServiceExt;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
const PROMPT: &str = "Reply with exactly the single word OK.";
|
||||
const PERMISSION_PROMPT: &str = "List files in the current directory using available tools.";
|
||||
const QUESTION_PROMPT: &str =
|
||||
"Ask the user a multiple-choice question with options yes/no using any built-in AskUserQuestion tool, then wait.";
|
||||
|
||||
struct TestApp {
|
||||
app: Router,
|
||||
|
|
@ -24,11 +28,22 @@ struct TestApp {
|
|||
|
||||
impl TestApp {
|
||||
fn new() -> Self {
|
||||
Self::new_with_auth(AuthConfig::disabled())
|
||||
}
|
||||
|
||||
fn new_with_auth(auth: AuthConfig) -> Self {
|
||||
Self::new_with_auth_and_cors(auth, None)
|
||||
}
|
||||
|
||||
fn new_with_auth_and_cors(auth: AuthConfig, cors: Option<CorsLayer>) -> Self {
|
||||
let install_dir = tempfile::tempdir().expect("create temp install dir");
|
||||
let manager = AgentManager::new(install_dir.path())
|
||||
.expect("create agent manager");
|
||||
let state = AppState::new(AuthConfig::disabled(), manager);
|
||||
let app = build_router(state);
|
||||
let state = AppState::new(auth, manager);
|
||||
let mut app = build_router(state);
|
||||
if let Some(cors) = cors {
|
||||
app = app.layer(cors);
|
||||
}
|
||||
Self {
|
||||
app,
|
||||
_install_dir: install_dir,
|
||||
|
|
@ -112,6 +127,37 @@ async fn send_json(app: &Router, method: Method, path: &str, body: Option<Value>
|
|||
(status, value)
|
||||
}
|
||||
|
||||
async fn send_request(app: &Router, request: Request<Body>) -> (StatusCode, HeaderMap, Bytes) {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request)
|
||||
.await
|
||||
.expect("request handled");
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let bytes = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("read body")
|
||||
.to_bytes();
|
||||
(status, headers, bytes)
|
||||
}
|
||||
|
||||
async fn send_json_request(
|
||||
app: &Router,
|
||||
request: Request<Body>,
|
||||
) -> (StatusCode, HeaderMap, Value) {
|
||||
let (status, headers, bytes) = send_request(app, request).await;
|
||||
let value = if bytes.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
serde_json::from_slice(&bytes)
|
||||
.unwrap_or(Value::String(String::from_utf8_lossy(&bytes).to_string()))
|
||||
};
|
||||
(status, headers, value)
|
||||
}
|
||||
|
||||
async fn send_status(app: &Router, method: Method, path: &str, body: Option<Value>) -> StatusCode {
|
||||
let (status, _) = send_json(app, method, path, body).await;
|
||||
status
|
||||
|
|
@ -128,14 +174,14 @@ async fn install_agent(app: &Router, agent: AgentId) {
|
|||
assert_eq!(status, StatusCode::NO_CONTENT, "install {agent}");
|
||||
}
|
||||
|
||||
async fn create_session(app: &Router, agent: AgentId, session_id: &str) {
|
||||
async fn create_session(app: &Router, agent: AgentId, session_id: &str, permission_mode: &str) {
|
||||
let status = send_status(
|
||||
app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{session_id}"),
|
||||
Some(json!({
|
||||
"agent": agent.as_str(),
|
||||
"permissionMode": "bypass"
|
||||
"permissionMode": permission_mode
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -211,7 +257,7 @@ async fn read_sse_events(
|
|||
_ => break,
|
||||
};
|
||||
let next = tokio::time::timeout(remaining, stream.next()).await;
|
||||
let chunk = match next {
|
||||
let chunk: Bytes = match next {
|
||||
Ok(Some(Ok(chunk))) => chunk,
|
||||
Ok(Some(Err(_))) => break,
|
||||
Ok(None) => break,
|
||||
|
|
@ -267,6 +313,23 @@ fn is_error_event(event: &Value) -> bool {
|
|||
.is_some()
|
||||
}
|
||||
|
||||
fn is_permission_event(event: &Value) -> bool {
|
||||
event
|
||||
.get("data")
|
||||
.and_then(|data| data.get("permissionAsked"))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn truncate_permission_events(events: &[Value]) -> Vec<Value> {
|
||||
if let Some(idx) = events.iter().position(is_permission_event) {
|
||||
return events[..=idx].to_vec();
|
||||
}
|
||||
if let Some(idx) = events.iter().position(is_assistant_message) {
|
||||
return events[..=idx].to_vec();
|
||||
}
|
||||
events.to_vec()
|
||||
}
|
||||
|
||||
fn normalize_events(events: &[Value]) -> Value {
|
||||
let normalized = events
|
||||
.iter()
|
||||
|
|
@ -276,6 +339,16 @@ fn normalize_events(events: &[Value]) -> Value {
|
|||
Value::Array(normalized)
|
||||
}
|
||||
|
||||
fn truncate_after_first_stop(events: &[Value]) -> Vec<Value> {
|
||||
if let Some(idx) = events
|
||||
.iter()
|
||||
.position(|event| is_assistant_message(event) || is_error_event(event))
|
||||
{
|
||||
return events[..=idx].to_vec();
|
||||
}
|
||||
events.to_vec()
|
||||
}
|
||||
|
||||
fn normalize_event(event: &Value, seq: usize) -> Value {
|
||||
let mut map = Map::new();
|
||||
map.insert("seq".to_string(), Value::Number(seq.into()));
|
||||
|
|
@ -379,8 +452,239 @@ fn normalize_permission(permission: &Value) -> Value {
|
|||
Value::Object(map)
|
||||
}
|
||||
|
||||
fn snapshot_name(prefix: &str, agent: AgentId) -> String {
|
||||
format!("{prefix}_{}", agent.as_str())
|
||||
fn normalize_agent_list(value: &Value) -> Value {
|
||||
let agents = value
|
||||
.get("agents")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut normalized = Vec::new();
|
||||
for agent in agents {
|
||||
let mut map = Map::new();
|
||||
if let Some(id) = agent.get("id").and_then(Value::as_str) {
|
||||
map.insert("id".to_string(), Value::String(id.to_string()));
|
||||
}
|
||||
// Skip installed/version/path fields - they depend on local environment
|
||||
// and make snapshots non-deterministic
|
||||
normalized.push(Value::Object(map));
|
||||
}
|
||||
normalized.sort_by(|a, b| {
|
||||
a.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.cmp(&b.get("id").and_then(Value::as_str))
|
||||
});
|
||||
json!({ "agents": normalized })
|
||||
}
|
||||
|
||||
fn normalize_agent_modes(value: &Value) -> Value {
|
||||
let modes = value
|
||||
.get("modes")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut normalized = Vec::new();
|
||||
for mode in modes {
|
||||
let mut map = Map::new();
|
||||
if let Some(id) = mode.get("id").and_then(Value::as_str) {
|
||||
map.insert("id".to_string(), Value::String(id.to_string()));
|
||||
}
|
||||
if let Some(name) = mode.get("name").and_then(Value::as_str) {
|
||||
map.insert("name".to_string(), Value::String(name.to_string()));
|
||||
}
|
||||
if mode.get("description").is_some() {
|
||||
map.insert("description".to_string(), Value::Bool(true));
|
||||
}
|
||||
normalized.push(Value::Object(map));
|
||||
}
|
||||
normalized.sort_by(|a, b| {
|
||||
a.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.cmp(&b.get("id").and_then(Value::as_str))
|
||||
});
|
||||
json!({ "modes": normalized })
|
||||
}
|
||||
|
||||
fn normalize_sessions(value: &Value) -> Value {
|
||||
let sessions = value
|
||||
.get("sessions")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut normalized = Vec::new();
|
||||
for session in sessions {
|
||||
let mut map = Map::new();
|
||||
if let Some(session_id) = session.get("sessionId").and_then(Value::as_str) {
|
||||
map.insert("sessionId".to_string(), Value::String(session_id.to_string()));
|
||||
}
|
||||
if let Some(agent) = session.get("agent").and_then(Value::as_str) {
|
||||
map.insert("agent".to_string(), Value::String(agent.to_string()));
|
||||
}
|
||||
if let Some(agent_mode) = session.get("agentMode").and_then(Value::as_str) {
|
||||
map.insert("agentMode".to_string(), Value::String(agent_mode.to_string()));
|
||||
}
|
||||
if let Some(permission_mode) = session.get("permissionMode").and_then(Value::as_str) {
|
||||
map.insert("permissionMode".to_string(), Value::String(permission_mode.to_string()));
|
||||
}
|
||||
if session.get("model").is_some() {
|
||||
map.insert("model".to_string(), Value::String("<redacted>".to_string()));
|
||||
}
|
||||
if session.get("variant").is_some() {
|
||||
map.insert("variant".to_string(), Value::String("<redacted>".to_string()));
|
||||
}
|
||||
if session.get("agentSessionId").is_some() {
|
||||
map.insert("agentSessionId".to_string(), Value::String("<redacted>".to_string()));
|
||||
}
|
||||
if let Some(ended) = session.get("ended").and_then(Value::as_bool) {
|
||||
map.insert("ended".to_string(), Value::Bool(ended));
|
||||
}
|
||||
if session.get("eventCount").is_some() {
|
||||
map.insert("eventCount".to_string(), Value::String("<redacted>".to_string()));
|
||||
}
|
||||
normalized.push(Value::Object(map));
|
||||
}
|
||||
normalized.sort_by(|a, b| {
|
||||
a.get("sessionId")
|
||||
.and_then(Value::as_str)
|
||||
.cmp(&b.get("sessionId").and_then(Value::as_str))
|
||||
});
|
||||
json!({ "sessions": normalized })
|
||||
}
|
||||
|
||||
fn normalize_create_session(value: &Value) -> Value {
|
||||
let mut map = Map::new();
|
||||
if let Some(healthy) = value.get("healthy").and_then(Value::as_bool) {
|
||||
map.insert("healthy".to_string(), Value::Bool(healthy));
|
||||
}
|
||||
if value.get("agentSessionId").is_some() {
|
||||
map.insert("agentSessionId".to_string(), Value::String("<redacted>".to_string()));
|
||||
}
|
||||
if let Some(error) = value.get("error") {
|
||||
map.insert("error".to_string(), error.clone());
|
||||
}
|
||||
Value::Object(map)
|
||||
}
|
||||
|
||||
fn normalize_health(value: &Value) -> Value {
|
||||
let mut map = Map::new();
|
||||
if let Some(status) = value.get("status").and_then(Value::as_str) {
|
||||
map.insert("status".to_string(), Value::String(status.to_string()));
|
||||
}
|
||||
Value::Object(map)
|
||||
}
|
||||
|
||||
fn snapshot_status(status: StatusCode) -> Value {
|
||||
json!({ "status": status.as_u16() })
|
||||
}
|
||||
|
||||
fn snapshot_cors(status: StatusCode, headers: &HeaderMap) -> Value {
|
||||
let mut map = Map::new();
|
||||
map.insert("status".to_string(), Value::Number(status.as_u16().into()));
|
||||
for name in [
|
||||
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||
header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
header::VARY,
|
||||
] {
|
||||
if let Some(value) = headers.get(&name) {
|
||||
map.insert(
|
||||
name.as_str().to_string(),
|
||||
Value::String(value.to_str().unwrap_or("<invalid>").to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Object(map)
|
||||
}
|
||||
|
||||
fn snapshot_name(prefix: &str, agent: Option<AgentId>) -> String {
|
||||
match agent {
|
||||
Some(agent) => format!("{prefix}_{}", agent.as_str()),
|
||||
None => format!("{prefix}_global"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn poll_events_until_match<F>(
|
||||
app: &Router,
|
||||
session_id: &str,
|
||||
timeout: Duration,
|
||||
stop: F,
|
||||
) -> Vec<Value>
|
||||
where
|
||||
F: Fn(&[Value]) -> bool,
|
||||
{
|
||||
let start = Instant::now();
|
||||
let mut offset = 0u64;
|
||||
let mut events = Vec::new();
|
||||
while start.elapsed() < timeout {
|
||||
let path = format!("/v1/sessions/{session_id}/events?offset={offset}&limit=200");
|
||||
let (status, payload) = send_json(app, Method::GET, &path, None).await;
|
||||
assert_eq!(status, StatusCode::OK, "poll events");
|
||||
let new_events = payload
|
||||
.get("events")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !new_events.is_empty() {
|
||||
if let Some(last) = new_events
|
||||
.last()
|
||||
.and_then(|event| event.get("id"))
|
||||
.and_then(Value::as_u64)
|
||||
{
|
||||
offset = last;
|
||||
}
|
||||
events.extend(new_events);
|
||||
if stop(&events) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn find_permission_id(events: &[Value]) -> Option<String> {
|
||||
events
|
||||
.iter()
|
||||
.find_map(|event| {
|
||||
event
|
||||
.get("data")
|
||||
.and_then(|data| data.get("permissionAsked"))
|
||||
.and_then(|permission| permission.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|id| id.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn find_question_id_and_answers(events: &[Value]) -> Option<(String, Vec<Vec<String>>)> {
|
||||
let question = events.iter().find_map(|event| {
|
||||
event
|
||||
.get("data")
|
||||
.and_then(|data| data.get("questionAsked"))
|
||||
.cloned()
|
||||
})?;
|
||||
let id = question.get("id").and_then(Value::as_str)?.to_string();
|
||||
let questions = question
|
||||
.get("questions")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut answers = Vec::new();
|
||||
for question in questions {
|
||||
let option = question
|
||||
.get("options")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|options| options.first())
|
||||
.and_then(|option| option.get("label"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|label| label.to_string());
|
||||
if let Some(label) = option {
|
||||
answers.push(vec![label]);
|
||||
} else {
|
||||
answers.push(Vec::new());
|
||||
}
|
||||
}
|
||||
Some((id, answers))
|
||||
}
|
||||
|
||||
async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||
|
|
@ -388,10 +692,11 @@ async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
|||
install_agent(app, config.agent).await;
|
||||
|
||||
let session_id = format!("session-{}", config.agent.as_str());
|
||||
create_session(app, config.agent, &session_id).await;
|
||||
create_session(app, config.agent, &session_id, "bypass").await;
|
||||
send_message(app, &session_id).await;
|
||||
|
||||
let events = poll_events_until(app, &session_id, Duration::from_secs(120)).await;
|
||||
let events = truncate_after_first_stop(&events);
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"no events collected for {}",
|
||||
|
|
@ -404,7 +709,7 @@ async fn run_http_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
|||
);
|
||||
let normalized = normalize_events(&events);
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("http_events", config.agent),
|
||||
snapshot_suffix => snapshot_name("http_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalized);
|
||||
});
|
||||
|
|
@ -415,7 +720,7 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
|||
install_agent(app, config.agent).await;
|
||||
|
||||
let session_id = format!("sse-{}", config.agent.as_str());
|
||||
create_session(app, config.agent, &session_id).await;
|
||||
create_session(app, config.agent, &session_id, "bypass").await;
|
||||
|
||||
let sse_task = {
|
||||
let app = app.clone();
|
||||
|
|
@ -428,6 +733,7 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
|||
send_message(app, &session_id).await;
|
||||
|
||||
let events = sse_task.await.expect("sse task");
|
||||
let events = truncate_after_first_stop(&events);
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"no sse events collected for {}",
|
||||
|
|
@ -440,26 +746,494 @@ async fn run_sse_events_snapshot(app: &Router, config: &TestAgentConfig) {
|
|||
);
|
||||
let normalized = normalize_events(&events);
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("sse_events", config.agent),
|
||||
snapshot_suffix => snapshot_name("sse_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalized);
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn auth_snapshots() {
|
||||
let token = "test-token";
|
||||
let app = TestApp::new_with_auth(AuthConfig::with_token(token.to_string()));
|
||||
|
||||
let (status, payload) = send_json(&app.app, Method::GET, "/v1/health", None).await;
|
||||
assert_eq!(status, StatusCode::OK, "health should be public");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("auth_health_public", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": normalize_health(&payload),
|
||||
}));
|
||||
});
|
||||
|
||||
let (status, payload) = send_json(&app.app, Method::GET, "/v1/agents", None).await;
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED, "missing token should 401");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("auth_missing_token", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": payload,
|
||||
}));
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/v1/agents")
|
||||
.header(header::AUTHORIZATION, "Bearer wrong-token")
|
||||
.body(Body::empty())
|
||||
.expect("auth invalid request");
|
||||
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED, "invalid token should 401");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("auth_invalid_token", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": payload,
|
||||
}));
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/v1/agents")
|
||||
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.body(Body::empty())
|
||||
.expect("auth valid request");
|
||||
let (status, _headers, payload) = send_json_request(&app.app, request).await;
|
||||
assert_eq!(status, StatusCode::OK, "valid token should allow request");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("auth_valid_token", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": normalize_agent_list(&payload),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn cors_snapshots() {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(vec![HeaderValue::from_static("http://example.com")])
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
|
||||
.allow_credentials(true);
|
||||
let app = TestApp::new_with_auth_and_cors(AuthConfig::disabled(), Some(cors));
|
||||
|
||||
let preflight = Request::builder()
|
||||
.method(Method::OPTIONS)
|
||||
.uri("/v1/health")
|
||||
.header(header::ORIGIN, "http://example.com")
|
||||
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||
.header(
|
||||
header::ACCESS_CONTROL_REQUEST_HEADERS,
|
||||
"authorization,content-type",
|
||||
)
|
||||
.body(Body::empty())
|
||||
.expect("cors preflight request");
|
||||
let (status, headers, _payload) = send_request(&app.app, preflight).await;
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("cors_preflight", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_cors(status, &headers));
|
||||
});
|
||||
|
||||
let actual = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/v1/health")
|
||||
.header(header::ORIGIN, "http://example.com")
|
||||
.body(Body::empty())
|
||||
.expect("cors actual request");
|
||||
let (status, headers, payload) = send_json_request(&app.app, actual).await;
|
||||
assert_eq!(status, StatusCode::OK, "cors actual request should succeed");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("cors_actual", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"cors": snapshot_cors(status, &headers),
|
||||
"payload": normalize_health(&payload),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn api_endpoints_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
let app = TestApp::new();
|
||||
|
||||
let (status, health) = send_json(&app.app, Method::GET, "/v1/health", None).await;
|
||||
assert_eq!(status, StatusCode::OK, "health status");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("health", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_health(&health));
|
||||
});
|
||||
|
||||
// List agents (just verify the API returns correct agent IDs, not install state)
|
||||
let (status, agents) = send_json(&app.app, Method::GET, "/v1/agents", None).await;
|
||||
assert_eq!(status, StatusCode::OK, "agents list");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("agents_list", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_agent_list(&agents));
|
||||
});
|
||||
|
||||
// Install agents (ensure they're available for subsequent tests)
|
||||
for config in &configs {
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/agents/{}/install", config.agent.as_str()),
|
||||
Some(json!({})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "install agent");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("agent_install", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||
});
|
||||
}
|
||||
|
||||
let mut session_ids = Vec::new();
|
||||
for config in &configs {
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
let (status, modes) = send_json(
|
||||
&app.app,
|
||||
Method::GET,
|
||||
&format!("/v1/agents/{}/modes", config.agent.as_str()),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK, "agent modes");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("agent_modes", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_agent_modes(&modes));
|
||||
});
|
||||
|
||||
let session_id = format!("snapshot-{}", config.agent.as_str());
|
||||
let (status, created) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{session_id}"),
|
||||
Some(json!({
|
||||
"agent": config.agent.as_str(),
|
||||
"permissionMode": "bypass"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::OK, "create session");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("create_session", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_create_session(&created));
|
||||
});
|
||||
session_ids.push((config.agent, session_id));
|
||||
}
|
||||
|
||||
let (status, sessions) = send_json(&app.app, Method::GET, "/v1/sessions", None).await;
|
||||
assert_eq!(status, StatusCode::OK, "list sessions");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("sessions_list", None),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_sessions(&sessions));
|
||||
});
|
||||
|
||||
for (agent, session_id) in &session_ids {
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{session_id}/messages"),
|
||||
Some(json!({ "message": PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send message");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("send_message", Some(*agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn approval_flow_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
let app = TestApp::new();
|
||||
|
||||
for config in &configs {
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
install_agent(&app.app, config.agent).await;
|
||||
|
||||
let permission_session = format!("perm-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &permission_session, "plan").await;
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{permission_session}/messages"),
|
||||
Some(json!({ "message": PERMISSION_PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send permission prompt");
|
||||
|
||||
let permission_events = poll_events_until_match(
|
||||
&app.app,
|
||||
&permission_session,
|
||||
Duration::from_secs(120),
|
||||
|events| find_permission_id(events).is_some() || should_stop(events),
|
||||
)
|
||||
.await;
|
||||
let permission_events = truncate_permission_events(&permission_events);
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("permission_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_events(&permission_events));
|
||||
});
|
||||
|
||||
if let Some(permission_id) = find_permission_id(&permission_events) {
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{permission_session}/permissions/{permission_id}/reply"
|
||||
),
|
||||
Some(json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "reply permission");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("permission_reply", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||
});
|
||||
} else {
|
||||
let (status, payload) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{permission_session}/permissions/missing-permission/reply"
|
||||
),
|
||||
Some(json!({ "reply": "once" })),
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "missing permission id should error");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("permission_reply_missing", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": payload,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
let question_reply_session = format!("question-reply-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &question_reply_session, "bypass").await;
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{question_reply_session}/messages"),
|
||||
Some(json!({ "message": QUESTION_PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt");
|
||||
|
||||
let question_events = poll_events_until_match(
|
||||
&app.app,
|
||||
&question_reply_session,
|
||||
Duration::from_secs(120),
|
||||
|events| find_question_id_and_answers(events).is_some() || should_stop(events),
|
||||
)
|
||||
.await;
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reply_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_events(&question_events));
|
||||
});
|
||||
|
||||
if let Some((question_id, answers)) = find_question_id_and_answers(&question_events) {
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reply_session}/questions/{question_id}/reply"
|
||||
),
|
||||
Some(json!({ "answers": answers })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "reply question");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reply", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||
});
|
||||
} else {
|
||||
let (status, payload) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reply_session}/questions/missing-question/reply"
|
||||
),
|
||||
Some(json!({ "answers": [] })),
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "missing question id should error");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reply_missing", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": payload,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
let question_reject_session = format!("question-reject-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &question_reject_session, "bypass").await;
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{question_reject_session}/messages"),
|
||||
Some(json!({ "message": QUESTION_PROMPT })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send question prompt reject");
|
||||
|
||||
let reject_events = poll_events_until_match(
|
||||
&app.app,
|
||||
&question_reject_session,
|
||||
Duration::from_secs(120),
|
||||
|events| find_question_id_and_answers(events).is_some() || should_stop(events),
|
||||
)
|
||||
.await;
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reject_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(normalize_events(&reject_events));
|
||||
});
|
||||
|
||||
if let Some((question_id, _)) = find_question_id_and_answers(&reject_events) {
|
||||
let status = send_status(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reject_session}/questions/{question_id}/reject"
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "reject question");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reject", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot_status(status));
|
||||
});
|
||||
} else {
|
||||
let (status, payload) = send_json(
|
||||
&app.app,
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/v1/sessions/{question_reject_session}/questions/missing-question/reject"
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "missing question id reject should error");
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("question_reject_missing", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(json!({
|
||||
"status": status.as_u16(),
|
||||
"payload": payload,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn http_events_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS");
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
let app = TestApp::new();
|
||||
for config in &configs {
|
||||
run_http_events_snapshot(&app.app, config).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_concurrency_snapshot(app: &Router, config: &TestAgentConfig) {
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
install_agent(app, config.agent).await;
|
||||
|
||||
let session_a = format!("concurrent-a-{}", config.agent.as_str());
|
||||
let session_b = format!("concurrent-b-{}", config.agent.as_str());
|
||||
create_session(app, config.agent, &session_a, "bypass").await;
|
||||
create_session(app, config.agent, &session_b, "bypass").await;
|
||||
|
||||
let app_a = app.clone();
|
||||
let app_b = app.clone();
|
||||
let send_a = send_message(&app_a, &session_a);
|
||||
let send_b = send_message(&app_b, &session_b);
|
||||
tokio::join!(send_a, send_b);
|
||||
|
||||
let app_a = app.clone();
|
||||
let app_b = app.clone();
|
||||
let poll_a = poll_events_until(&app_a, &session_a, Duration::from_secs(120));
|
||||
let poll_b = poll_events_until(&app_b, &session_b, Duration::from_secs(120));
|
||||
let (events_a, events_b) = tokio::join!(poll_a, poll_b);
|
||||
let events_a = truncate_after_first_stop(&events_a);
|
||||
let events_b = truncate_after_first_stop(&events_b);
|
||||
|
||||
assert!(
|
||||
!events_a.is_empty(),
|
||||
"no events collected for concurrent session a {}",
|
||||
config.agent
|
||||
);
|
||||
assert!(
|
||||
!events_b.is_empty(),
|
||||
"no events collected for concurrent session b {}",
|
||||
config.agent
|
||||
);
|
||||
assert!(
|
||||
should_stop(&events_a),
|
||||
"timed out waiting for assistant/error event for concurrent session a {}",
|
||||
config.agent
|
||||
);
|
||||
assert!(
|
||||
should_stop(&events_b),
|
||||
"timed out waiting for assistant/error event for concurrent session b {}",
|
||||
config.agent
|
||||
);
|
||||
|
||||
let snapshot = json!({
|
||||
"session_a": normalize_events(&events_a),
|
||||
"session_b": normalize_events(&events_b),
|
||||
});
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => snapshot_name("concurrency_events", Some(config.agent)),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(snapshot);
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sse_events_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS");
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
let app = TestApp::new();
|
||||
for config in &configs {
|
||||
run_sse_events_snapshot(&app.app, config).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn concurrency_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
let app = TestApp::new();
|
||||
for config in &configs {
|
||||
run_concurrency_snapshot(&app.app, config).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 874
|
||||
expression: snapshot_status(status)
|
||||
---
|
||||
status: 204
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 900
|
||||
expression: normalize_agent_modes(&modes)
|
||||
---
|
||||
modes:
|
||||
- description: true
|
||||
id: build
|
||||
name: Build
|
||||
- description: true
|
||||
id: plan
|
||||
name: Plan
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 881
|
||||
expression: normalize_agent_list(&agents)
|
||||
---
|
||||
agents:
|
||||
- id: amp
|
||||
- id: claude
|
||||
- id: codex
|
||||
- id: opencode
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 918
|
||||
expression: normalize_create_session(&created)
|
||||
---
|
||||
healthy: true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 850
|
||||
expression: normalize_health(&health)
|
||||
---
|
||||
status: ok
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 943
|
||||
expression: snapshot_status(status)
|
||||
---
|
||||
status: 204
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 928
|
||||
expression: normalize_sessions(&sessions)
|
||||
---
|
||||
sessions:
|
||||
- agent: claude
|
||||
agentMode: build
|
||||
agentSessionId: "<redacted>"
|
||||
ended: false
|
||||
eventCount: "<redacted>"
|
||||
model: "<redacted>"
|
||||
permissionMode: bypass
|
||||
sessionId: snapshot-claude
|
||||
variant: "<redacted>"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 978
|
||||
expression: normalize_events(&permission_events)
|
||||
---
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1011
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||
---
|
||||
payload:
|
||||
detail: "invalid request: unknown permission id: missing-permission"
|
||||
status: 400
|
||||
title: Invalid Request
|
||||
type: "urn:sandbox-agent:error:invalid_request"
|
||||
status: 400
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1100
|
||||
expression: normalize_events(&reject_events)
|
||||
---
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1151
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||
---
|
||||
payload:
|
||||
detail: "invalid request: unknown question id: missing-question"
|
||||
status: 400
|
||||
title: Invalid Request
|
||||
type: "urn:sandbox-agent:error:invalid_request"
|
||||
status: 400
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1039
|
||||
expression: normalize_events(&question_events)
|
||||
---
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1072
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||
---
|
||||
payload:
|
||||
detail: "invalid request: unknown question id: missing-question"
|
||||
status: 400
|
||||
title: Invalid Request
|
||||
type: "urn:sandbox-agent:error:invalid_request"
|
||||
status: 400
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 765
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": normalize_health(&payload), })"
|
||||
---
|
||||
payload:
|
||||
status: ok
|
||||
status: 200
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 793
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||
---
|
||||
payload:
|
||||
detail: token invalid
|
||||
details:
|
||||
message: missing or invalid token
|
||||
status: 401
|
||||
title: Token Invalid
|
||||
type: "urn:sandbox-agent:error:token_invalid"
|
||||
status: 401
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 776
|
||||
expression: "json!({ \"status\": status.as_u16(), \"payload\": payload, })"
|
||||
---
|
||||
payload:
|
||||
detail: token invalid
|
||||
details:
|
||||
message: missing or invalid token
|
||||
status: 401
|
||||
title: Token Invalid
|
||||
type: "urn:sandbox-agent:error:token_invalid"
|
||||
status: 401
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 810
|
||||
expression: "json!({\n \"status\": status.as_u16(), \"payload\": normalize_agent_list(&payload),\n})"
|
||||
---
|
||||
payload:
|
||||
agents:
|
||||
- id: amp
|
||||
- id: claude
|
||||
- id: codex
|
||||
- id: opencode
|
||||
status: 200
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 842
|
||||
expression: "json!({\n \"cors\": snapshot_cors(status, &headers), \"payload\":\n normalize_health(&payload),\n})"
|
||||
---
|
||||
cors:
|
||||
access-control-allow-credentials: "true"
|
||||
access-control-allow-origin: "http://example.com"
|
||||
status: 200
|
||||
vary: "origin, access-control-request-method, access-control-request-headers"
|
||||
payload:
|
||||
status: ok
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 818
|
||||
expression: "snapshot_cors(status, &headers)"
|
||||
---
|
||||
access-control-allow-credentials: "true"
|
||||
access-control-allow-headers: "content-type,authorization"
|
||||
access-control-allow-methods: "GET,POST"
|
||||
access-control-allow-origin: "http://example.com"
|
||||
status: 200
|
||||
vary: "origin, access-control-request-method, access-control-request-headers"
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 1232
|
||||
expression: snapshot
|
||||
---
|
||||
session_a:
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
session_b:
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 721
|
||||
expression: normalized
|
||||
---
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/http_sse_snapshots.rs
|
||||
assertion_line: 729
|
||||
expression: normalized
|
||||
---
|
||||
- agent: claude
|
||||
kind: started
|
||||
seq: 1
|
||||
started:
|
||||
message: session.created
|
||||
- agent: claude
|
||||
kind: unknown
|
||||
seq: 2
|
||||
- agent: claude
|
||||
kind: message
|
||||
message:
|
||||
parts:
|
||||
- text: "<redacted>"
|
||||
type: text
|
||||
role: assistant
|
||||
seq: 3
|
||||
Loading…
Add table
Add a link
Reference in a new issue