Let’s look at this logically, using quotes from the GNU man page dated 2017-09-15, as included in Debian 10:
If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.
As you correctly state (sort of), n == 5. So what is the “length of src”? As we should all know, C strings are null-terminated. The man page hints at this:
The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0') … The strncpy() function is similar, except that at most n bytes of src are copied.
Your string is not null-terminated, though. So after reading the 4 bytes you defined, strncpy tries to read a fifth byte, and what does it find? The first byte of dest, apparently (actually, this is implementation-dependent, as far as I know). It still has not found a null to terminate the string, so does it keep reading? No, because as stated above:
a total of n bytes are written
and
at most n bytes of src are copied.
So it copies the 5 bytes "abcdq" into dest.
You said:
other 4 characters should be copied in any case, and they should be \0s
This implies a total length of 8 bytes. Where would you get this from? This is the “length” (or at least declared array length) of dest, not src, and in any case would be overridden by the fact that n < 8.
The case where src is null-terminated is straightforward.
So no, the implementation is not faulty.
Actually, you got lucky:
The strings may not overlap
They do here, so, technically, anything could happen. Indeed, a comment on the question says that Valgrind warns about this.
and the destination string dest must be large enough to receive the copy. Beware of buffer overruns!
This carelessness with string lengths should be a warning to take extra care, before you create a buffer overrun.
On another note, while it is possible that a function like strncpy would have a bug, it is extremely unlikely. Standard implementations have been thoroughly tested in a wide variety of environments. Any apparent bug is much more likely to a bug in your own code, or a lack of understanding of your own code, as is the case here.