I have been able to narrow this down to a subprocess that we spawn in one of the tests (via subprocess.Popen). When it does not exit cleanly and is forcefully terminated, this causes Travis CI to fail the job. Our test still passes in this particular case, and so it is unclear why Travis CI thinks the build failed.
The fix that we settled on was to send SIGBREAK (CTRL+BREAK) to the process. SIGINT (CTRL+C) did not work on Travis, although it did seem to work on a desktop Windows box (this might be due to bash, but I can’t say for sure). The exit code for the process is still non-zero when using SIGBREAK to terminate it, but Travis seems to like this approach better for some reason vs. what we were doing before, and flags the job as having succeeded.
Regardless, it still seems like there is a bug in the job “succeeded/failed” logic used by Travis; IMO it should only depend on the exit code of the script command (in this case, tox).