1use std::env;
2use std::ffi::OsString;
3use std::fs;
4use std::net::{SocketAddr, TcpListener, TcpStream};
5use std::path::{Path, PathBuf};
6use std::process::{Child, Command, Stdio};
7use std::thread;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use crate::{
11 generate_markdown_report, load_results_from_dir, load_scenarios_from_dir, CompatibilityReport,
12};
13
14const SERVER_STARTUP_ATTEMPTS: usize = 900;
15const SERVER_STARTUP_POLL_INTERVAL: Duration = Duration::from_millis(100);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum PeerTarget {
19 Js,
20 Python,
21 Go,
22 Cpp,
23}
24
25impl PeerTarget {
26 pub fn label(self) -> &'static str {
27 match self {
28 Self::Js => "js",
29 Self::Python => "python",
30 Self::Go => "go",
31 Self::Cpp => "cpp",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ConformanceAuthMode {
38 StaticBearer,
39 LocalOAuth,
40}
41
42#[derive(Debug, Clone)]
43pub struct ConformanceRunOptions {
44 pub repo_root: PathBuf,
45 pub scenarios_dir: PathBuf,
46 pub results_dir: PathBuf,
47 pub report_output: PathBuf,
48 pub policy_path: PathBuf,
49 pub upstream_server_script: PathBuf,
50 pub auth_mode: ConformanceAuthMode,
51 pub auth_token: String,
52 pub admin_token: String,
53 pub auth_scope: String,
54 pub listen: Option<SocketAddr>,
55 pub peers: Vec<PeerTarget>,
56 pub node_binary: OsString,
57 pub python_binary: OsString,
58 pub go_binary: OsString,
59 pub cargo_binary: OsString,
60 pub peer_binaries: Vec<(PeerTarget, PathBuf)>,
61}
62
63#[derive(Debug, Clone)]
64pub struct ConformanceRunSummary {
65 pub listen: SocketAddr,
66 pub results_dir: PathBuf,
67 pub report_output: PathBuf,
68 pub peer_result_files: Vec<PathBuf>,
69}
70
71struct ConformanceRuntimeState {
72 _directory: tempfile::TempDir,
75 auth_server_seed_path: PathBuf,
76 session_db_path: PathBuf,
77}
78
79impl ConformanceRuntimeState {
80 fn create() -> Result<Self, std::io::Error> {
81 let mut builder = tempfile::Builder::new();
82 builder.prefix("chio-conformance-runtime-");
83 #[cfg(unix)]
84 {
85 use std::os::unix::fs::PermissionsExt;
86 builder.permissions(fs::Permissions::from_mode(0o700));
87 }
88 let directory = builder.tempdir()?;
89 #[cfg(unix)]
90 {
91 use std::os::unix::fs::PermissionsExt;
92 fs::set_permissions(directory.path(), fs::Permissions::from_mode(0o700))?;
93 }
94 let auth_server_seed_path = directory.path().join("auth-server.seed");
95 let session_db_path = directory.path().join("mcp-session.sqlite3");
96 Ok(Self {
97 _directory: directory,
98 auth_server_seed_path,
99 session_db_path,
100 })
101 }
102}
103
104#[derive(Debug, thiserror::Error)]
105pub enum RunnerError {
106 #[error("i/o error: {0}")]
107 Io(#[from] std::io::Error),
108
109 #[error("failed to spawn process `{command}`: {source}")]
110 Spawn {
111 command: String,
112 #[source]
113 source: std::io::Error,
114 },
115
116 #[error("process `{command}` exited unsuccessfully with status {status}; see {log_path}")]
117 ProcessFailed {
118 command: String,
119 status: i32,
120 log_path: String,
121 },
122
123 #[error("timeout while waiting for MCP edge on {listen}")]
124 ServerStartupTimeout { listen: SocketAddr },
125
126 #[error("failed to load generated artifacts: {0}")]
127 Load(#[from] crate::load::LoadError),
128
129 #[error("peer result generation produced no JSON files in {path}")]
130 NoResults { path: String },
131}
132
133fn conformance_fixture_root_from_manifest_dir(manifest_dir: &Path) -> PathBuf {
134 let workspace_root = manifest_dir
135 .ancestors()
136 .nth(3)
137 .map(Path::to_path_buf)
138 .unwrap_or_else(|| manifest_dir.to_path_buf());
139 if workspace_root
140 .join("tests/conformance/scenarios/mcp_core")
141 .is_dir()
142 && workspace_root
143 .join("tests/conformance/fixtures/mcp_core/policy.yaml")
144 .is_file()
145 {
146 return workspace_root;
147 }
148
149 manifest_dir.to_path_buf()
150}
151
152pub fn default_repo_root() -> PathBuf {
153 conformance_fixture_root_from_manifest_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
154}
155
156fn conformance_token_from_env(var: &str, role: &str) -> String {
162 if let Ok(value) = env::var(var) {
163 let trimmed = value.trim();
164 if !trimmed.is_empty() {
165 return trimmed.to_string();
166 }
167 }
168 let nonce = SystemTime::now()
169 .duration_since(UNIX_EPOCH)
170 .map(|duration| duration.as_nanos())
171 .unwrap_or(0);
172 format!("dev-only-conformance-{role}-{nonce}")
173}
174
175fn apply_conformance_auth_env(
178 command: &mut Command,
179 options: &ConformanceRunOptions,
180 auth_mode: ConformanceAuthMode,
181) {
182 command
183 .env("CHIO_CONFORMANCE_AUTH_TOKEN", &options.auth_token)
184 .env("CHIO_CONFORMANCE_ADMIN_TOKEN", &options.admin_token);
185 match auth_mode {
186 ConformanceAuthMode::StaticBearer => {
187 command.env("CHIO_AUTH_TOKEN", &options.auth_token);
188 }
189 ConformanceAuthMode::LocalOAuth => {
190 command
191 .env_remove("CHIO_AUTH_TOKEN")
192 .env_remove("CHIO_MCP_AUTH_TOKEN")
193 .env_remove("CHIO_MCP_ADMIN_TOKEN");
194 command.env("CHIO_ADMIN_TOKEN", &options.admin_token);
195 }
196 }
197}
198
199pub fn default_run_options() -> ConformanceRunOptions {
200 let repo_root = default_repo_root();
201 ConformanceRunOptions {
202 scenarios_dir: repo_root.join("tests/conformance/scenarios/mcp_core"),
203 results_dir: repo_root.join("tests/conformance/results/generated/mcp-core-live"),
204 report_output: repo_root.join("tests/conformance/reports/generated/mcp-core-live.md"),
205 policy_path: repo_root.join("tests/conformance/fixtures/mcp_core/policy.yaml"),
206 upstream_server_script: repo_root
207 .join("tests/conformance/fixtures/mcp_core/mock_mcp_server.py"),
208 auth_mode: ConformanceAuthMode::StaticBearer,
209 auth_token: conformance_token_from_env("CHIO_CONFORMANCE_AUTH_TOKEN", "auth"),
210 admin_token: conformance_token_from_env("CHIO_CONFORMANCE_ADMIN_TOKEN", "admin"),
211 auth_scope: "mcp:invoke".to_string(),
212 listen: None,
213 peers: vec![PeerTarget::Js, PeerTarget::Python],
214 node_binary: OsString::from("node"),
215 python_binary: OsString::from("python3"),
216 go_binary: OsString::from("go"),
217 cargo_binary: OsString::from("cargo"),
218 peer_binaries: Vec::new(),
219 repo_root,
220 }
221}
222
223pub fn run_conformance_harness(
224 options: &ConformanceRunOptions,
225) -> Result<ConformanceRunSummary, RunnerError> {
226 if options.results_dir.exists() {
227 fs::remove_dir_all(&options.results_dir)?;
228 }
229 fs::create_dir_all(&options.results_dir)?;
230 #[cfg(unix)]
231 {
232 use std::os::unix::fs::PermissionsExt;
233 fs::set_permissions(&options.results_dir, fs::Permissions::from_mode(0o700))?;
234 }
235 if let Some(parent) = options.report_output.parent() {
236 fs::create_dir_all(parent)?;
237 }
238
239 let artifacts_dir = options.results_dir.join("artifacts");
240 let logs_dir = artifacts_dir.join("logs");
241 fs::create_dir_all(&logs_dir)?;
242 #[cfg(unix)]
243 {
244 use std::os::unix::fs::PermissionsExt;
245 fs::set_permissions(&artifacts_dir, fs::Permissions::from_mode(0o700))?;
246 }
247
248 let listen = match options.listen {
249 Some(listen) => listen,
250 None => reserve_listen_addr()?,
251 };
252 let chio_executable = ensure_chio_executable(&options.repo_root, &options.cargo_binary)?;
253 let runtime_state = ConformanceRuntimeState::create()?;
254 let server_log_path = logs_dir.join("chio-mcp-serve-http.log");
255 let server = spawn_remote_edge(
256 &chio_executable,
257 options,
258 listen,
259 &runtime_state,
260 &server_log_path,
261 )?;
262 let mut server_guard = ChildGuard { child: server };
263 wait_for_server(listen, &mut server_guard.child, &server_log_path)?;
264
265 let mut peer_result_files = Vec::new();
266 for peer in &options.peers {
267 let peer_results_path = options
268 .results_dir
269 .join(format!("{}-remote-http.json", peer.label()));
270 let peer_artifacts_dir = artifacts_dir.join(peer.label());
271 let peer_log_path = logs_dir.join(format!("{}-peer.log", peer.label()));
272 fs::create_dir_all(&peer_artifacts_dir)?;
273 run_peer(
274 *peer,
275 options,
276 listen,
277 &peer_results_path,
278 &peer_artifacts_dir,
279 &peer_log_path,
280 )?;
281 peer_result_files.push(peer_results_path);
282 }
283
284 let results = load_results_from_dir(&options.results_dir)?;
285 if results.is_empty() {
286 return Err(RunnerError::NoResults {
287 path: options.results_dir.display().to_string(),
288 });
289 }
290 let report = CompatibilityReport {
291 scenarios: load_scenarios_from_dir(&options.scenarios_dir)?,
292 results,
293 };
294 fs::write(&options.report_output, generate_markdown_report(&report))?;
295
296 Ok(ConformanceRunSummary {
297 listen,
298 results_dir: options.results_dir.clone(),
299 report_output: options.report_output.clone(),
300 peer_result_files,
301 })
302}
303
304fn ensure_chio_executable(
305 repo_root: &Path,
306 cargo_binary: &OsString,
307) -> Result<PathBuf, RunnerError> {
308 let candidates = chio_executable_candidates(repo_root);
309 for candidate in &candidates {
310 if candidate.exists() {
311 return Ok(candidate.clone());
312 }
313 }
314 let status = Command::new(cargo_binary)
315 .current_dir(repo_root)
316 .arg("build")
317 .arg("-q")
318 .arg("-p")
319 .arg("chio-cli")
320 .status()
321 .map_err(|source| RunnerError::Spawn {
322 command: "cargo build -q -p chio-cli".to_string(),
323 source,
324 })?;
325 if !status.success() {
326 return Err(RunnerError::ProcessFailed {
327 command: "cargo build -q -p chio-cli".to_string(),
328 status: status.code().unwrap_or(1),
329 log_path: "<stderr>".to_string(),
330 });
331 }
332 for candidate in &candidates {
333 if candidate.exists() {
334 return Ok(candidate.clone());
335 }
336 }
337 let checked = candidates
338 .iter()
339 .map(|candidate| candidate.display().to_string())
340 .collect::<Vec<_>>()
341 .join(", ");
342 Err(RunnerError::ProcessFailed {
343 command: "cargo build -q -p chio-cli".to_string(),
344 status: 1,
345 log_path: format!("<stderr>; checked {checked}"),
346 })
347}
348
349fn chio_binary_name() -> &'static str {
350 if cfg!(windows) {
351 "chio.exe"
352 } else {
353 "chio"
354 }
355}
356
357fn chio_executable_candidates(repo_root: &Path) -> Vec<PathBuf> {
358 let mut candidates = Vec::new();
359 if let Ok(current_exe) = env::current_exe() {
360 if current_exe.file_name().and_then(|name| name.to_str()) == Some(chio_binary_name()) {
361 push_chio_candidate(&mut candidates, current_exe);
362 }
363 }
364 if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR") {
365 push_chio_candidate(
366 &mut candidates,
367 PathBuf::from(target_dir)
368 .join("debug")
369 .join(chio_binary_name()),
370 );
371 }
372 push_chio_candidate(
373 &mut candidates,
374 repo_root
375 .join("target")
376 .join("debug")
377 .join(chio_binary_name()),
378 );
379 candidates
380}
381
382fn push_chio_candidate(candidates: &mut Vec<PathBuf>, path: PathBuf) {
383 if !candidates.iter().any(|candidate| candidate == &path) {
384 candidates.push(path);
385 }
386}
387
388fn spawn_remote_edge(
389 chio_executable: &Path,
390 options: &ConformanceRunOptions,
391 listen: SocketAddr,
392 runtime_state: &ConformanceRuntimeState,
393 log_path: &Path,
394) -> Result<Child, RunnerError> {
395 let log = fs::File::create(log_path)?;
396 let log_clone = log.try_clone()?;
397 let mut command = Command::new(chio_executable);
398 command
399 .current_dir(&options.repo_root)
400 .arg("mcp")
401 .arg("serve-http")
402 .arg("--policy")
403 .arg(&options.policy_path)
404 .arg("--server-id")
405 .arg("conformance-mcp-core")
406 .arg("--server-name")
407 .arg("Conformance Fixture")
408 .arg("--server-version")
409 .arg("0.1.0")
410 .arg("--listen")
411 .arg(listen.to_string());
412
413 let public_base_url = format!("http://{listen}");
414 command
415 .arg("--session-db")
416 .arg(&runtime_state.session_db_path);
417 let mut command_description = format!(
418 "{} mcp serve-http --policy {} --server-id conformance-mcp-core --listen {} --session-db {}",
419 chio_executable.display(),
420 options.policy_path.display(),
421 listen,
422 runtime_state.session_db_path.display()
423 );
424
425 apply_conformance_auth_env(&mut command, options, options.auth_mode);
426
427 match options.auth_mode {
428 ConformanceAuthMode::StaticBearer => {
429 command_description.push_str(" (auth via CHIO_AUTH_TOKEN env)");
430 }
431 ConformanceAuthMode::LocalOAuth => {
432 command
433 .arg("--public-base-url")
434 .arg(&public_base_url)
435 .arg("--auth-server-seed-file")
436 .arg(&runtime_state.auth_server_seed_path)
437 .arg("--auth-jwt-audience")
438 .arg(format!("{public_base_url}/mcp"))
439 .arg("--auth-scope")
440 .arg(&options.auth_scope);
441 command_description.push_str(&format!(
442 " --public-base-url {} --auth-server-seed-file {} --auth-jwt-audience {}/mcp --auth-scope {} (admin via CHIO_ADMIN_TOKEN env)",
443 public_base_url,
444 runtime_state.auth_server_seed_path.display(),
445 public_base_url,
446 options.auth_scope
447 ));
448 }
449 }
450
451 command
452 .arg("--")
453 .arg(&options.python_binary)
454 .arg(&options.upstream_server_script)
455 .stdout(Stdio::from(log))
456 .stderr(Stdio::from(log_clone))
457 .spawn()
458 .map_err(|source| RunnerError::Spawn {
459 command: format!(
460 "{} -- {} {}",
461 command_description,
462 PathBuf::from(&options.python_binary).display(),
463 options.upstream_server_script.display()
464 ),
465 source,
466 })
467}
468
469fn run_peer(
470 peer: PeerTarget,
471 options: &ConformanceRunOptions,
472 listen: SocketAddr,
473 results_output: &Path,
474 artifacts_dir: &Path,
475 log_path: &Path,
476) -> Result<(), RunnerError> {
477 let log = fs::File::create(log_path)?;
478 let log_clone = log.try_clone()?;
479 let base_url = format!("http://{listen}");
480 let command_description;
481 let mut command = if let Some(binary) = peer_binary_override(peer, options) {
482 command_description = binary.display().to_string();
483 let mut command = Command::new(binary);
484 command.current_dir(&options.repo_root);
485 command
486 } else {
487 match peer {
488 PeerTarget::Js => {
489 let script = options
490 .repo_root
491 .join("tests/conformance/peers/js/client.mjs");
492 command_description = format!(
493 "{} {}",
494 PathBuf::from(&options.node_binary).display(),
495 script.display()
496 );
497 let mut command = Command::new(&options.node_binary);
498 command.current_dir(&options.repo_root).arg(script);
499 command
500 }
501 PeerTarget::Python => {
502 let script = options
503 .repo_root
504 .join("tests/conformance/peers/python/client.py");
505 command_description = format!(
506 "{} {}",
507 PathBuf::from(&options.python_binary).display(),
508 script.display()
509 );
510 let mut command = Command::new(&options.python_binary);
511 command.current_dir(&options.repo_root).arg(script);
512 command
513 }
514 PeerTarget::Go => {
515 command_description = format!(
516 "{} run ./cmd/conformance-peer",
517 PathBuf::from(&options.go_binary).display()
518 );
519 let mut command = Command::new(&options.go_binary);
520 command
521 .current_dir(options.repo_root.join("sdks/go/chio-go"))
522 .arg("run")
523 .arg("./cmd/conformance-peer");
524 command
525 }
526 PeerTarget::Cpp => {
527 let executable = ensure_cpp_peer_executable(&options.repo_root)?;
528 command_description = executable.display().to_string();
529 let mut command = Command::new(executable);
530 command.current_dir(&options.repo_root);
531 command
532 }
533 }
534 };
535
536 apply_conformance_auth_env(&mut command, options, options.auth_mode);
537
538 let status = command
539 .arg("--base-url")
540 .arg(base_url)
541 .arg("--auth-mode")
542 .arg(match options.auth_mode {
543 ConformanceAuthMode::StaticBearer => "static-bearer",
544 ConformanceAuthMode::LocalOAuth => "oauth-local",
545 })
546 .arg("--auth-scope")
547 .arg(&options.auth_scope)
548 .arg("--scenarios-dir")
549 .arg(&options.scenarios_dir)
550 .arg("--results-output")
551 .arg(results_output)
552 .arg("--artifacts-dir")
553 .arg(artifacts_dir)
554 .stdout(Stdio::from(log))
555 .stderr(Stdio::from(log_clone))
556 .status()
557 .map_err(|source| RunnerError::Spawn {
558 command: format!(
559 "{} --base-url http://{} --scenarios-dir {} --results-output {}",
560 command_description,
561 listen,
562 options.scenarios_dir.display(),
563 results_output.display()
564 ),
565 source,
566 })?;
567
568 if !status.success() {
569 return Err(RunnerError::ProcessFailed {
570 command: command_description,
571 status: status.code().unwrap_or(1),
572 log_path: log_path.display().to_string(),
573 });
574 }
575 Ok(())
576}
577
578fn peer_binary_override(peer: PeerTarget, options: &ConformanceRunOptions) -> Option<&Path> {
579 options
580 .peer_binaries
581 .iter()
582 .find_map(|(target, path)| (*target == peer).then_some(path.as_path()))
583}
584
585fn ensure_cpp_peer_executable(repo_root: &Path) -> Result<PathBuf, RunnerError> {
586 let build_dir = repo_root.join("target/chio-cpp-conformance");
587 let build_config = "Debug";
588 let executable_name = format!("chio_cpp_conformance_peer{}", std::env::consts::EXE_SUFFIX);
589 let executable_candidates = [
590 build_dir.join(&executable_name),
591 build_dir.join(build_config).join(&executable_name),
592 ];
593 let source_dir = repo_root.join("sdks/cpp/chio-cpp");
594 let configure_status = Command::new("cmake")
595 .current_dir(repo_root)
596 .arg("-S")
597 .arg(&source_dir)
598 .arg("-B")
599 .arg(&build_dir)
600 .arg("-DCHIO_CPP_BUILD_TESTS=OFF")
601 .arg("-DCHIO_CPP_BUILD_EXAMPLES=OFF")
602 .arg("-DCHIO_CPP_ENABLE_CURL=ON")
603 .arg("-DCHIO_CPP_BUILD_CONFORMANCE_PEER=ON")
604 .arg("-DCMAKE_BUILD_TYPE=Debug")
605 .status()
606 .map_err(|source| RunnerError::Spawn {
607 command: "cmake configure chio_cpp_conformance_peer".to_string(),
608 source,
609 })?;
610 if !configure_status.success() {
611 return Err(RunnerError::ProcessFailed {
612 command: "cmake configure chio_cpp_conformance_peer".to_string(),
613 status: configure_status.code().unwrap_or(1),
614 log_path: "<stderr>".to_string(),
615 });
616 }
617
618 let build_status = Command::new("cmake")
619 .current_dir(repo_root)
620 .arg("--build")
621 .arg(&build_dir)
622 .arg("--target")
623 .arg("chio_cpp_conformance_peer")
624 .arg("--config")
625 .arg(build_config)
626 .status()
627 .map_err(|source| RunnerError::Spawn {
628 command: "cmake --build chio_cpp_conformance_peer".to_string(),
629 source,
630 })?;
631 if !build_status.success() {
632 return Err(RunnerError::ProcessFailed {
633 command: "cmake --build chio_cpp_conformance_peer".to_string(),
634 status: build_status.code().unwrap_or(1),
635 log_path: "<stderr>".to_string(),
636 });
637 }
638
639 for executable in executable_candidates {
640 if executable.exists() {
641 return Ok(executable);
642 }
643 }
644 Err(RunnerError::ProcessFailed {
645 command: "cmake --build chio_cpp_conformance_peer".to_string(),
646 status: 1,
647 log_path: "<stderr>".to_string(),
648 })
649}
650
651fn reserve_listen_addr() -> Result<SocketAddr, RunnerError> {
652 let listener = TcpListener::bind("127.0.0.1:0")?;
653 let addr = listener.local_addr()?;
654 drop(listener);
655 Ok(addr)
656}
657
658fn wait_for_server(
659 listen: SocketAddr,
660 server: &mut Child,
661 log_path: &Path,
662) -> Result<(), RunnerError> {
663 for _ in 0..SERVER_STARTUP_ATTEMPTS {
664 if TcpStream::connect(listen).is_ok() {
665 thread::sleep(SERVER_STARTUP_POLL_INTERVAL);
666 return Ok(());
667 }
668 if let Some(status) = server.try_wait()? {
669 return Err(RunnerError::ProcessFailed {
670 command: "chio mcp serve-http".to_string(),
671 status: status.code().unwrap_or(1),
672 log_path: log_path.display().to_string(),
673 });
674 }
675 thread::sleep(SERVER_STARTUP_POLL_INTERVAL);
676 }
677 Err(RunnerError::ServerStartupTimeout { listen })
678}
679
680struct ChildGuard {
681 child: Child,
682}
683
684impl Drop for ChildGuard {
685 fn drop(&mut self) {
686 let _ = self.child.kill();
687 let _ = self.child.wait();
688 }
689}
690
691pub fn unique_run_dir(prefix: &str) -> PathBuf {
692 let nonce = SystemTime::now()
693 .duration_since(UNIX_EPOCH)
694 .map(|duration| duration.as_nanos())
695 .unwrap_or(0);
696 std::env::temp_dir().join(format!("{prefix}-{nonce}"))
697}
698
699#[cfg(test)]
700mod tests {
701 use super::{
702 apply_conformance_auth_env, conformance_fixture_root_from_manifest_dir,
703 default_run_options, ConformanceAuthMode, ConformanceRuntimeState,
704 };
705 use std::ffi::OsStr;
706 use std::fs;
707 use std::path::Path;
708 use std::process::Command;
709 use std::time::{SystemTime, UNIX_EPOCH};
710
711 fn command_env(command: &Command, key: &str) -> Option<Option<String>> {
712 command.get_envs().find_map(|(name, value)| {
713 if name == OsStr::new(key) {
714 return Some(value.map(|raw| raw.to_string_lossy().into_owned()));
715 }
716 None
717 })
718 }
719
720 fn unique_test_dir(label: &str) -> std::path::PathBuf {
721 let nanos = SystemTime::now()
722 .duration_since(UNIX_EPOCH)
723 .map(|duration| duration.as_nanos())
724 .unwrap_or(0);
725 std::env::temp_dir().join(format!(
726 "chio-conformance-{label}-{}-{nanos}",
727 std::process::id()
728 ))
729 }
730
731 fn create_default_fixture_tree(root: &Path) {
732 let scenarios = root.join("tests/conformance/scenarios/mcp_core");
733 let fixtures = root.join("tests/conformance/fixtures/mcp_core");
734 if let Err(error) = fs::create_dir_all(&scenarios) {
735 panic!("failed to create {}: {error}", scenarios.display());
736 }
737 if let Err(error) = fs::create_dir_all(&fixtures) {
738 panic!("failed to create {}: {error}", fixtures.display());
739 }
740 if let Err(error) = fs::write(scenarios.join("initialize.json"), "{}\n") {
741 panic!("failed to write scenario fixture: {error}");
742 }
743 if let Err(error) = fs::write(fixtures.join("policy.yaml"), "version: 1\n") {
744 panic!("failed to write policy fixture: {error}");
745 }
746 }
747
748 #[test]
749 fn runtime_state_is_private_and_removed_after_the_run() {
750 let runtime_state = ConformanceRuntimeState::create()
751 .unwrap_or_else(|error| panic!("create conformance runtime state: {error}"));
752 let runtime_root = runtime_state._directory.path().to_path_buf();
753 assert_eq!(
754 runtime_state.session_db_path.parent(),
755 Some(runtime_root.as_path())
756 );
757 assert_eq!(
758 runtime_state.auth_server_seed_path.parent(),
759 Some(runtime_root.as_path())
760 );
761 #[cfg(unix)]
762 {
763 use std::os::unix::fs::PermissionsExt;
764 let metadata = fs::metadata(&runtime_root)
765 .unwrap_or_else(|error| panic!("read runtime directory metadata: {error}"));
766 assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
767 }
768
769 drop(runtime_state);
770 assert!(!runtime_root.exists());
771 }
772
773 #[test]
774 fn store_less_fixture_opts_into_ephemeral_revocation() {
775 #[derive(serde::Deserialize)]
784 struct KernelSection {
785 #[serde(default)]
786 allow_ephemeral_receipt_log: bool,
787 #[serde(default)]
788 allow_ephemeral_revocation_store: bool,
789 }
790 #[derive(serde::Deserialize)]
791 struct FixturePolicy {
792 kernel: KernelSection,
793 }
794
795 let fixture_root =
796 conformance_fixture_root_from_manifest_dir(Path::new(env!("CARGO_MANIFEST_DIR")));
797 let policy_path = fixture_root.join("tests/conformance/fixtures/mcp_core/policy.yaml");
798 let raw = fs::read_to_string(&policy_path)
799 .unwrap_or_else(|error| panic!("read {}: {error}", policy_path.display()));
800 let policy: FixturePolicy = serde_yaml::from_str(&raw)
801 .unwrap_or_else(|error| panic!("parse {}: {error}", policy_path.display()));
802
803 if policy.kernel.allow_ephemeral_receipt_log {
804 assert!(
805 policy.kernel.allow_ephemeral_revocation_store,
806 "a store-less conformance fixture that allows an ephemeral receipt log must also \
807 allow an ephemeral revocation store, or mediated tool calls are denied by the \
808 revocation durability gate"
809 );
810 }
811 }
812
813 #[test]
814 fn fixture_root_prefers_workspace_tree_when_present() {
815 let root = unique_test_dir("workspace");
816 let manifest_dir = root.join("crates/tooling/chio-conformance");
817 create_default_fixture_tree(&root);
818 if let Err(error) = fs::create_dir_all(&manifest_dir) {
819 panic!("failed to create {}: {error}", manifest_dir.display());
820 }
821
822 assert_eq!(
823 conformance_fixture_root_from_manifest_dir(&manifest_dir),
824 root
825 );
826
827 let _ = fs::remove_dir_all(root);
828 }
829
830 #[test]
831 fn fixture_root_falls_back_to_packaged_crate_tree() {
832 let package_root = unique_test_dir("package");
833 create_default_fixture_tree(&package_root);
834
835 assert_eq!(
836 conformance_fixture_root_from_manifest_dir(&package_root),
837 package_root
838 );
839
840 let _ = fs::remove_dir_all(package_root);
841 }
842
843 #[test]
844 fn local_oauth_child_env_removes_inherited_static_bearer_token() {
845 let mut options = default_run_options();
846 options.auth_mode = ConformanceAuthMode::LocalOAuth;
847 options.auth_token = "local-auth-token".to_string();
848 options.admin_token = "local-admin-token".to_string();
849 let mut command = Command::new("chio");
850 command.env("CHIO_AUTH_TOKEN", "parent-static-token");
851 command.env("CHIO_MCP_AUTH_TOKEN", "parent-mcp-static-token");
852 command.env("CHIO_MCP_ADMIN_TOKEN", "parent-mcp-admin-token");
853
854 apply_conformance_auth_env(&mut command, &options, options.auth_mode);
855
856 assert_eq!(command_env(&command, "CHIO_AUTH_TOKEN"), Some(None));
857 assert_eq!(command_env(&command, "CHIO_MCP_AUTH_TOKEN"), Some(None));
858 assert_eq!(command_env(&command, "CHIO_MCP_ADMIN_TOKEN"), Some(None));
859 assert_eq!(
860 command_env(&command, "CHIO_ADMIN_TOKEN"),
861 Some(Some("local-admin-token".to_string()))
862 );
863 assert_eq!(
864 command_env(&command, "CHIO_CONFORMANCE_AUTH_TOKEN"),
865 Some(Some("local-auth-token".to_string()))
866 );
867 assert_eq!(
868 command_env(&command, "CHIO_CONFORMANCE_ADMIN_TOKEN"),
869 Some(Some("local-admin-token".to_string()))
870 );
871 }
872}