SchemaFixture与兼容测试
本文承接Schema与TypeScript导出,面向已经理解 TS/JSON 生成和 stable/experimental exports 的读者。本文回答 fixture 测试比较什么、如何消除平台噪声、预计算文件怎样与 freshly generated output 对齐,以及这些测试不能证明什么;不展开具体 API 兼容策略评审。
1. Fixture闭环
fixture 测试不是简单 snapshot:它读取 vendored 文件,重新生成同一子树,规范化 JSON/TS,再比较文件集合和内容;预计算测试还比较压缩 artifact 解码后的 maps。
2. 重新生成
源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: write_schema_fixtures_with_options
ensure_empty_dir(&typescript_out_dir)?;
ensure_empty_dir(&json_out_dir)?;
generate_ts_with_options(&typescript_out_dir, prettier, GenerateTsOptions::default())?;
generate_json_with_experimental(&json_out_dir, /*experimental_api*/ false)?;
let internal_dir = tempfile::tempdir().context("create internal schema temp dir")?;
generate_internal_json_schema(internal_dir.path())?;
let exports = PrecomputedExports {
typescript: collect_export_files_recursive(&typescript_out_dir)?,
json_schema: collect_export_files_recursive(&json_out_dir)?,
internal_json_schema: collect_export_files_recursive(internal_dir.path())?,
};
write_precomputed_exports(schema_root, "stable", &exports)?;先清空输出目录是为了删除 stale artifact。stable 和 experimental 生成路径不同,后者使用临时目录且不写 internal schema。
3. 文件集合
源码位置:codex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: assert_schema_trees_match
let fixture_paths = fixture_tree
.keys()
.map(|path| path.display().to_string())
.collect::<Vec<_>>();
let generated_paths = generated_tree
.keys()
.map(|path| path.display().to_string())
.collect::<Vec<_>>();
if fixture_paths != generated_paths {
let diff = TextDiff::from_lines(&fixture_paths.join("\n"), &generated_paths.join("\n"))
.unified_diff()
.header("fixture", "generated")
.to_string();
panic!("Vendored schema fixture file set differs from generated output.\\n\\n{diff}");
}测试首先比较路径集合,再比较每个文件内容。缺少一个生成文件和单个字段差异都是契约变化,不能只看核心 index.ts 是否存在。
4. JSON规范化
源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: read_file_bytes、canonicalize_json
if path.extension().is_some_and(|ext| ext == "json") {
let value: Value = serde_json::from_slice(&bytes)?;
let value = canonicalize_json(&value);
return Ok(serde_json::to_vec_pretty(&value)?);
}canonicalization 对 object key 和部分 schema arrays 稳定排序;只有每个数组元素都能生成稳定 sort key 时才排序,避免误改 tuple/prefixItems 等有序语义。
5. TypeScript规范化
源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: read_file_bytes
if path.extension().is_some_and(|ext| ext == "ts") {
let text = String::from_utf8(bytes)?;
let text = text.replace("\\r\\n", "\\n").replace('\\r', "\\n");
let text = text
.strip_prefix(GENERATED_TS_HEADER)
.unwrap_or(&text)
.to_string();
return Ok(text.into_bytes());
}TS fixture 比较去掉生成 banner 并统一换行,避免平台换行和重复 banner 造成无意义差异;import 依赖仍会被单独验证。
TS fixture 会统一换行、去除生成 banner 后比较内容;测试还验证生成的 import 依赖存在。banner 不属于 schema 语义,缺失依赖则属于真实生成错误。
6. 预计算对比
源码位置:codex-rs/app-server-protocol/src/precomputed_exports_tests.rs :: precomputed_exports_are_written_to_disk
let exports = decode_precomputed_exports(/*experimental_api*/ false)?;
assert_eq!(
collect_export_files_recursive(&typescript_dir)?,
exports.typescript
);
assert_eq!(collect_export_files_recursive(&json_dir)?, exports.json_schema);
assert_json_export_trees_match(
&exports.internal_json_schema,
&collect_export_files_recursive(&internal_json_dir)?,
)?;预计算压缩文件解码后按 path→content map 比较。它验证嵌入 artifact 与当前生成结果一致,而不只是检查压缩文件存在。
7. 失败与兼容边界
文件集合变化通常意味着新增/删除公开类型;JSON 字段变化可能是 additive、breaking 或仅排序噪声;TS import 缺失是生成拓扑错误。fixture 通过不能证明旧客户端能理解新字段,也不能证明 runtime handler 接受所有 schema 合法输入。
8. 源码验证
typescript_schema_fixtures_match_generated 比较 TS 文件树;json_schema_fixtures_match_generated 比较 canonical JSON;stable_precomputed_exports_match_schema_fixtures 和 experimental_precomputed_exports_match_generated 比较 stable/experimental artifact。测试失败信息会生成 unified diff,并提示重新写入 fixture。
源码位置:
codex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: typescript_schema_fixtures_match_generatedcodex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: json_schema_fixtures_match_generatedcodex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: stable_precomputed_exports_match_schema_fixturescodex-rs/app-server-protocol/src/precomputed_exports_tests.rs :: precomputed_exports_are_written_to_disk
cd codex-rs
cargo test -p codex-app-server-protocol schema_fixtures_match_generated
cargo test -p codex-app-server-protocol precomputed_exports_are_written_to_disk9. 差异排查
看到 fixture diff 时,先判断是文件集合、JSON 语义、TS 依赖还是 stable/experimental 分层变化;若只是 array 顺序,检查 canonicalization 是否覆盖该 schema;若 fixture 通过但客户端行为异常,再回到 runtime decoder 和 handler,而不是继续修改 snapshot。
