Preserve precedence across feature requirement aliases (#42863)

## Why

Requirements layers may use either `features` or its `feature_requirements`
alias. Treating those names as separate tables prevents feature settings from
merging consistently across layers.

## What changed

Normalize `feature_requirements` to `features` after validating each layer so
mixed aliases share one merge path and retain layer precedence.

## Testing

Add coverage for both alias orderings across low- and high-priority layers.

GitOrigin-RevId: e23ad3e0853b8b49ca10447875f33706de39bcfe
This commit is contained in:
zm-oai
2026-09-04 21:53:08 +00:00
committed by copyberry
parent 3921a30d6b
commit d2d5b70241
2 changed files with 36 additions and 0 deletions
@@ -102,6 +102,13 @@ impl ComposableRequirementsLayer {
(regular_toml, requirements)
};
// Merge aliases under one key after validating each individual layer.
if let Some(table) = regular_toml.as_table_mut()
&& let Some(features) = table.remove("feature_requirements")
{
table.insert("features".to_string(), features);
}
// Hostname lookup is configuration-driven and may block on DNS, so only
// resolve it when this layer contains hostname-based sandbox selectors.
let hostname = requirements
@@ -469,6 +469,35 @@ approval_mode = "approve"
);
}
#[test]
fn feature_aliases_merge_with_layer_precedence() {
for (low_key, high_key) in [
("features", "feature_requirements"),
("feature_requirements", "features"),
] {
let composed = compose(vec![
layer(
"req_low",
"Low",
&format!("[{low_key}]\nchronicle = true\nshell_snapshot = false"),
),
layer(
"req_high",
"High",
&format!("[{high_key}]\nchronicle = false\napps = false"),
),
])
.expect("compose mixed feature aliases");
assert_eq!(
composed,
Some(expected_requirements(
"[features]\nchronicle = false\nshell_snapshot = false\napps = false"
))
);
}
}
#[test]
fn merged_table_source_is_composite_in_priority_order() {
let high_source = RequirementSource::EnterpriseManaged {