use anyhow::Context; use anyhow::Result; use codex_app_server_protocol::generate_json_with_experimental; use codex_app_server_protocol::generate_typescript_schema_fixture_subtree_for_tests; use codex_app_server_protocol::read_schema_fixture_subtree; use similar::TextDiff; use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; #[test] fn typescript_schema_fixtures_match_generated() -> Result<()> { let schema_root = schema_root()?; let fixture_tree = read_tree(&schema_root, "typescript")?; let generated_tree = generate_typescript_schema_fixture_subtree_for_tests() .context("generate in-memory typescript schema fixtures")?; assert_schema_trees_match("typescript", &fixture_tree, &generated_tree)?; Ok(()) } #[test] fn json_schema_fixtures_match_generated() -> Result<()> { assert_schema_fixtures_match_generated("json", |output_dir| { generate_json_with_experimental(output_dir, false) }) } fn assert_schema_fixtures_match_generated( label: &'static str, generate: impl FnOnce(&Path) -> Result<()>, ) -> Result<()> { let schema_root = schema_root()?; let fixture_tree = read_tree(&schema_root, label)?; let temp_dir = tempfile::tempdir().context("create temp dir")?; let generated_root = temp_dir.path().join(label); generate(&generated_root).with_context(|| { format!( "generate {label} schema fixtures into {}", generated_root.display() ) })?; let generated_tree = read_tree(temp_dir.path(), label)?; assert_schema_trees_match(label, &fixture_tree, &generated_tree)?; Ok(()) } fn assert_schema_trees_match( label: &str, fixture_tree: &BTreeMap>, generated_tree: &BTreeMap>, ) -> Result<()> { let fixture_paths = fixture_tree .keys() .map(|p| p.display().to_string()) .collect::>(); let generated_paths = generated_tree .keys() .map(|p| p.display().to_string()) .collect::>(); if fixture_paths != generated_paths { let expected = fixture_paths.join("\n"); let actual = generated_paths.join("\n"); let diff = TextDiff::from_lines(&expected, &actual) .unified_diff() .header("fixture", "generated") .to_string(); panic!( "Vendored {label} app-server schema fixture file set doesn't match freshly generated output. \ Run `just write-app-server-schema` to overwrite with your changes.\n\n{diff}" ); } // If the file sets match, diff contents for each file for a nicer error. for (path, expected) in fixture_tree { let actual = generated_tree .get(path) .ok_or_else(|| anyhow::anyhow!("missing generated file: {}", path.display()))?; if expected == actual { continue; } let expected_str = String::from_utf8_lossy(expected); let actual_str = String::from_utf8_lossy(actual); let diff = TextDiff::from_lines(&expected_str, &actual_str) .unified_diff() .header("fixture", "generated") .to_string(); panic!( "Vendored {label} app-server schema fixture {} differs from generated output. \ Run `just write-app-server-schema` to overwrite with your changes.\n\n{diff}", path.display() ); } Ok(()) } fn schema_root() -> Result { // In Bazel runfiles (especially manifest-only mode), resolving directories is not // reliable. Resolve a known file, then walk up to the schema root. let typescript_index = codex_utils_cargo_bin::find_resource!("schema/typescript/index.ts") .context("resolve TypeScript schema index.ts")?; let schema_root = typescript_index .parent() .and_then(|p| p.parent()) .context("derive schema root from schema/typescript/index.ts")? .to_path_buf(); // Sanity check that the JSON fixtures resolve to the same schema root. let json_bundle = codex_utils_cargo_bin::find_resource!("schema/json/codex_app_server_protocol.schemas.json") .context("resolve JSON schema bundle")?; let json_root = json_bundle .parent() .and_then(|p| p.parent()) .context("derive schema root from schema/json/codex_app_server_protocol.schemas.json")?; anyhow::ensure!( schema_root == json_root, "schema roots disagree: typescript={} json={}", schema_root.display(), json_root.display() ); Ok(schema_root) } fn read_tree(root: &Path, label: &str) -> Result>> { read_schema_fixture_subtree(root, label).with_context(|| { format!( "read {label} schema fixture subtree from {}", root.display() ) }) }