fix(pi): fail closed on unrepresentable compat spread

This commit is contained in:
SaladDay
2026-08-02 16:16:14 +00:00
parent bd3e2e54fc
commit d2ad7995a3
6 changed files with 511 additions and 34 deletions
+112
View File
@@ -91,6 +91,7 @@ writeFileSync(
`export { streamSimple as openaiResponses } from "${PI}/packages/ai/src/api/openai-responses.ts";`,
`export { streamSimple as openaiCompletions } from "${PI}/packages/ai/src/api/openai-completions.ts";`,
`export { composeModelProvider } from "${PI}/packages/coding-agent/src/core/provider-composer.ts";`,
`export { resolveConfigValueOrThrow } from "${PI}/packages/coding-agent/src/core/resolve-config-value.ts";`,
].join("\n"),
);
const bundlePath = join(harnessDirectory, "bundle.mjs");
@@ -242,6 +243,115 @@ const compatProvider = adapters.composeModelProvider(
);
const compatSpread = compatProvider.getModels()[0].compat;
function jsonSafeJavaScriptValue(value) {
if (typeof value === "string") {
const codeUnits = Array.from({ length: value.length }, (_, index) =>
value.charCodeAt(index),
);
const hasLoneSurrogate = codeUnits.some((unit, index) => {
if (unit >= 0xd800 && unit <= 0xdbff) {
return !(
index + 1 < codeUnits.length &&
codeUnits[index + 1] >= 0xdc00 &&
codeUnits[index + 1] <= 0xdfff
);
}
if (unit >= 0xdc00 && unit <= 0xdfff) {
return !(
index > 0 &&
codeUnits[index - 1] >= 0xd800 &&
codeUnits[index - 1] <= 0xdbff
);
}
return false;
});
return hasLoneSurrogate
? {
$javascriptStringUtf16: codeUnits.map((unit) =>
unit.toString(16).padStart(4, "0"),
),
}
: value;
}
if (Array.isArray(value)) {
return value.map(jsonSafeJavaScriptValue);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [
key,
jsonSafeJavaScriptValue(child),
]),
);
}
return value;
}
function captureCompatSpread(label, baseValue, overlayValue) {
const providerId = `compat-${label}`;
const provider = adapters.composeModelProvider(
providerId,
undefined,
{
getProvider(candidate) {
if (candidate !== providerId) return undefined;
return {
api: "openai-responses",
baseUrl: "https://compat.example/v1",
apiKey: "literal",
compat: { chatTemplateKwargs: baseValue },
models: [{ id: "m" }],
modelOverrides: {
m: { compat: { chatTemplateKwargs: overlayValue } },
},
};
},
},
undefined,
);
return {
label,
baseValue,
overlayValue,
result: jsonSafeJavaScriptValue(
provider.getModels()[0].compat.chatTemplateKwargs,
),
};
}
const compatEdgeCases = [
captureCompatSpread("ascii-string-to-string", "ab", "cd"),
captureCompatSpread("astral-string-to-object", "😀", { named: true }),
captureCompatSpread("string-to-array", "ab", ["first", "second"]),
];
const resolverInputs = [
"literal-secret",
"cash$money",
"café$literal",
"$$literal-$!bang",
"prefix-${PI_CAPTURE_MISSING}-suffix",
];
const resolverCases = resolverInputs.map((input) => {
try {
return {
input,
status: "success",
result: adapters.resolveConfigValueOrThrow(
input,
"transport capture",
{},
),
};
} catch (error) {
return {
input,
status: "error",
error: error instanceof Error ? error.message : String(error),
};
}
});
server.close();
console.log(
JSON.stringify(
@@ -252,6 +362,8 @@ console.log(
baseUrl,
results,
compatSpread,
compatEdgeCases,
resolverCases,
},
null,
2,
+178 -12
View File
@@ -144,6 +144,7 @@ fn path_segments(path: &SynPath) -> Vec<String> {
struct ArchitectureVisitor<'a> {
path: &'a str,
source_module: Option<&'static str>,
violations: Vec<Violation>,
internal_edges: BTreeSet<String>,
}
@@ -159,7 +160,7 @@ fn pi_config_source_module(path: &str) -> Option<&'static str> {
impl ArchitectureVisitor<'_> {
fn record_dependency(&mut self, segments: &[String]) {
let Some(source_module) = pi_config_source_module(self.path) else {
let Some(source_module) = self.source_module else {
return;
};
let qualified_internal_path = segments.len() > 1
@@ -346,6 +347,14 @@ fn provider_dml_path_allowed(path: &str) -> bool {
}
fn scan_source(path: &str, source: &str) -> (Vec<Violation>, BTreeSet<String>) {
scan_source_as_module(path, source, pi_config_source_module(path))
}
fn scan_source_as_module(
path: &str,
source: &str,
source_module: Option<&'static str>,
) -> (Vec<Violation>, BTreeSet<String>) {
let syntax = match syn::parse_file(source) {
Ok(syntax) => syntax,
Err(error) => {
@@ -366,6 +375,7 @@ fn scan_source(path: &str, source: &str) -> (Vec<Violation>, BTreeSet<String>) {
}
let mut visitor = ArchitectureVisitor {
path,
source_module,
violations: Vec::new(),
internal_edges: BTreeSet::new(),
};
@@ -394,23 +404,142 @@ fn rust_sources(root: &Path) -> Vec<PathBuf> {
output
}
fn module_path_attribute(attrs: &[Attribute]) -> Option<PathBuf> {
attrs.iter().find_map(|attribute| {
if !attribute.path().is_ident("path") {
return None;
}
let Meta::NameValue(value) = &attribute.meta else {
return None;
};
let syn::Expr::Lit(expression) = &value.value else {
return None;
};
let Lit::Str(path) = &expression.lit else {
return None;
};
Some(PathBuf::from(path.value()))
})
}
fn collect_path_module_targets(items: &[Item], source_path: &Path, output: &mut Vec<PathBuf>) {
for item in items {
let Item::Mod(module) = item else {
continue;
};
if is_cfg_test(&module.attrs) {
continue;
}
if let Some(relative) = module_path_attribute(&module.attrs) {
output.push(
source_path
.parent()
.expect("Rust source has a parent directory")
.join(relative),
);
}
if let Some((_, nested)) = &module.content {
collect_path_module_targets(nested, source_path, output);
}
}
}
fn inherited_module_owners(
sources: &[(PathBuf, String, String)],
) -> BTreeMap<PathBuf, BTreeSet<&'static str>> {
let mut owners = BTreeMap::<PathBuf, BTreeSet<&'static str>>::new();
for (path, relative, _) in sources {
if let Some(owner) = pi_config_source_module(relative) {
owners
.entry(fs::canonicalize(path).expect("canonicalize Rust source"))
.or_default()
.insert(owner);
}
}
loop {
let mut changed = false;
for (path, _, source) in sources {
let canonical = fs::canonicalize(path).expect("canonicalize Rust source");
let source_owners = owners.get(&canonical).cloned().unwrap_or_default();
if source_owners.is_empty() {
continue;
}
let Ok(syntax) = syn::parse_file(source) else {
continue;
};
if is_cfg_test(&syntax.attrs) {
continue;
}
let mut targets = Vec::new();
collect_path_module_targets(&syntax.items, path, &mut targets);
for target in targets {
let Ok(target) = fs::canonicalize(target) else {
continue;
};
let target_owners = owners.entry(target).or_default();
let before = target_owners.len();
target_owners.extend(source_owners.iter().copied());
changed |= target_owners.len() != before;
}
}
if !changed {
return owners;
}
}
}
fn scan_production_tree(
manifest_dir: &Path,
source_root: &Path,
) -> (Vec<Violation>, BTreeSet<String>) {
let mut violations = Vec::new();
let mut edges = BTreeSet::new();
for path in rust_sources(source_root) {
let relative = path
.strip_prefix(manifest_dir)
.expect("source is under manifest")
.to_string_lossy()
.replace('\\', "/");
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
let (mut source_violations, source_edges) = scan_source(&relative, &source);
violations.append(&mut source_violations);
edges.extend(source_edges);
let sources = rust_sources(source_root)
.into_iter()
.map(|path| {
let relative = path
.strip_prefix(manifest_dir)
.expect("source is under manifest")
.to_string_lossy()
.replace('\\', "/");
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read {}: {error}", path.display()));
(path, relative, source)
})
.collect::<Vec<_>>();
let owners = inherited_module_owners(&sources);
let scanned_paths = sources
.iter()
.map(|(path, _, _)| fs::canonicalize(path).expect("canonicalize Rust source"))
.collect::<BTreeSet<_>>();
for (target, target_owners) in &owners {
if !scanned_paths.contains(target) {
violations.push(Violation {
kind: "module_path_outside_scan",
path: target.display().to_string(),
detail: format!(
"a #[path] module owned by {} escapes the scanned source tree",
target_owners.iter().copied().collect::<Vec<_>>().join(",")
),
});
}
}
for (path, relative, source) in sources {
let canonical = fs::canonicalize(&path).expect("canonicalize Rust source");
let inherited = owners.get(&canonical).cloned().unwrap_or_default();
if inherited.is_empty() {
let (mut source_violations, source_edges) = scan_source(&relative, &source);
violations.append(&mut source_violations);
edges.extend(source_edges);
} else {
for owner in inherited {
let (mut source_violations, source_edges) =
scan_source_as_module(&relative, &source, Some(owner));
violations.append(&mut source_violations);
edges.extend(source_edges);
}
}
}
(violations, edges)
}
@@ -670,4 +799,41 @@ fn architecture_scanner_negative_fixtures_prove_each_guard_fires() {
{expected_kind}: {violations:?}"
);
}
let custom_path_root = tempfile::tempdir().expect("temp custom-path architecture tree");
let composer = custom_path_root.path().join("src/pi_config/composer.rs");
let shared = custom_path_root.path().join("src/shared/helper.rs");
fs::create_dir_all(composer.parent().expect("composer parent")).expect("create pi_config");
fs::create_dir_all(shared.parent().expect("shared parent")).expect("create shared");
fs::write(
&composer,
r#"
#[path = "../shared/helper.rs"]
mod helper;
"#,
)
.expect("write custom-path parent");
fs::write(
&shared,
r#"
use crate::pi_config::gateway::PiGatewayApiFamily;
fn production_escape_attempt() {
save_provider();
}
"#,
)
.expect("write custom-path child");
let (violations, _) = scan_production_tree(
custom_path_root.path(),
&custom_path_root.path().join("src"),
);
for expected_kind in ["cross_layer_import", "forbidden_provider_symbol"] {
assert!(
violations.iter().any(|violation| {
violation.kind == expected_kind && violation.path.ends_with("src/shared/helper.rs")
}),
"#[path] modules must inherit composer ownership and trigger \
{expected_kind}: {violations:?}"
);
}
}
+69 -3
View File
@@ -65,6 +65,7 @@ pub(crate) enum PiComposerReasonCode {
MissingEffectiveApi,
MissingEffectiveEndpoint,
NonPositiveModelLimit,
UnrepresentableCompat,
CompositionFailed,
}
@@ -165,6 +166,21 @@ impl PiNativeComposition {
}],
}
}
fn unknown(code: PiComposerReasonCode, pointer: impl Into<String>) -> Self {
Self {
status: PiComposerStatus::Unknown,
provider_id: None,
provider_name: None,
provider_base_url: None,
models: Vec::new(),
ignored_override_keys: Vec::new(),
reasons: vec![PiComposerReason {
code,
json_pointer: pointer.into(),
}],
}
}
}
pub(super) fn compose_explicit_custom_catalog(
@@ -274,6 +290,16 @@ pub(super) fn compose_explicit_custom_catalog(
}
}
let compat =
match merge_pi_compat(provider_compat.clone(), definition.get("compat").cloned()) {
Ok(compat) => compat,
Err(_) => {
return PiNativeComposition::unknown(
PiComposerReasonCode::UnrepresentableCompat,
format!("/models/{index}/compat"),
)
}
};
let model = PiComposedNativeModel {
id: id.to_string(),
name: definition
@@ -305,7 +331,7 @@ pub(super) fn compose_explicit_custom_catalog(
headers: BTreeMap::new(),
provider_headers: provider_header_entries.clone(),
model_headers: Vec::new(),
compat: merge_pi_compat(provider_compat.clone(), definition.get("compat").cloned()),
compat,
api_key: api_key.clone(),
oauth: oauth.clone(),
auth_header,
@@ -400,8 +426,18 @@ pub(super) fn compose_explicit_custom_catalog(
if let Some(max_tokens) = model_override.get("maxTokens") {
model.max_tokens = max_tokens.clone();
}
model.compat =
merge_pi_compat(model.compat.clone(), model_override.get("compat").cloned());
model.compat = match merge_pi_compat(
model.compat.clone(),
model_override.get("compat").cloned(),
) {
Ok(compat) => compat,
Err(_) => {
return PiNativeComposition::unknown(
PiComposerReasonCode::UnrepresentableCompat,
format!("/modelOverrides/{}/compat", escape_json_pointer(&model.id)),
)
}
};
model.override_extra = unknown_fields(model_override, OVERRIDE_FIELDS);
}
}
@@ -830,6 +866,36 @@ mod tests {
);
}
#[test]
fn compat_spread_fails_closed_when_pinned_output_requires_lone_surrogates() {
let value = json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"apiKey": "literal",
"compat": {"chatTemplateKwargs": "😀"},
"models": [{"id": "m"}],
"modelOverrides": {
"m": {"compat": {"chatTemplateKwargs": {"named": true}}}
}
});
let raw = evaluate_provider_value(&value);
let composition = compose_explicit_custom_catalog(
"compat-surrogate",
raw.valid_provider.as_ref().expect("raw-valid"),
);
assert_eq!(composition.status, PiComposerStatus::Unknown);
assert_eq!(
composition.reasons,
vec![PiComposerReason {
code: PiComposerReasonCode::UnrepresentableCompat,
json_pointer: "/modelOverrides/m/compat".to_string(),
}],
"capture records UTF-16 d83d/de00 as two lone-surrogate values, which \
serde_json::Value cannot represent"
);
}
#[test]
fn header_layers_retain_runtime_precedence_and_source_pointers() {
let value = json!({
+49 -17
View File
@@ -21,22 +21,34 @@ const PI_COMPAT_NESTED_SPREAD_KEYS: [&str; 3] = [
"chatTemplateKwargs",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PiCompatMergeError;
/// Mirror pinned Pi's `mergeCompat` JavaScript object-spread semantics.
///
/// Arrays expose numeric enumerable properties, strings expose character
/// properties, objects expose their own fields, and the remaining JSON
/// primitives expose none. Existing key positions are retained when an
/// overlay replaces their values, matching object spread.
fn merge_pi_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value> {
///
/// A JavaScript string is indexed by UTF-16 code unit. Spreading an astral
/// character therefore creates lone-surrogate string values, which cannot be
/// represented by Rust `String` or `serde_json::Value`. That shape is rejected
/// explicitly so callers can fail closed instead of emitting a different
/// composed model.
fn merge_pi_compat(
base: Option<Value>,
overlay: Option<Value>,
) -> Result<Option<Value>, PiCompatMergeError> {
let Some(overlay) = overlay else {
return base;
return Ok(base);
};
if !javascript_truthy(&overlay) {
return base;
return Ok(base);
}
let mut merged = javascript_object_spread(base.as_ref());
merged.extend(javascript_object_spread(Some(&overlay)));
let mut merged = javascript_object_spread(base.as_ref())?;
merged.extend(javascript_object_spread(Some(&overlay))?);
for key in PI_COMPAT_NESTED_SPREAD_KEYS {
let base_value = javascript_property(base.as_ref(), key);
@@ -44,12 +56,12 @@ fn merge_pi_compat(base: Option<Value>, overlay: Option<Value>) -> Option<Value>
if base_value.is_some_and(javascript_is_object)
|| overlay_value.is_some_and(javascript_is_object)
{
let mut nested = javascript_object_spread(base_value);
nested.extend(javascript_object_spread(overlay_value));
let mut nested = javascript_object_spread(base_value)?;
nested.extend(javascript_object_spread(overlay_value)?);
merged.insert(key.to_string(), Value::Object(nested));
}
}
Some(Value::Object(merged))
Ok(Some(Value::Object(merged)))
}
fn javascript_truthy(value: &Value) -> bool {
@@ -69,21 +81,29 @@ fn javascript_property<'a>(value: Option<&'a Value>, key: &str) -> Option<&'a Va
value.and_then(Value::as_object)?.get(key)
}
fn javascript_object_spread(value: Option<&Value>) -> Map<String, Value> {
match value {
fn javascript_object_spread(
value: Option<&Value>,
) -> Result<Map<String, Value>, PiCompatMergeError> {
let spread = match value {
Some(Value::Object(object)) => object.clone(),
Some(Value::Array(values)) => values
.iter()
.enumerate()
.map(|(index, value)| (index.to_string(), value.clone()))
.collect(),
Some(Value::String(value)) => value
.chars()
.enumerate()
.map(|(index, value)| (index.to_string(), Value::String(value.to_string())))
.collect(),
Some(Value::String(value)) => {
if value.chars().any(|character| character.len_utf16() != 1) {
return Err(PiCompatMergeError);
}
value
.chars()
.enumerate()
.map(|(index, value)| (index.to_string(), Value::String(value.to_string())))
.collect()
}
Some(Value::Null | Value::Bool(_) | Value::Number(_)) | None => Map::new(),
}
};
Ok(spread)
}
#[cfg(test)]
@@ -105,6 +125,7 @@ mod compat_spread_tests {
"overlayOnly": true
})),
)
.expect("representable compat spread")
.expect("truthy overlay produces an object");
assert_eq!(
@@ -121,6 +142,17 @@ mod compat_spread_tests {
#[test]
fn compat_falsy_overlay_returns_base_without_spreading() {
let base = Some(json!({"openRouterRouting": ["kept"]}));
assert_eq!(merge_pi_compat(base.clone(), Some(Value::Null)), base);
assert_eq!(merge_pi_compat(base.clone(), Some(Value::Null)), Ok(base));
}
#[test]
fn compat_spread_rejects_unrepresentable_javascript_surrogates() {
assert_eq!(
merge_pi_compat(
Some(json!({"chatTemplateKwargs": "😀"})),
Some(json!({"chatTemplateKwargs": {"named": true}})),
),
Err(PiCompatMergeError)
);
}
}
+50 -2
View File
@@ -311,6 +311,10 @@ pub(crate) enum PiConfigError {
UnknownModelOverride(String),
#[error("Pi compat at '{json_pointer}' must be an object")]
InvalidCompat { json_pointer: String },
#[error(
"Pi compat at '{json_pointer}' requires JavaScript UTF-16 values that cannot be represented"
)]
UnrepresentableCompat { json_pointer: String },
#[error("Pi {field} cannot be empty when present")]
EmptyOptionalField { field: &'static str },
#[error("Pi model '{model_id}' {field} must be greater than zero")]
@@ -401,6 +405,7 @@ pub(crate) enum PiReasonCode {
MissingEffectiveEndpoint,
InvalidEndpoint,
InvalidCompat,
UnrepresentableCompat,
NonPositiveModelLimit,
InvalidThinkingLevel,
CompositionFailed,
@@ -552,10 +557,18 @@ fn effective_pi_model_unchecked(
.map(|entry| apply_cost_override(base_cost.clone(), entry))
.unwrap_or(base_cost);
let compat = merge_pi_compat(provider.compat.clone(), model.compat.clone()).map_err(|_| {
PiConfigError::UnrepresentableCompat {
json_pointer: "/compat".to_string(),
}
})?;
let compat = merge_pi_compat(
merge_pi_compat(provider.compat.clone(), model.compat.clone()),
compat,
model_override.and_then(|entry| entry.compat.clone()),
);
)
.map_err(|_| PiConfigError::UnrepresentableCompat {
json_pointer: format!("/modelOverrides/{}/compat", escape_json_pointer(&model.id)),
})?;
Ok(PiEffectiveModel {
id: model.id.clone(),
@@ -908,6 +921,27 @@ mod tests {
);
}
#[test]
fn effective_compat_rejects_unrepresentable_javascript_surrogate_spread() {
let config: PiManagedProviderConfig = serde_json::from_value(json!({
"api": "openai-responses",
"baseUrl": "https://compat.example/v1",
"compat": {"chatTemplateKwargs": "😀"},
"models": [{"id": "m"}],
"modelOverrides": {
"m": {"compat": {"chatTemplateKwargs": {"named": true}}}
}
}))
.expect("deserialize pinned compat vector");
assert_eq!(
effective_pi_model(&config, "m"),
Err(PiConfigError::UnrepresentableCompat {
json_pointer: "/modelOverrides/m/compat".to_string(),
})
);
}
#[test]
fn fractional_pi_numbers_survive_managed_round_trip() {
let mut fractional = model("fractional");
@@ -1092,5 +1126,19 @@ mod tests {
"jsonPointer": "/apiKey"
})
);
let compat_reason = PiDiagnosticReason::new(
PiDiagnosticLayer::Composition,
PiReasonCode::UnrepresentableCompat,
Some("/modelOverrides/m/compat".into()),
);
assert_eq!(
serde_json::to_value(compat_reason).expect("serialize"),
json!({
"layer": "composition",
"code": "unrepresentable_compat",
"jsonPointer": "/modelOverrides/m/compat"
})
);
}
}
+53
View File
@@ -466,6 +466,13 @@ fn collect_managed_reasons(config: &PiManagedProviderConfig) -> Vec<PiDiagnostic
}
fn managed_validation_reason(error: PiConfigError) -> PiDiagnosticReason {
if let PiConfigError::UnrepresentableCompat { json_pointer } = &error {
return diagnostic_reason(
PiDiagnosticLayer::Managed,
PiReasonCode::UnrepresentableCompat,
json_pointer,
);
}
let (code, pointer) = match error {
PiConfigError::ProviderHasNoModels => (PiReasonCode::MissingExplicitModels, "/models"),
PiConfigError::EmptyApiId => (PiReasonCode::ManagedTypeConversionFailed, "/api"),
@@ -481,6 +488,9 @@ fn managed_validation_reason(error: PiConfigError) -> PiDiagnosticReason {
(PiReasonCode::UnknownModelOverride, "/modelOverrides")
}
PiConfigError::InvalidCompat { .. } => (PiReasonCode::InvalidCompat, "/compat"),
PiConfigError::UnrepresentableCompat { .. } => {
unreachable!("handled before the exhaustive mapping")
}
PiConfigError::EmptyOptionalField { .. } => (PiReasonCode::EmptyOptionalField, ""),
PiConfigError::NonPositiveModelLimit { .. } => {
(PiReasonCode::NonPositiveModelLimit, "/models")
@@ -585,6 +595,9 @@ fn map_composer_reasons(composition: &PiNativeComposition) -> Vec<PiDiagnosticRe
PiComposerReasonCode::NonPositiveModelLimit => {
PiReasonCode::NonPositiveModelLimit
}
PiComposerReasonCode::UnrepresentableCompat => {
PiReasonCode::UnrepresentableCompat
}
PiComposerReasonCode::CompositionFailed => PiReasonCode::CompositionFailed,
},
&reason.json_pointer,
@@ -760,6 +773,46 @@ mod tests {
assert_eq!(malformed.gateway_status, PiGatewayStatus::Unknown);
}
#[test]
fn public_inspection_fails_closed_for_unrepresentable_compat_spread() {
let temp = tempfile::tempdir().expect("tempdir");
let path = temp.path().join("models.json");
fs::write(
&path,
r#"{"providers":{"surrogate":{
"api":"openai-responses",
"baseUrl":"https://compat.example/v1",
"apiKey":"literal",
"compat":{"chatTemplateKwargs":"😀"},
"models":[{"id":"m"}],
"modelOverrides":{"m":{"compat":{"chatTemplateKwargs":{"named":true}}}}
}}}"#,
)
.expect("write");
let diagnostic =
&PiNativeInspectionService::inspect_catalog(&path, &BTreeMap::new()).unwrap()[0];
assert_eq!(diagnostic.raw_validity, PiRawNativeValidity::Valid);
assert_eq!(
diagnostic.managed_assessment,
PiManagedAssessment::Unsupported
);
assert_eq!(diagnostic.composition_status, PiCompositionStatus::Unknown);
assert_eq!(diagnostic.gateway_status, PiGatewayStatus::Unknown);
assert!(has_reason(
diagnostic,
PiDiagnosticLayer::Managed,
PiReasonCode::UnrepresentableCompat,
"/modelOverrides/m/compat"
));
assert!(has_reason(
diagnostic,
PiDiagnosticLayer::Composition,
PiReasonCode::UnrepresentableCompat,
"/modelOverrides/m/compat"
));
}
#[test]
fn managed_rejection_does_not_control_raw_composition() {
let temp = tempfile::tempdir().expect("tempdir");