feat: vendor app-server protocol schema fixtures (#10371)

Similar to what @sayan-oai did in openai/codex#8956 for
`config.schema.json`, this PR updates the repo so that it includes the
output of `codex app-server generate-json-schema` and `codex app-server
generate-ts` and adds a test to verify it is in sync with the current
code.

Motivation:
- This makes any schema changes introduced by a PR transparent during
code review.
- In particular, this should help us catch PRs that would introduce a
non-backwards-compatible change to the app schema (eventually, this
should also be enforced by tooling).
- Once https://github.com/openai/codex/pull/10231 is in to formalize the
notion of "experimental" fields, we can work on ensuring the
non-experimental bits are backwards-compatible.

`codex-rs/app-server-protocol/tests/schema_fixtures.rs` was added as the
test and `just write-app-server-schema` can be use to generate the
vendored schema files.

Incidentally, when I run:

```
rg _ codex-rs/app-server-protocol/schema/typescript/v2
```

I see a number of `snake_case` names that should be `camelCase`.
This commit is contained in:
Michael Bolin
2026-02-01 23:38:43 -08:00
committed by GitHub
parent 08a5ad95a8
commit 974355cfdd
570 changed files with 91513 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
use anyhow::Context;
use anyhow::Result;
use serde_json::Map;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
pub fn read_schema_fixture_tree(schema_root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>> {
let typescript_root = schema_root.join("typescript");
let json_root = schema_root.join("json");
let mut all = BTreeMap::new();
for (rel, bytes) in collect_files_recursive(&typescript_root)? {
all.insert(PathBuf::from("typescript").join(rel), bytes);
}
for (rel, bytes) in collect_files_recursive(&json_root)? {
all.insert(PathBuf::from("json").join(rel), bytes);
}
Ok(all)
}
/// Regenerates `schema/typescript/` and `schema/json/`.
///
/// This is intended to be used by tooling (e.g., `just write-app-server-schema`).
/// It deletes any previously generated files so stale artifacts are removed.
pub fn write_schema_fixtures(schema_root: &Path, prettier: Option<&Path>) -> Result<()> {
let typescript_out_dir = schema_root.join("typescript");
let json_out_dir = schema_root.join("json");
ensure_empty_dir(&typescript_out_dir)?;
ensure_empty_dir(&json_out_dir)?;
crate::generate_ts(&typescript_out_dir, prettier)?;
crate::generate_json(&json_out_dir)?;
Ok(())
}
fn ensure_empty_dir(dir: &Path) -> Result<()> {
if dir.exists() {
std::fs::remove_dir_all(dir)
.with_context(|| format!("failed to remove {}", dir.display()))?;
}
std::fs::create_dir_all(dir).with_context(|| format!("failed to create {}", dir.display()))?;
Ok(())
}
fn read_file_bytes(path: &Path) -> Result<Vec<u8>> {
let bytes =
std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
if path.extension().is_some_and(|ext| ext == "json") {
let value: Value = serde_json::from_slice(&bytes)
.with_context(|| format!("failed to parse JSON in {}", path.display()))?;
let value = canonicalize_json(&value);
let normalized = serde_json::to_vec_pretty(&value)
.with_context(|| format!("failed to reserialize JSON in {}", path.display()))?;
return Ok(normalized);
}
if path.extension().is_some_and(|ext| ext == "ts") {
// Windows checkouts (and some generators) may produce CRLF; normalize so the
// fixture test is platform-independent.
let text = String::from_utf8(bytes)
.with_context(|| format!("expected UTF-8 TypeScript in {}", path.display()))?;
let text = text.replace("\r\n", "\n").replace('\r', "\n");
return Ok(text.into_bytes());
}
Ok(bytes)
}
fn canonicalize_json(value: &Value) -> Value {
match value {
Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
Value::Object(map) => {
let mut entries: Vec<_> = map.iter().collect();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut sorted = Map::with_capacity(map.len());
for (key, child) in entries {
sorted.insert(key.clone(), canonicalize_json(child));
}
Value::Object(sorted)
}
_ => value.clone(),
}
}
fn collect_files_recursive(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>> {
let mut files = BTreeMap::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)
.with_context(|| format!("failed to read dir {}", dir.display()))?
{
let entry =
entry.with_context(|| format!("failed to read dir entry in {}", dir.display()))?;
let path = entry.path();
// On some platforms, Bazel runfiles are symlinks. `DirEntry::file_type()` does not
// follow symlinks, so use `metadata()` here to treat symlinks as the files/dirs they
// point to.
let metadata = std::fs::metadata(&path)
.with_context(|| format!("failed to stat {}", path.display()))?;
if metadata.is_dir() {
stack.push(path);
continue;
} else if !metadata.is_file() {
continue;
}
let rel = path
.strip_prefix(root)
.with_context(|| {
format!(
"failed to strip prefix {} from {}",
root.display(),
path.display()
)
})?
.to_path_buf();
files.insert(rel, read_file_bytes(&path)?);
}
}
Ok(files)
}