Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b535abb
test(sidecar): prototype shared unit-test contracts
JulienDarve Sep 15, 2026
9a497e5
test(sidecar): run shared contracts against mocker servers
JulienDarve Sep 16, 2026
48e472e
test(sidecar): establish shared CPU-only testkit
JulienDarve Sep 16, 2026
d486872
test(sidecar): update shared harness to current APIs
JulienDarve Sep 18, 2026
73192d6
test(sidecar): add shared CPU wire conformance coverage
JulienDarve Sep 19, 2026
b623454
test(sidecar): isolate vLLM unit contracts and fix regressions
JulienDarve Sep 19, 2026
10456cb
test(sidecar): verify disabled draft weight updates
JulienDarve Sep 19, 2026
286d6fd
test(sidecar): refresh shared harness and preserve backend contracts
JulienDarve Sep 22, 2026
3556377
test(sidecar): isolate unit coverage on the shared harness
JulienDarve Sep 22, 2026
b3ab163
chore(sidecar): preserve published unit history during restack
JulienDarve Sep 22, 2026
0857c07
test(sidecar): share unit scenarios and add per-test CI lanes
JulienDarve Sep 23, 2026
47ae1fb
test(sidecar): simplify and consolidate unit test layout
JulienDarve Sep 23, 2026
532d299
test(sidecar): fix checkpoint budgets and adapter ownership
JulienDarve Sep 23, 2026
e7d4465
test(sidecar): verify terminal replay fault behavior
JulienDarve Sep 24, 2026
2f6224d
test(sidecar): place unit cases beside production code
JulienDarve Sep 24, 2026
2dc7efd
test(sidecar): simplify local unit test infrastructure
JulienDarve Sep 28, 2026
ef4d745
test(sidecar): use ordinary Rust unit tests
JulienDarve Sep 28, 2026
40c329f
chore(sidecar): merge latest shared test foundation
JulienDarve Sep 28, 2026
48252bb
test(sidecar): add shared CPU integration testkit
JulienDarve Sep 28, 2026
f6f9063
docs(sidecar): include controller unit test in validation command
JulienDarve Sep 28, 2026
8c0056d
test(sidecar): add isolated SGLang unit coverage
JulienDarve Sep 29, 2026
01f1199
docs(sidecar): explain backend-local unit testing
JulienDarve Sep 29, 2026
e8e7af8
chore(sidecar): merge updated testing foundation
JulienDarve Sep 29, 2026
6370289
test(sidecar): omit UUID values from failure output
JulienDarve Sep 29, 2026
791a15a
chore(sidecar): merge updated vLLM unit-test base
JulienDarve Sep 29, 2026
05a8d21
test(sidecar): add isolated units beside production code
JulienDarve Sep 29, 2026
22ca1e1
chore(sidecar): merge rebased vLLM unit-test base [skip ci]
JulienDarve Sep 29, 2026
f326d9a
chore(sidecar): add empty follow-up [skip ci]
JulienDarve Sep 29, 2026
857f465
test(sidecar): consolidate SGLang response coverage [skip ci]
JulienDarve Sep 29, 2026
0879894
chore(sidecar): merge main after vLLM unit tests
JulienDarve Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/sidecar/sglang/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ tonic-build = { workspace = true }

[dev-dependencies]
dynamo-backend-common = { workspace = true, features = ["testing"] }
tracing-subscriber = { workspace = true }
276 changes: 262 additions & 14 deletions lib/sidecar/sglang/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,12 +369,16 @@ pub fn status_to_dynamo(rpc: &str, status: tonic::Status) -> DynamoError {
mod tests {
use std::time::Duration;

use dynamo_backend_common::{BackendError, ErrorType};
use serde_json::json;
use tokio::net::TcpListener;
use tokio::time::Instant;
use tonic::transport::Endpoint;

use super::{client_from_channel, discover, json_u32, json_u64, parse_discovery};
use super::{
client_from_channel, discover, json_u32, json_u64, parse_discovery, rpc_with_deadline,
status_to_dynamo,
};
use crate::proto as pb;

#[test]
Expand All @@ -383,6 +387,25 @@ mod tests {
assert_eq!(json_u64(&value, "a"), Some(16));
assert_eq!(json_u32(&value, "b"), Some(32));
assert_eq!(json_u64(&value, "c"), None);
for value in [json!(u64::MAX), json!(u64::MAX.to_string())] {
let info = json!({"limit": value});
assert_eq!(json_u64(&info, "limit"), Some(u64::MAX));
assert_eq!(json_u32(&info, "limit"), None);
}
for value in [json!(u32::MAX), json!(u32::MAX.to_string())] {
assert_eq!(json_u32(&json!({"limit": value}), "limit"), Some(u32::MAX));
}
for value in [
json!(null),
json!(true),
json!(1.5),
json!("-1"),
json!("1.5"),
json!("18446744073709551616"),
] {
assert_eq!(json_u64(&json!({"limit": value}), "limit"), None);
}
assert_eq!(json_u32(&json!({}), "limit"), None);
}

#[test]
Expand All @@ -404,19 +427,244 @@ mod tests {

#[test]
fn discovery_requires_incremental_streaming() {
let error = parse_discovery(
pb::GetModelInfoResponse {
model_path: "model-repo".to_string(),
json_info: "{}".to_string(),
},
pb::GetServerInfoResponse {
json_info: json!({"incremental_streaming_output": false}).to_string(),
},
Vec::new(),
)
.unwrap_err()
.to_string();
assert!(error.contains("--incremental-streaming-output"), "{error}");
for info in [
json!({}),
json!({"incremental_streaming_output": false}),
json!({"incremental_streaming_output": "true"}),
json!({"incremental_streaming_output": null}),
] {
let error = parse_discovery(
pb::GetModelInfoResponse {
model_path: "model-repo".to_string(),
json_info: "{}".to_string(),
},
pb::GetServerInfoResponse {
json_info: info.to_string(),
},
Vec::new(),
)
.unwrap_err();
assert_eq!(
error.error_type(),
ErrorType::Backend(BackendError::InvalidArgument)
);
assert!(
error.to_string().contains("--incremental-streaming-output"),
"{error}"
);
}
}

#[test]
fn discovery_rejects_malformed_json_and_non_object_metadata() {
for label in ["GetModelInfo.json_info", "GetServerInfo.json_info"] {
for raw in ["{", "null", "[]", "1", "\"metadata\""] {
let error = parse_discovery(
pb::GetModelInfoResponse {
model_path: "model".into(),
json_info: if label.starts_with("GetModelInfo") {
raw
} else {
"{}"
}
.into(),
},
pb::GetServerInfoResponse {
json_info: if label.starts_with("GetServerInfo") {
raw.into()
} else {
json!({"incremental_streaming_output": true}).to_string()
},
},
vec![],
)
.unwrap_err();
assert_eq!(
error.error_type(),
ErrorType::Backend(BackendError::Unknown)
);
assert!(error.to_string().contains(label), "{raw}: {error}");
}
}
}

#[test]
fn discovery_resolves_model_path_and_tokenizer_fallbacks() {
for (native_path, json_path, expected) in [
("native-model", "json-model", Some("native-model")),
(" ", "json-model", Some("json-model")),
("", " ", None),
] {
for tokenizer in [json!(null), json!(""), json!(" "), json!(7)] {
let result = parse_discovery(
pb::GetModelInfoResponse {
model_path: native_path.into(),
json_info: json!({"model_path": json_path, "tokenizer_path": tokenizer})
.to_string(),
},
pb::GetServerInfoResponse {
json_info: json!({"incremental_streaming_output": true}).to_string(),
},
vec![],
);
if let Some(expected) = expected {
let info = result.unwrap();
assert_eq!(info.model_path, expected);
assert_eq!(info.tokenizer_path, expected);
assert_eq!(info.served_model_name, None);
assert_eq!(info.max_model_len, None);
} else {
let error = result.unwrap_err();
assert_eq!(
error.error_type(),
ErrorType::Backend(BackendError::Unknown)
);
assert!(error.to_string().contains("empty model_path"));
}
}
}
}

#[test]
fn discovery_prefers_matching_model_and_server_alias() {
for (id, root, fallback_name) in [
("card-alias", "model", Some("card-alias")),
("model", "different-root", None),
] {
for server_name in [None, Some("server-alias")] {
let model_info = json!({"tokenizer_path": "tokenizer", "custom": [1, 2]});
let server_info = json!({
"incremental_streaming_output": true,
"served_model_name": server_name,
"context_length": 1024,
});
let info = parse_discovery(
pb::GetModelInfoResponse {
model_path: "model".into(),
json_info: model_info.to_string(),
},
pb::GetServerInfoResponse {
json_info: server_info.to_string(),
},
vec![
pb::ModelCard {
id: "unrelated".into(),
max_model_len: Some(512),
..Default::default()
},
pb::ModelCard {
id: id.into(),
root: root.into(),
max_model_len: Some(4096),
..Default::default()
},
],
)
.unwrap();
assert_eq!(
info.served_model_name.as_deref(),
server_name.or(fallback_name)
);
assert_eq!(info.max_model_len, Some(4096));
assert_eq!(info.model_info, model_info);
assert_eq!(info.server_info, server_info);
}
}
}

#[test]
fn discovery_falls_back_to_first_card_and_valid_server_limits() {
for (card_limit, context, input_limit, expected) in [
(Some(2048), json!(4096), json!(8192), Some(2048)),
(Some(-1), json!("4096"), json!(8192), Some(4096)),
(
None,
json!(u64::from(u32::MAX) + 1),
json!("8192"),
Some(8192),
),
(None, json!(null), json!(-1), None),
] {
let info = parse_discovery(
pb::GetModelInfoResponse {
model_path: "model".into(),
json_info: "{}".into(),
},
pb::GetServerInfoResponse {
json_info: json!({
"incremental_streaming_output": true,
"served_model_name": "",
"context_length": context,
"max_req_input_len": input_limit,
})
.to_string(),
},
vec![pb::ModelCard {
id: "first-alias".into(),
max_model_len: card_limit,
..Default::default()
}],
)
.unwrap();
assert_eq!(info.served_model_name.as_deref(), Some("first-alias"));
assert_eq!(info.max_model_len, expected);
}
}

#[test]
fn rpc_status_mapping_preserves_error_kind_and_context() {
for (code, kind) in [
(tonic::Code::InvalidArgument, BackendError::InvalidArgument),
(tonic::Code::NotFound, BackendError::InvalidArgument),
(tonic::Code::OutOfRange, BackendError::InvalidArgument),
(tonic::Code::Unavailable, BackendError::CannotConnect),
(tonic::Code::Cancelled, BackendError::Cancelled),
(
tonic::Code::DeadlineExceeded,
BackendError::ConnectionTimeout,
),
(tonic::Code::Internal, BackendError::Unknown),
] {
let error =
status_to_dynamo("GetModelInfo", tonic::Status::new(code, "native failure"));
assert_eq!(error.error_type(), ErrorType::Backend(kind));
assert!(error.to_string().contains("GetModelInfo: native failure"));
assert!(error.to_string().contains(&format!("{code:?}")));
}
}

#[tokio::test]
async fn rpc_deadline_preserves_success_status_and_timeout() {
let deadline = Instant::now() + Duration::from_secs(60);
assert_eq!(
rpc_with_deadline("HealthCheck", deadline, async { Ok(17) })
.await
.unwrap(),
17
);
let error = rpc_with_deadline::<(), _>("HealthCheck", deadline, async {
Err(tonic::Status::unavailable("offline"))
})
.await
.unwrap_err();
assert_eq!(
error.error_type(),
ErrorType::Backend(BackendError::CannotConnect)
);

let error =
rpc_with_deadline::<(), _>("HealthCheck", Instant::now(), std::future::pending())
.await
.unwrap_err();
assert_eq!(
error.error_type(),
ErrorType::Backend(BackendError::ConnectionTimeout)
);
assert!(
error
.to_string()
.contains("HealthCheck exceeded the configured deadline")
);
}

#[tokio::test]
Expand Down
Loading
Loading