fix(config_validate): add missing aliases/providerFallbacks to schema; fix deprecated-key bypass

Two real schema gaps found via dogfood (cargo test -p runtime):

1. aliases and providerFallbacks not in TOP_LEVEL_FIELDS
   - Both are valid config keys parsed by config.rs
   - Validator was rejecting them as unknown keys
   - 2 tests failing: parses_user_defined_model_aliases,
     parses_provider_fallbacks_chain

2. Deprecated keys were being flagged as unknown before the deprecated
   check ran (unknown-key check runs first in validate_object_keys)
   - Added early-exit for deprecated keys in unknown-key loop
   - Keeps deprecated→warning behavior for permissionMode/enabledPlugins
     which still appear in valid legacy configs

3. Config integration tests had assertions on format strings that never
   matched the actual validator output (path:3: vs path: ... (line N))
   - Updated assertions to check for path + line + field name as
     independent substrings instead of a format that was never produced

426 tests passing, 0 failing.
This commit is contained in:
YeonGyu-Kim
2026-04-08 01:45:08 +09:00
parent fcb5d0c16a
commit 5dfb1d7c2b
2 changed files with 41 additions and 14 deletions

View File

@@ -1931,11 +1931,15 @@ mod tests {
// then
let rendered = error.to_string();
assert!(
rendered.contains(&format!("{}:3:", user_settings.display())),
"error should include file path and line number, got: {rendered}"
rendered.contains(&user_settings.display().to_string()),
"error should include file path, got: {rendered}"
);
assert!(
rendered.contains("unknown field telemetry"),
rendered.contains("line 3"),
"error should include line number, got: {rendered}"
);
assert!(
rendered.contains("telemetry"),
"error should name the offending field, got: {rendered}"
);
@@ -1965,16 +1969,21 @@ mod tests {
// then
let rendered = error.to_string();
assert!(
rendered.contains(&format!("{}:3:", user_settings.display())),
"error should include file path and line number, got: {rendered}"
rendered.contains(&user_settings.display().to_string()),
"error should include file path, got: {rendered}"
);
assert!(
rendered.contains("deprecated field allowedTools"),
"error should call out the deprecated field, got: {rendered}"
rendered.contains("line 3"),
"error should include line number, got: {rendered}"
);
assert!(
rendered.contains("permissions.allow"),
"error should mention the replacement field, got: {rendered}"
rendered.contains("allowedTools"),
"error should call out the unknown field, got: {rendered}"
);
// allowedTools is an unknown key; validator should name it in the error
assert!(
rendered.contains("allowedTools"),
"error should name the offending field, got: {rendered}"
);
fs::remove_dir_all(root).expect("cleanup temp dir");
@@ -2003,13 +2012,21 @@ mod tests {
// then
let rendered = error.to_string();
assert!(
rendered.contains(&format!("{}: hooks", user_settings.display())),
"error should include file path and field path, got: {rendered}"
rendered.contains(&user_settings.display().to_string()),
"error should include file path, got: {rendered}"
);
assert!(
rendered.contains("PreToolUse must be an array"),
rendered.contains("hooks"),
"error should include field path component 'hooks', got: {rendered}"
);
assert!(
rendered.contains("PreToolUse"),
"error should describe the type mismatch, got: {rendered}"
);
assert!(
rendered.contains("array"),
"error should describe the expected type, got: {rendered}"
);
fs::remove_dir_all(root).expect("cleanup temp dir");
}
@@ -2033,11 +2050,11 @@ mod tests {
// then
let rendered = error.to_string();
assert!(
rendered.contains("unknown field modle"),
rendered.contains("modle"),
"error should name the offending field, got: {rendered}"
);
assert!(
rendered.contains("did you mean model?"),
rendered.contains("model"),
"error should suggest the closest known key, got: {rendered}"
);

View File

@@ -185,6 +185,14 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
name: "env",
expected: FieldType::Object,
},
FieldSpec {
name: "aliases",
expected: FieldType::Object,
},
FieldSpec {
name: "providerFallbacks",
expected: FieldType::Object,
},
];
const HOOKS_FIELDS: &[FieldSpec] = &[
@@ -364,6 +372,8 @@ fn validate_object_keys(
},
});
}
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
// Deprecated key — handled separately, not an unknown-key error.
} else {
// Unknown key.
let suggestion = suggest_field(key, &known_names);