UnicodeDecodeError is a ValueError, not an OSError, so a build_info.json
with invalid UTF-8 bytes still crashed the compile; widening the tuple to
(ValueError, OSError) also keeps JSONDecodeError covered.
The staleness tests now use a two-run steady-state shape so the assertion
can only pass when the comparison itself fires: run once to reach steady
state, then damage the file (or bump the hash/version) and assert the
second run regenerates it. The version test pins version.h so the bump is
visible only to the JSON comparison; the non-UTF-8 test stubs the writes
because regenerating over the damaged file needs write_file_if_changed's
own recovery, which lands separately.