use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use cf_integration::infrastructure::compose::{ComposeProject, validate_integration_contract};
fn workspace_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
}
use serde_json::json;
const EXPECTED_IMAGE: &str = "ghcr.io/ibm/cfex-mcp-fast-time-server:latest";
fn valid_config() -> serde_json::Value {
json!({
"services": {
"fast_time_server": {
"image": EXPECTED_IMAGE,
"labels": {"name": "cf-fast-time-server"}
},
"register_fast_time": {
"labels": {"name": "cf-register-fast-time"},
"command": [
"wait for http://fast_time_server:9080/health",
"register http://fast_time_server:9080/mcp"
]
}
}
})
}
fn messages(config: &serde_json::Value) -> Vec<String> {
validate_integration_contract(config, EXPECTED_IMAGE)
.into_iter()
.map(|violation| violation.to_string())
.collect()
}
fn run_fixture_patch(source: &str) -> (std::process::ExitStatus, String) {
let directory = tempfile::tempdir().expect("create patch test directory");
let target = directory.path().join("everything-server.ts");
fs::write(&target, source).expect("write patch test input");
let script = workspace_root().join("docker/patch-mcp-conformance-hosts.mjs");
let output = Command::new("node")
.arg(script)
.arg(&target)
.output()
.expect("run conformance host patch");
let contents = fs::read_to_string(target).expect("read patch test output");
(output.status, contents)
}
#[test]
fn dataplane_compose_files_are_in_override_order() {
let repository_root = PathBuf::from("repo");
let checkout = PathBuf::from("checkout");
let project =
ComposeProject::dataplane(&repository_root, &checkout, OsString::from("cf"), false);
let expected_files = [
checkout.join("docker-compose.yml"),
repository_root
.join("docker")
.join("docker-compose.cf-controlplane-build-labels.yaml"),
repository_root
.join("docker")
.join("docker-compose.cf-dataplane.yaml"),
repository_root
.join("docker")
.join("docker-compose.cf-integration.yaml"),
repository_root
.join("docker")
.join("docker-compose.cf-dataplane-config.yaml"),
];
assert_eq!(project.files(), expected_files);
assert!(project.profiles().is_empty());
assert_eq!(
project.command(["config", "--format", "json"]).arguments(),
[
OsString::from("compose"),
OsString::from("-p"),
OsString::from("cf"),
OsString::from("-f"),
expected_files[0].as_os_str().to_owned(),
OsString::from("-f"),
expected_files[1].as_os_str().to_owned(),
OsString::from("-f"),
expected_files[2].as_os_str().to_owned(),
OsString::from("-f"),
expected_files[3].as_os_str().to_owned(),
OsString::from("-f"),
expected_files[4].as_os_str().to_owned(),
OsString::from("config"),
OsString::from("--format"),
OsString::from("json"),
]
);
}
#[test]
fn conformance_runtime_matches_the_python_builtin_image_contract() {
let root = workspace_root();
let compose =
fs::read_to_string(root.join("docker/docker-compose.cf-conformance-runtime.yaml"))
.expect("read conformance runtime overlay");
let compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse conformance runtime overlay");
let gateway = &compose["services"]["gateway"];
assert_eq!(gateway["environment"]["GUNICORN_WORKERS"], "1");
assert_eq!(gateway["environment"]["RATE_LIMITING_ENABLED"], "false");
assert_eq!(gateway["environment"]["RUST_MCP_MODE"], "off");
for service in ["gateway", "migration"] {
assert_eq!(
compose["services"][service]["build"]["args"]["ENABLE_RUST"],
"false"
);
assert_eq!(
compose["services"][service]["build"]["args"]["ENABLE_RUST_MCP_RMCP"],
"false"
);
}
let project = ComposeProject::controlplane(
Path::new("/repo"),
Path::new("/checkout"),
OsString::from("cf"),
false,
)
.with_conformance_overlay(Path::new("/repo"))
.with_conformance_runtime(Path::new("/repo"));
assert_eq!(
project.files().last(),
Some(&PathBuf::from(
"/repo/docker/docker-compose.cf-conformance-runtime.yaml"
))
);
}
#[test]
fn shared_metadata_overlay_clears_obsolete_fast_time_arguments() {
let compose = fs::read_to_string(
workspace_root().join("docker/docker-compose.cf-controlplane-build-labels.yaml"),
)
.expect("read shared control-plane metadata overlay");
let overlay: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse shared control-plane metadata overlay");
assert_eq!(
overlay["services"]["fast_time_server"]["command"],
yaml_serde::Value::Sequence(Vec::new())
);
let gateway_environment = overlay["services"]["gateway"]["environment"]
.as_mapping()
.expect("shared gateway environment must be a mapping");
for key in [
"PLATFORM_ADMIN_EMAIL",
"PLATFORM_ADMIN_PASSWORD",
"PASSWORD_CHANGE_ENFORCEMENT_ENABLED",
"ADMIN_REQUIRE_PASSWORD_CHANGE_ON_BOOTSTRAP",
"REQUIRE_PASSWORD_CHANGE_FOR_DEFAULT_PASSWORD",
] {
assert!(
gateway_environment.contains_key(yaml_serde::Value::String(key.to_owned())),
"shared gateway environment must define {key}"
);
}
}
#[test]
fn compose_overlays_assign_short_container_display_names() {
let shared = fs::read_to_string(
workspace_root().join("docker/docker-compose.cf-controlplane-build-labels.yaml"),
)
.expect("read shared control-plane metadata overlay");
let shared: yaml_serde::Value =
yaml_serde::from_str(&shared).expect("parse shared control-plane metadata overlay");
let dataplane =
fs::read_to_string(workspace_root().join("docker/docker-compose.cf-dataplane.yaml"))
.expect("read dataplane Compose overlay");
let dataplane: yaml_serde::Value =
yaml_serde::from_str(&dataplane).expect("parse dataplane Compose overlay");
let conformance =
fs::read_to_string(workspace_root().join("docker/docker-compose.cf-conformance.yaml"))
.expect("read conformance Compose overlay");
let conformance: yaml_serde::Value =
yaml_serde::from_str(&conformance).expect("parse conformance Compose overlay");
let fixture = fs::read_to_string(
workspace_root().join("docker/docker-compose.cf-conformance-fixture.yaml"),
)
.expect("read standalone conformance fixture Compose file");
let fixture: yaml_serde::Value =
yaml_serde::from_str(&fixture).expect("parse standalone conformance fixture Compose file");
let observability =
fs::read_to_string(workspace_root().join("docker/docker-compose.cf-telemetry.yaml"))
.expect("read observability Compose overlay");
let observability: yaml_serde::Value =
yaml_serde::from_str(&observability).expect("parse observability Compose overlay");
for (service, expected_name) in [
("gateway", "cf-controlplane"),
("migration", "cf-migration"),
("register_fast_time", "cf-register-fast-time"),
("fast_time_server", "cf-fast-time-server"),
("nginx", "cf-nginx"),
("postgres", "cf-postgres"),
("pgbouncer", "cf-pgbouncer"),
("redis", "cf-redis"),
("locust", "cf-locust"),
("locust_worker", "cf-locust-worker"),
("locust_token", "cf-locust-token"),
("a2a_echo_agent", "cf-a2a-echo-agent"),
("a2a_echo_agent_v0_3_0", "cf-a2a-echo-agent-v0-3-0"),
("register_a2a_echo", "cf-register-a2a-echo"),
("mcp_inspector", "cf-mcp-inspector"),
("keycloak", "cf-keycloak"),
] {
assert_eq!(
shared["services"][service]["labels"]["name"].as_str(),
Some(expected_name),
"{service} must expose a concise container display name"
);
}
assert_eq!(
dataplane["services"]["dataplane"]["labels"]["name"].as_str(),
Some("cf-dataplane")
);
assert_eq!(
fixture["services"]["mcp_conformance_server"]["labels"]["name"].as_str(),
Some("cf-conformance-server")
);
assert_eq!(
conformance["services"]["mcp_conformance_proxy"]["labels"]["name"].as_str(),
Some("cf-conformance-proxy")
);
assert_eq!(
observability["services"]["clickstack"]["labels"]["name"].as_str(),
Some("cf-clickstack")
);
}
#[test]
fn dataplane_overlays_track_the_current_image_build_and_environment_contract() {
let root = workspace_root();
let compose = fs::read_to_string(root.join("docker/docker-compose.cf-dataplane.yaml"))
.expect("read dataplane Compose overlay");
let compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse dataplane Compose overlay");
let environment = compose["services"]["dataplane"]["environment"]
.as_mapping()
.expect("dataplane environment must be a mapping");
assert!(compose["services"]["gateway"]["volumes"].is_null());
for key in [
"CONTEXTFORGE_DATA_PLANE_ADDRESS",
"CONTEXTFORGE_DATA_PLANE_REDIS_HOSTNAME",
"CONTEXTFORGE_DATA_PLANE_REDIS_PORT",
"CONTEXTFORGE_DATA_PLANE_REDIS_CONNECTION_MODE",
"CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET",
"CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY",
"CONTEXTFORGE_DATA_PLANE_UPSTREAM_CONNECTION_MODE",
"CONTEXTFORGE_DATA_PLANE_USER_CONFIG_CACHE_EXPIRY_SECONDS",
"CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS",
"CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_ORIGINS",
] {
assert!(
environment.contains_key(yaml_serde::Value::String(key.to_owned())),
"dataplane environment must define {key}"
);
}
assert_eq!(
environment[yaml_serde::Value::String(
"CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY".to_owned()
)]
.as_str(),
Some("/dev/null"),
"the unused local-bootstrap signing key must not add a real private key to the harness"
);
assert!(
environment
[yaml_serde::Value::String("CONTEXTFORGE_GATEWAY_RS_MCP_ALLOWED_HOSTS".to_owned())]
.as_str()
.expect("MCP Host allowlist must be text")
.contains(",nginx}"),
"the default MCP Host allowlist must accept containerized Locust through nginx"
);
assert_eq!(
compose["services"]["dataplane"]["extra_hosts"][0].as_str(),
Some("host.docker.internal:host-gateway"),
"client conformance must let the dataplane reach the official scenario server"
);
assert_eq!(
compose["services"]["dataplane"]["pull_policy"].as_str(),
Some("${CF_DATAPLANE_PULL_POLICY:-always}")
);
for obsolete in [
"CONTEXTFORGE_GATEWAY_RS_ADDRESS",
"CONTEXTFORGE_GATEWAY_RS_REDIS_HOSTNAME",
"CONTEXTFORGE_GATEWAY_RS_TOKEN_SECRET",
"CONTEXTFORGE_GATEWAY_RS_UPSTREAM_CONNECTION_MODE",
"CONTEXTFORGE_GATEWAY_RS_USER_CONFIG_CACHE_EXPIRY_SECONDS",
] {
assert!(
!environment.contains_key(yaml_serde::Value::String(obsolete.to_owned())),
"obsolete dataplane environment key must be absent: {obsolete}"
);
}
let build = fs::read_to_string(root.join("docker/docker-compose.cf-dataplane-build.yaml"))
.expect("read dataplane build overlay");
let build: yaml_serde::Value =
yaml_serde::from_str(&build).expect("parse dataplane build overlay");
assert_eq!(
build["services"]["dataplane"]["build"]["dockerfile"].as_str(),
Some("docker/Dockerfile")
);
}
#[test]
fn controlplane_image_consumers_share_the_explicit_pull_policy() {
let root = workspace_root();
let compose =
fs::read_to_string(root.join("docker/docker-compose.cf-controlplane-build-labels.yaml"))
.expect("read controlplane metadata overlay");
let compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse controlplane metadata overlay");
for service in ["gateway", "migration", "register_fast_time"] {
assert_eq!(
compose["services"][service]["pull_policy"].as_str(),
Some("${CF_CONTROLPLANE_PULL_POLICY:-always}"),
"{service} must honor the controlplane image pull policy"
);
}
}
#[test]
fn source_dataplane_adds_build_overlay_last() {
let project = ComposeProject::dataplane(
Path::new("/repo"),
Path::new("/checkout"),
OsString::from("project"),
true,
);
assert_eq!(
project.files().last().map(PathBuf::as_path),
Some(Path::new(
"/repo/docker/docker-compose.cf-dataplane-build.yaml"
))
);
}
#[test]
fn standalone_dataplane_has_no_controlplane_compose_inputs() {
let project =
ComposeProject::standalone_dataplane(Path::new("/repo"), OsString::from("project"), true);
assert_eq!(
project.files(),
[
PathBuf::from("/repo/docker/docker-compose.cf-dataplane-standalone.yaml"),
PathBuf::from("/repo/docker/docker-compose.cf-dataplane-config.yaml"),
PathBuf::from("/repo/docker/docker-compose.cf-dataplane-build.yaml"),
]
);
assert!(
project
.files()
.iter()
.all(|file| !file.to_string_lossy().contains("controlplane"))
);
}
#[test]
fn both_external_projects_provide_the_client_conformance_config_writer() {
let root = workspace_root();
for project in [
ComposeProject::dataplane(
root,
Path::new("/checkout"),
OsString::from("normal"),
false,
),
ComposeProject::standalone_dataplane(root, OsString::from("standalone"), false),
] {
let helpers: Vec<_> = project
.files()
.iter()
.filter_map(|file| {
let source = fs::read_to_string(file).ok()?;
let compose: yaml_serde::Value =
yaml_serde::from_str(&source).expect("Compose YAML");
let service = &compose["services"]["config_writer"];
(!service.is_null()).then(|| service.clone())
})
.collect();
assert_eq!(
helpers.len(),
1,
"each external project needs exactly one writer"
);
assert_eq!(helpers[0]["profiles"][0].as_str(), Some("helpers"));
assert_eq!(helpers[0]["networks"][0].as_str(), Some("mcpnet"));
assert_eq!(
helpers[0]["entrypoint"][1].as_str(),
Some("/opt/contextforge-integration/write_dataplane_config.mjs")
);
}
}
#[test]
fn standalone_dataplane_owns_ephemeral_jwks_auth_and_mock_helpers() {
let root = workspace_root();
let compose =
fs::read_to_string(root.join("docker/docker-compose.cf-dataplane-standalone.yaml"))
.expect("read standalone dataplane Compose file");
let compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse standalone dataplane Compose file");
let services = compose["services"]
.as_mapping()
.expect("standalone services must be a mapping");
assert_eq!(services.len(), 5);
assert!(compose["services"]["gateway"].is_null());
assert_eq!(
compose["services"]["auth_keygen"]["network_mode"].as_str(),
Some("none")
);
assert_eq!(
compose["services"]["dataplane"]["environment"]["CONTEXTFORGE_DATA_PLANE_JWKS_URL"]
.as_str(),
Some("http://127.0.0.1:4445/contextforge-rs/admin/.well-known/jwks.json")
);
assert_eq!(
compose["services"]["dataplane"]["command"][1].as_str(),
Some("/keys/jwt.key")
);
assert!(
compose["services"]["dataplane"]["environment"]["CONTEXTFORGE_DATA_PLANE_TOKEN_SECRET"]
.is_null()
);
assert!(
compose["services"]["dataplane"]["environment"]
["CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY"]
.is_null()
);
assert_eq!(
compose["services"]["nginx"]["healthcheck"]["test"][1].as_str(),
Some("wget -q -O - http://127.0.0.1/health >/dev/null")
);
assert_eq!(
compose["services"]["locust"]["profiles"][0].as_str(),
Some("performance")
);
assert_eq!(
compose["services"]["locust"]["image"].as_str(),
Some("locustio/locust:2.46.2")
);
assert!(compose["services"]["locust"]["environment"]["JWT_SECRET_KEY"].is_null());
}
#[test]
fn conformance_fixture_is_an_explicit_overlay_and_profile() {
let default_project = ComposeProject::dataplane(
Path::new("/repo"),
Path::new("/checkout"),
OsString::from("project"),
false,
);
assert!(default_project.profiles().is_empty());
assert!(
!default_project
.files()
.iter()
.any(|file| file.ends_with("docker-compose.cf-conformance-fixture.yaml"))
);
assert!(
!default_project
.files()
.iter()
.any(|file| file.ends_with("docker-compose.cf-conformance.yaml"))
);
let overlay = default_project
.clone()
.with_profiles(["testing"])
.with_conformance_overlay(Path::new("/repo"));
assert_eq!(overlay.profiles(), ["testing"]);
assert_eq!(
&overlay.files()[..default_project.files().len()],
default_project.files()
);
assert_eq!(
&overlay.files()[default_project.files().len()..],
[
PathBuf::from("/repo/docker/docker-compose.cf-conformance-fixture.yaml"),
PathBuf::from("/repo/docker/docker-compose.cf-conformance.yaml"),
]
);
let conformance = overlay.with_conformance_fixture(Path::new("/repo"));
assert_eq!(conformance.profiles(), ["testing", "conformance"]);
let deduplicated = conformance.with_conformance_fixture(Path::new("/repo"));
assert_eq!(deduplicated.profiles(), ["testing", "conformance"]);
assert_eq!(
deduplicated
.files()
.iter()
.filter(|file| file.ends_with("docker-compose.cf-conformance.yaml"))
.count(),
1
);
assert_eq!(
deduplicated
.files()
.iter()
.filter(|file| file.ends_with("docker-compose.cf-conformance-fixture.yaml"))
.count(),
1
);
let standalone =
ComposeProject::conformance_fixture(Path::new("/repo"), OsString::from("fixture"));
assert_eq!(
standalone.files(),
[PathBuf::from(
"/repo/docker/docker-compose.cf-conformance-fixture.yaml"
)]
);
assert_eq!(standalone.profiles(), ["conformance"]);
let observability =
ComposeProject::observability(Path::new("/repo"), OsString::from("observability"));
assert_eq!(
observability.files(),
[PathBuf::from(
"/repo/docker/docker-compose.cf-telemetry.yaml"
)]
);
assert!(observability.profiles().is_empty());
}
#[test]
fn observability_overlays_are_explicit_and_include_the_selected_lane() {
let root = Path::new("/repo");
let checkout = Path::new("/checkout");
let built_in = ComposeProject::controlplane(root, checkout, OsString::from("built-in"), false)
.with_observability(root, false);
let external = ComposeProject::dataplane(root, checkout, OsString::from("external"), false)
.with_observability(root, true);
assert!(built_in.files().ends_with(&[PathBuf::from(
"/repo/docker/docker-compose.cf-controlplane-observability.yaml"
)]));
assert!(external.files().ends_with(&[
PathBuf::from("/repo/docker/docker-compose.cf-controlplane-observability.yaml"),
PathBuf::from("/repo/docker/docker-compose.cf-dataplane-observability.yaml"),
]));
}
#[test]
fn observability_is_ephemeral_and_exports_both_routed_services() {
let root = workspace_root();
let compose = fs::read_to_string(root.join("docker/docker-compose.cf-telemetry.yaml"))
.expect("read observability Compose overlay");
let compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse observability Compose overlay");
let services = compose["services"]
.as_mapping()
.expect("telemetry services must be a mapping");
let clickstack = &compose["services"]["clickstack"];
assert_eq!(services.len(), 1);
assert!(
clickstack["image"]
.as_str()
.expect("ClickStack image must be text")
.contains("clickstack-local:2.37.0@sha256:")
);
assert_eq!(
clickstack["ports"][0].as_str(),
Some("127.0.0.1:${CF_OBSERVABILITY_UI_PORT:-3000}:8080")
);
assert_eq!(
clickstack["ports"][1].as_str(),
Some("127.0.0.1:${CF_OBSERVABILITY_LOG_PORT:-24224}:24224")
);
assert_eq!(
clickstack["tmpfs"]
.as_sequence()
.expect("ClickStack storage must use tmpfs")
.iter()
.filter_map(yaml_serde::Value::as_str)
.collect::<Vec<_>>(),
["/var/lib/clickhouse", "/var/log/clickhouse-server"]
);
assert_eq!(
clickstack["environment"]["CUSTOM_OTELCOL_CONFIG_FILE"].as_str(),
Some("/etc/otelcol-contrib/custom.config.yaml")
);
assert_eq!(
clickstack["volumes"][0].as_str(),
Some(
"${CF_INTEGRATION_ROOT:?Set CF_INTEGRATION_ROOT to the integration harness root}/docker/clickstack/collector.yaml:/etc/otelcol-contrib/custom.config.yaml:ro"
)
);
let collector = fs::read_to_string(root.join("docker/clickstack/collector.yaml"))
.expect("read ClickStack collector extension");
let collector: yaml_serde::Value =
yaml_serde::from_str(&collector).expect("parse ClickStack collector extension");
assert_eq!(
collector["receivers"]["fluent_forward/docker"]["endpoint"].as_str(),
Some("0.0.0.0:24224")
);
assert_eq!(
collector["service"]["pipelines"]["logs/docker"]["receivers"][0].as_str(),
Some("fluent_forward/docker")
);
assert_eq!(
collector["service"]["pipelines"]
.as_mapping()
.expect("collector pipelines must be a mapping")
.len(),
1,
"the extension must not duplicate ClickStack's built-in OTLP pipelines"
);
assert_eq!(
collector["service"]["pipelines"]["logs/docker"]["exporters"][0].as_str(),
Some("clickhouse")
);
let gateway =
fs::read_to_string(root.join("docker/docker-compose.cf-controlplane-observability.yaml"))
.expect("read gateway observability overlay");
let gateway: yaml_serde::Value =
yaml_serde::from_str(&gateway).expect("parse gateway observability overlay");
assert_eq!(
gateway["services"]["gateway"]["environment"]["OTEL_EXPORTER_OTLP_ENDPOINT"].as_str(),
Some("http://clickstack:4317")
);
assert_eq!(
gateway["services"]["gateway"]["logging"]["driver"].as_str(),
Some("fluentd")
);
assert_eq!(
gateway["networks"]["observability"]["external"].as_bool(),
Some(true)
);
let dataplane =
fs::read_to_string(root.join("docker/docker-compose.cf-dataplane-observability.yaml"))
.expect("read dataplane observability overlay");
let dataplane: yaml_serde::Value =
yaml_serde::from_str(&dataplane).expect("parse dataplane observability overlay");
assert_eq!(
dataplane["services"]["dataplane"]["environment"]
["CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_ENDPOINT"]
.as_str(),
Some("http://clickstack:4318/v1/traces")
);
assert_eq!(
dataplane["services"]["dataplane"]["environment"]
["CONTEXTFORGE_DATA_PLANE_ENABLE_OTEL_METRICS"]
.as_str(),
Some("true")
);
assert_eq!(
dataplane["services"]["dataplane"]["environment"]
["CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]
.as_str(),
Some("http://clickstack:4318/v1/metrics")
);
assert_eq!(
dataplane["services"]["dataplane"]["environment"]
["CONTEXTFORGE_DATA_PLANE_OTEL_EXPORTER_OTLP_PROTOCOL"]
.as_str(),
Some("http-protobuf")
);
assert_eq!(
dataplane["services"]["dataplane"]["logging"]["driver"].as_str(),
Some("fluentd")
);
assert_eq!(
dataplane["networks"]["observability"]["external"].as_bool(),
Some(true)
);
}
#[test]
fn conformance_container_inputs_pin_the_runner_revision_and_protocol_fixture() {
let root = workspace_root();
let dockerfile = fs::read_to_string(root.join("docker/mcp-conformance-server.Dockerfile"))
.expect("read conformance Dockerfile");
let patch = fs::read_to_string(root.join("docker/patch-mcp-conformance-hosts.mjs"))
.expect("read host patch script");
let compose =
fs::read_to_string(root.join("docker/docker-compose.cf-conformance-fixture.yaml"))
.expect("read standalone conformance fixture Compose file");
let routed_compose = fs::read_to_string(root.join("docker/docker-compose.cf-conformance.yaml"))
.expect("read routed conformance Compose overlay");
let controlplane_compose =
fs::read_to_string(root.join("docker/docker-compose.cf-conformance-controlplane.yaml"))
.expect("read control-plane conformance Compose overlay");
assert!(dockerfile.contains("FROM node:22-bookworm-slim"));
assert!(
dockerfile
.contains("ARG MCP_CONFORMANCE_REVISION=c321dd32035556e6769d3724a8ee97d87c3faaac")
);
assert!(dockerfile.contains(
"git clone https://github.com/modelcontextprotocol/conformance.git mcp-conformance"
));
assert!(
dockerfile
.contains("git -C mcp-conformance checkout --detach \"${MCP_CONFORMANCE_REVISION}\"")
);
assert!(dockerfile.contains("WORKDIR /opt/mcp-conformance/examples/servers/typescript"));
assert!(dockerfile.contains("npm ci"));
assert!(
dockerfile
.contains("node /usr/local/bin/patch-mcp-conformance-hosts.mjs everything-server.ts")
);
assert!(dockerfile.contains(
"git diff --exit-code -- . ':(exclude)examples/servers/typescript/everything-server.ts'"
));
assert!(
dockerfile.contains("git diff --check -- examples/servers/typescript/everything-server.ts")
);
assert!(dockerfile.contains(
"grep -Fxc \"const app = createMcpExpressApp({ allowedHosts: ['mcp_conformance_server', 'localhost', '127.0.0.1', '::1'] });\" examples/servers/typescript/everything-server.ts"
));
assert!(dockerfile.contains("ENV PORT=3000"));
assert!(dockerfile.contains("EXPOSE 3000"));
assert!(dockerfile.contains("CMD [\"npm\", \"start\"]"));
let old = "const app = createMcpExpressApp();";
let replacement = "const app = createMcpExpressApp({ allowedHosts: ['mcp_conformance_server', 'localhost', '127.0.0.1', '::1'] });";
assert!(patch.contains(old));
assert!(patch.contains(replacement));
assert!(patch.contains("replacementCount !== 1"));
assert!(patch.contains("process.argv[2]"));
assert!(patch.contains("MCP_CONFORMANCE_SERVER_ERA"));
assert!(patch.contains("isModernEraRequest"));
assert!(patch.contains("UnsupportedProtocolVersionError"));
let actual_compose: yaml_serde::Value =
yaml_serde::from_str(&compose).expect("parse conformance Compose overlay");
let expected_compose: yaml_serde::Value = yaml_serde::from_str(
r#"
services:
mcp_conformance_server:
profiles: ["conformance"]
image: cf-integration/mcp-conformance-server:0.2.0-alpha.11
labels:
name: cf-conformance-server
build:
context: ${CF_INTEGRATION_ROOT:?Set CF_INTEGRATION_ROOT to the integration harness root}
dockerfile: docker/mcp-conformance-server.Dockerfile
restart: "no"
environment:
PORT: "3000"
MCP_CONFORMANCE_SERVER_ERA: ${CF_CONFORMANCE_SERVER_ERA:?Set CF_CONFORMANCE_SERVER_ERA to legacy, modern, or dual}
ports:
- "127.0.0.1:${CF_CONFORMANCE_PORT:-0}:3000"
healthcheck:
test:
- CMD
- node
- -e
- fetch('http://127.0.0.1:3000/mcp').then(response => { if (response.status !== 400) process.exit(1); }).catch(() => process.exit(1))
interval: 2s
timeout: 2s
retries: 30
start_period: 2s
"#,
)
.expect("parse expected conformance Compose contract");
assert_eq!(actual_compose, expected_compose);
let actual_routed_compose: yaml_serde::Value =
yaml_serde::from_str(&routed_compose).expect("parse routed conformance Compose overlay");
let expected_routed_compose: yaml_serde::Value = yaml_serde::from_str(
r#"
services:
mcp_conformance_server:
networks:
- mcpnet
mcp_conformance_proxy:
profiles: ["conformance"]
image: nginx:1.30.4-alpine3.24
labels:
name: cf-conformance-proxy
restart: "no"
volumes:
- ${CF_INTEGRATION_ROOT:?Set CF_INTEGRATION_ROOT to the integration harness root}/docker/nginx.cf-conformance-proxy.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- mcpnet
depends_on:
mcp_conformance_server:
condition: service_healthy
"#,
)
.expect("parse expected routed conformance Compose overlay");
assert_eq!(actual_routed_compose, expected_routed_compose);
let actual_controlplane_compose: yaml_serde::Value =
yaml_serde::from_str(&controlplane_compose)
.expect("parse control-plane conformance Compose overlay");
let expected_controlplane_compose: yaml_serde::Value = yaml_serde::from_str(
r#"
services:
gateway:
environment:
GATEWAY_TOOL_NAME_SEPARATOR: "_"
"#,
)
.expect("parse expected control-plane conformance Compose overlay");
assert_eq!(actual_controlplane_compose, expected_controlplane_compose);
let proxy = fs::read_to_string(root.join("docker/nginx.cf-conformance-proxy.conf"))
.expect("read conformance proxy config");
assert!(proxy.contains("proxy_pass http://mcp_conformance_server:3000;"));
assert!(proxy.contains("proxy_set_header Host localhost:3000;"));
}
#[test]
fn conformance_fixture_patch_is_fail_closed_and_adds_server_era_routing() {
let old = "const app = createMcpExpressApp();";
let replacement = "const app = createMcpExpressApp({ allowedHosts: ['mcp_conformance_server', 'localhost', '127.0.0.1', '::1'] });";
let versions = r#"const LEGACY_SESSION_PROTOCOL_VERSIONS = [
'2024-11-05',
'2025-03-26',
'2025-06-18',
'2025-11-25'
];"#;
let classification = r#" const isLegacySessionEraRequest =
meta === undefined &&
reqVersion !== undefined &&
LEGACY_SESSION_PROTOCOL_VERSIONS.includes(reqVersion);
if (!sessionId && (reqVersion || meta) && !isLegacySessionEraRequest) {"#;
let source = format!("before\n{old}\n{versions}\n{classification}\nafter\n");
let (status, patched) = run_fixture_patch(&source);
assert!(status.success());
assert!(patched.contains(replacement));
assert!(patched.contains("process.env.MCP_CONFORMANCE_SERVER_ERA;"));
assert!(!patched.contains("?? 'dual'"));
assert!(patched.contains("CONFORMANCE_SERVER_ERA === 'legacy' && isModernEraRequest"));
assert!(patched.contains("CONFORMANCE_SERVER_ERA === 'modern'"));
assert!(!patched.contains(old));
for missing in [old, versions, classification] {
let unchanged = source.replacen(missing, "missing patch target", 1);
let (status, contents) = run_fixture_patch(&unchanged);
assert!(!status.success());
assert_eq!(contents, unchanged);
}
}
#[test]
fn controlplane_default_excludes_optional_profiles_and_sso_is_explicit() {
let without_sso = ComposeProject::controlplane(
Path::new("/repo"),
Path::new("/checkout"),
OsString::from("cp"),
false,
);
let with_sso = ComposeProject::controlplane(
Path::new("/repo"),
Path::new("/checkout"),
OsString::from("cp"),
true,
);
assert!(without_sso.profiles().is_empty());
assert_eq!(with_sso.profiles(), ["sso"]);
assert_eq!(
without_sso.files(),
[
PathBuf::from("/checkout/docker-compose.yml"),
PathBuf::from("/repo/docker/docker-compose.cf-controlplane-build-labels.yaml"),
]
);
}
#[test]
fn valid_contract_has_no_violations() {
assert!(messages(&valid_config()).is_empty());
}
#[test]
fn contract_reports_missing_or_wrong_fast_time_image() {
let mut missing = valid_config();
missing["services"]
.as_object_mut()
.expect("services object")
.remove("fast_time_server");
assert_eq!(
messages(&missing),
["fast_time_server is missing from the integration compose config"]
);
let mut wrong = valid_config();
wrong["services"]["fast_time_server"]["image"] = json!("wrong/image:tag");
assert_eq!(
messages(&wrong),
[format!(
"fast_time_server image is \"wrong/image:tag\"; expected \"{EXPECTED_IMAGE}\""
)]
);
}
#[test]
fn contract_reports_incorrect_container_display_names() {
let mut config = valid_config();
config["services"]["register_fast_time"]["labels"]["name"] = json!("wrong");
config["services"]["fast_time_server"]
.as_object_mut()
.expect("service object")
.remove("labels");
assert_eq!(
messages(&config),
[
"register_fast_time display name is \"wrong\"; expected \"cf-register-fast-time\"",
"fast_time_server display name is \"\"; expected \"cf-fast-time-server\"",
]
);
}
#[test]
fn every_legacy_image_prefix_is_rejected_in_sorted_service_order() {
let mut config = valid_config();
let services = config["services"].as_object_mut().expect("services object");
services.insert(
"zulu".to_owned(),
json!({"image": "ghcr.io/ibm/fast-time-server:old"}),
);
services.insert(
"alpha".to_owned(),
json!({"image": "ghcr.io/ibm/fast-time-server@sha256:abc"}),
);
assert_eq!(
messages(&config),
[
"alpha uses legacy Fast Time image \"ghcr.io/ibm/fast-time-server@sha256:abc\"",
"zulu uses legacy Fast Time image \"ghcr.io/ibm/fast-time-server:old\"",
]
);
}
#[test]
fn registration_must_use_health_and_streamable_http_urls() {
let mut config = valid_config();
config["services"]["register_fast_time"]["command"] = json!("wrong command");
assert_eq!(
messages(&config),
[
"register_fast_time does not wait for fast_time_server on port 9080",
"register_fast_time does not register the streamable HTTP endpoint at /mcp",
]
);
}
#[test]
fn malformed_rendered_config_returns_stable_diagnostics_instead_of_panicking() {
assert_eq!(
messages(&json!({"services": []})),
["rendered Compose config has no services object"]
);
assert_eq!(
messages(&json!({})),
["rendered Compose config has no services object"]
);
}
#[test]
fn multiple_violations_have_stable_contract_order() {
let config = json!({
"services": {
"register_fast_time": {
"command": [],
"labels": {"name": "cf-register-fast-time"}
},
"legacy_time": {
"image": "ghcr.io/ibm/fast-time-server:old",
"labels": {"name": "cf-legacy-time"}
}
}
});
assert_eq!(
messages(&config),
[
"fast_time_server is missing from the integration compose config",
"legacy_time uses legacy Fast Time image \"ghcr.io/ibm/fast-time-server:old\"",
"register_fast_time does not wait for fast_time_server on port 9080",
"register_fast_time does not register the streamable HTTP endpoint at /mcp",
]
);
}