mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-09 05:36:45 +00:00
fix: preserve runtime config validation compatibility
This commit is contained in:
@@ -92,6 +92,8 @@ enum FieldType {
|
|||||||
Bool,
|
Bool,
|
||||||
Object,
|
Object,
|
||||||
StringArray,
|
StringArray,
|
||||||
|
HookArray,
|
||||||
|
RulesImport,
|
||||||
Number,
|
Number,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,8 @@ impl FieldType {
|
|||||||
Self::Bool => "a boolean",
|
Self::Bool => "a boolean",
|
||||||
Self::Object => "an object",
|
Self::Object => "an object",
|
||||||
Self::StringArray => "an array of strings",
|
Self::StringArray => "an array of strings",
|
||||||
|
Self::RulesImport => "a string or an array of strings",
|
||||||
|
Self::HookArray => "an array of strings or hook objects",
|
||||||
Self::Number => "a number",
|
Self::Number => "a number",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +118,13 @@ impl FieldType {
|
|||||||
Self::StringArray => value
|
Self::StringArray => value
|
||||||
.as_array()
|
.as_array()
|
||||||
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some())),
|
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some())),
|
||||||
|
Self::HookArray => true,
|
||||||
|
Self::RulesImport => {
|
||||||
|
value.as_str().is_some()
|
||||||
|
|| value
|
||||||
|
.as_array()
|
||||||
|
.is_some_and(|arr| arr.iter().all(|v| v.as_str().is_some()))
|
||||||
|
}
|
||||||
Self::Number => value.as_i64().is_some(),
|
Self::Number => value.as_i64().is_some(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +212,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||||||
name: "provider",
|
name: "provider",
|
||||||
expected: FieldType::Object,
|
expected: FieldType::Object,
|
||||||
},
|
},
|
||||||
|
FieldSpec {
|
||||||
|
name: "rulesImport",
|
||||||
|
expected: FieldType::RulesImport,
|
||||||
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "subagentModel",
|
name: "subagentModel",
|
||||||
expected: FieldType::String,
|
expected: FieldType::String,
|
||||||
@@ -210,15 +225,15 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||||||
const HOOKS_FIELDS: &[FieldSpec] = &[
|
const HOOKS_FIELDS: &[FieldSpec] = &[
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PreToolUse",
|
name: "PreToolUse",
|
||||||
expected: FieldType::StringArray,
|
expected: FieldType::HookArray,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PostToolUse",
|
name: "PostToolUse",
|
||||||
expected: FieldType::StringArray,
|
expected: FieldType::HookArray,
|
||||||
},
|
},
|
||||||
FieldSpec {
|
FieldSpec {
|
||||||
name: "PostToolUseFailure",
|
name: "PostToolUseFailure",
|
||||||
expected: FieldType::StringArray,
|
expected: FieldType::HookArray,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -410,9 +425,8 @@ fn validate_object_keys(
|
|||||||
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
|
} else if DEPRECATED_FIELDS.iter().any(|d| d.name == key) {
|
||||||
// Deprecated key — handled separately, not an unknown-key error.
|
// Deprecated key — handled separately, not an unknown-key error.
|
||||||
} else {
|
} else {
|
||||||
// Unknown key.
|
|
||||||
let suggestion = suggest_field(key, &known_names);
|
let suggestion = suggest_field(key, &known_names);
|
||||||
result.errors.push(ConfigDiagnostic {
|
result.warnings.push(ConfigDiagnostic {
|
||||||
path: path_display.to_string(),
|
path: path_display.to_string(),
|
||||||
field: field_path,
|
field: field_path,
|
||||||
line: find_key_line(source, key),
|
line: find_key_line(source, key),
|
||||||
@@ -591,10 +605,11 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].field, "unknownField");
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].field, "unknownField");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
result.errors[0].kind,
|
result.warnings[0].kind,
|
||||||
DiagnosticKind::UnknownKey { .. }
|
DiagnosticKind::UnknownKey { .. }
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -674,9 +689,10 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].line, Some(3));
|
assert_eq!(result.warnings.len(), 1);
|
||||||
assert_eq!(result.errors[0].field, "badKey");
|
assert_eq!(result.warnings[0].line, Some(3));
|
||||||
|
assert_eq!(result.warnings[0].field, "badKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -697,7 +713,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validates_nested_hooks_keys() {
|
fn validates_nested_hooks_keys() {
|
||||||
// given
|
// given
|
||||||
let source = r#"{"hooks": {"PreToolUse": ["cmd"], "BadHook": ["x"]}}"#;
|
let source = r#"{"hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":"cmd"}]}], "BadHook": ["x"]}}"#;
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
let object = parsed.as_object().expect("object");
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
@@ -705,8 +721,64 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
assert!(result.errors.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
result.warnings.len(),
|
||||||
|
1,
|
||||||
|
"expected only the unknown key warning, got {:?}",
|
||||||
|
result.warnings
|
||||||
|
);
|
||||||
|
assert_eq!(result.warnings[0].field, "hooks.BadHook");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_object_style_hook_entries() {
|
||||||
|
let source = r#"{"hooks":{"PreToolUse":["legacy",{"matcher":"Bash","hooks":[{"type":"command","command":"echo ok"}]}]}}"#;
|
||||||
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
|
assert!(result.errors.is_empty(), "{:?}", result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_wrong_hook_entry_types_for_partial_runtime_validation_441() {
|
||||||
|
let source = r#"{"hooks":{"PreToolUse":[42]}}"#;
|
||||||
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
|
assert!(result.errors.is_empty(), "{:?}", result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_rules_import_string_and_array_forms() {
|
||||||
|
for source in [
|
||||||
|
r#"{"rulesImport":"auto"}"#,
|
||||||
|
r#"{"rulesImport":"none"}"#,
|
||||||
|
r#"{"rulesImport":["cursor","copilot"]}"#,
|
||||||
|
] {
|
||||||
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
|
assert!(result.errors.is_empty(), "{source}: {:?}", result.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_rules_import_wrong_type() {
|
||||||
|
let source = r#"{"rulesImport":42}"#;
|
||||||
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
|
let object = parsed.as_object().expect("object");
|
||||||
|
|
||||||
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert_eq!(result.errors.len(), 1);
|
||||||
assert_eq!(result.errors[0].field, "hooks.BadHook");
|
assert_eq!(result.errors[0].field, "rulesImport");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -720,8 +792,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].field, "permissions.denyAll");
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].field, "permissions.denyAll");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -735,8 +808,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].field, "sandbox.containerMode");
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].field, "sandbox.containerMode");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -750,8 +824,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].field, "plugins.autoUpdate");
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].field, "plugins.autoUpdate");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -765,8 +840,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
assert_eq!(result.errors[0].field, "oauth.secret");
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert_eq!(result.warnings[0].field, "oauth.secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -774,7 +850,7 @@ mod tests {
|
|||||||
// given
|
// given
|
||||||
let source = r#"{
|
let source = r#"{
|
||||||
"model": "opus",
|
"model": "opus",
|
||||||
"hooks": {"PreToolUse": ["guard"]},
|
"hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":"guard"}]}]},
|
||||||
"permissions": {"defaultMode": "plan", "allow": ["Read"]},
|
"permissions": {"defaultMode": "plan", "allow": ["Read"]},
|
||||||
"mcpServers": {},
|
"mcpServers": {},
|
||||||
"sandbox": {"enabled": false}
|
"sandbox": {"enabled": false}
|
||||||
@@ -801,8 +877,9 @@ mod tests {
|
|||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert_eq!(result.errors.len(), 1);
|
assert!(result.errors.is_empty());
|
||||||
match &result.errors[0].kind {
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
match &result.warnings[0].kind {
|
||||||
DiagnosticKind::UnknownKey {
|
DiagnosticKind::UnknownKey {
|
||||||
suggestion: Some(s),
|
suggestion: Some(s),
|
||||||
} => assert_eq!(s, "model"),
|
} => assert_eq!(s, "model"),
|
||||||
@@ -813,7 +890,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_diagnostics_includes_all_entries() {
|
fn format_diagnostics_includes_all_entries() {
|
||||||
// given
|
// given
|
||||||
let source = r#"{"permissionMode": "plan", "badKey": 1}"#;
|
let source = r#"{"model": 42, "badKey": 1}"#;
|
||||||
let parsed = JsonValue::parse(source).expect("valid json");
|
let parsed = JsonValue::parse(source).expect("valid json");
|
||||||
let object = parsed.as_object().expect("object");
|
let object = parsed.as_object().expect("object");
|
||||||
let result = validate_config_file(object, source, &test_path());
|
let result = validate_config_file(object, source, &test_path());
|
||||||
@@ -825,7 +902,7 @@ mod tests {
|
|||||||
assert!(output.contains("warning:"));
|
assert!(output.contains("warning:"));
|
||||||
assert!(output.contains("error:"));
|
assert!(output.contains("error:"));
|
||||||
assert!(output.contains("badKey"));
|
assert!(output.contains("badKey"));
|
||||||
assert!(output.contains("permissionMode"));
|
assert!(output.contains("model"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user