Useless's answer is very complete; but just to simplify it a bit... There are exactly two things you could have meant with your original code. Either you meant to invoke ADL on begin, or you didn't.
If you don't mean to invoke ADL, then you should use a qualified name. You wouldn't write sort(v.begin(), v.end()) when you meant std::sort, so you shouldn't write begin(v) when you mean std::begin(v), either. In fact, you shouldn't write std::begin(v) when you mean v.begin()!
template<class T>
auto test(const std::vector<T>& v) {
return std::begin(v); // Right (no ADL)
}
template<class T>
auto test(const std::vector<T>& v) {
return v.begin(); // Even better (no ADL)
}
If you do mean to invoke ADL, then you must enable ADL by adding a using-declaration. Typically you use ADL when you're writing generic code (templates), and you want the author of T to have some control over what happens in your templatey code. In your actual code snippet, v is just a std::vector<int> with no templatey bits; but for the sake of argument let's pretend that its type is some unknown container type T (perhaps even a native array type, like int[10]). Then we should do the std::swap two-step:
template<class T>
auto test(const T& v) {
using std::begin;
return begin(v); // Right (ADL)
}
These ("qualified" and "ADL two-step") are the only two ways of making non-member function calls in C++ that you need to know; they are designed to work 100% of the time. Your original snippet deviated from the best practice by trying to use ADL without the two-step; that's why it sometimes failed for you.
Note that in C++20 you can (if you like) make a qualified call to std::ranges::begin, which will itself do the ADL two-step for you:
template<class T>
auto test(const T& v) {
return std::ranges::begin(v); // Right (ADL happens internally)
}