diff --git a/internal/validators/constants.go b/internal/validators/constants.go index 90dec391..41c1743c 100644 --- a/internal/validators/constants.go +++ b/internal/validators/constants.go @@ -26,6 +26,7 @@ var ( ErrInvalidNamedArgumentName = errors.New("invalid named argument name format") ErrArgumentValueStartsWithName = errors.New("argument value cannot start with the argument name") ErrArgumentDefaultStartsWithName = errors.New("argument default cannot start with the argument name") + ErrValueHintOnNamedArgument = errors.New("valueHint is only valid for positional arguments, not named arguments") // Server name validation errors ErrMultipleSlashesInServerName = errors.New("server name cannot contain multiple slashes") diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 114ed3c7..5e7007e0 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -404,6 +404,20 @@ func validateArgument(ctx *ValidationContext, obj *model.Argument) *ValidationRe // Validate value and default don't start with the name valueResult := validateArgumentValueFields(ctx, obj.Name, obj.Value, obj.Default) result.Merge(valueResult) + + // valueHint names a positional slot for transport URL variable + // substitution, so it only makes sense on positional arguments. The + // schema allows it on named arguments (the union branches don't set + // additionalProperties: false), so reject it here instead. + if obj.ValueHint != "" { + issue := NewValidationIssueFromError( + ValidationIssueTypeSemantic, + ctx.Field("valueHint").String(), + ErrValueHintOnNamedArgument, + "valuehint-on-named-argument", + ) + result.AddIssue(issue) + } } return result } diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index c3e5585b..35a0dbf0 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -1286,6 +1286,34 @@ func TestValidateArgument_InvalidNamedArgumentNames(t *testing.T) { } } +func TestValidateArgument_ValueHintOnNamedArgument(t *testing.T) { + // valueHint identifies a positional slot, so a named argument must not carry + // one. The schema misses this (the argument union branches allow extra + // properties), so the semantic validator is the enforcement point. + t.Run("named_with_valueHint_rejected", func(t *testing.T) { + arg := model.Argument{ + Type: model.ArgumentTypeNamed, + Name: "--port", + ValueHint: "port_number", + } + server := createValidServerWithArgument(arg) + result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic) + assert.Error(t, result.FirstError(), "named argument carrying valueHint should be rejected") + }) + + // A positional argument with a valueHint stays valid (no regression). + t.Run("positional_with_valueHint_valid", func(t *testing.T) { + arg := model.Argument{ + InputWithVariables: model.InputWithVariables{Input: model.Input{Value: "/etc/config.json"}}, + Type: model.ArgumentTypePositional, + ValueHint: "config_path", + } + server := createValidServerWithArgument(arg) + result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic) + assert.NoError(t, result.FirstError(), "positional argument with valueHint should remain valid") + }) +} + func TestValidateArgument_InvalidValueFields(t *testing.T) { invalidValueCases := []struct { name string