test: cover the runner's untested surface, and fix what the tests found #58
test/coverage-pass
into main
Coverage pass over anvil-cli, run against a written standard (docs/TESTING.md) rather than at the coverage number.
Numbers
| before | after | |
|---|---|---|
| Regions | 64.15% | 71.83% |
| Lines | 65.88% | 72.74% |
| Functions | 73.35% | 81.71% |
| Lib tests | 232 | 364 |
Five modules were at literal 0%.
| module | before | after |
|---|---|---|
runner/artifacts.rs |
0% | 97.63% |
runner/heartbeat.rs |
0% | 95.25% |
runner/inference.rs |
0% | 76.38% |
platform/unix.rs |
38.24% | 94.48% |
runner/config.rs |
66.67% | 95.68% |
runner/service_manager.rs |
37.91% | 70.42% |
commands/auth.rs |
51.99% | 71.91% |
Three defects, each reproduced before it was fixed
Runner token written world-readable. RunnerConfig::save used a plain fs::write — 0644 under a default umask. The file holds runner_token, which can claim and report CI jobs, so on a shared runner host every account could read it. The user config in src/config.rs has always used create-at-0600-then-rename; the runner config now does too, which additionally tightens a file an older build left permissive and makes the save atomic.
Artifact paths could escape the workspace. resolve_paths did workspace.join(spec.path), and Path::join treats an absolute argument as a replacement, not an append — so a spec of /etc/passwd resolved to exactly that, and ../ climbed out just as easily. Artifact specs come from the built branch’s .anvil.yml, so anyone able to open a pull request could have any runner-readable file uploaded to the server as a build artifact. Results are now confined to the workspace, canonicalizing both sides so a planted symlink can’t smuggle a file out either.
Worth a look on this one: it is a semantics change to a user-facing config surface. A pipeline that names an absolute artifact path now gets a loud Refusing '<path>': ... is outside the job workspace and no upload, where before it silently worked.
A blank line in SHA256SUMS disabled update verification. parse_checksum_line used ? on a line’s first token, which returns from the whole function rather than skipping the line. One blank or comment line hid every entry after it; the update then failed with no SHA256 entry, and the documented way past that is --no-verify. A stray newline could talk a user out of verifying the downloaded binary at all.
Quality work on already-green tests
validate_instance_nameasserted onlyis_err(), so its three distinct rules could have collapsed into one unhelpful message undetected. Each is now pinned to the diagnostic it produces.verify_sha256gained the rejection cases a supply-chain check needs: hash prefix, empty expected hash, and that the failure reports both digests.- Split a compound assert in
prepare.rsso a failure names which half broke.
Deliberately still at 0%
runner/shutdown.rs installs process-wide signal handlers and runner/detach.rs daemonizes. Exercising either from the test process means raising real signals at, or forking, the test runner itself — the tests would be less trustworthy than the gap.
Verification
cargo test— 364 lib + 251 integration, green- Stable across repeated runs and under
--test-threads=1 cargo clippy --all-targets --all-features -- -D warnings— cleancargo fmtapplied- One test module went from 30.1s to 0.21s: it had been pointed at
127.0.0.1:1, which WSL2 black-holes rather than refuses, so every probe burned a full timeout. Unreachable-endpoint tests now bind and drop an ephemeral port.
🤖 Generated with Claude Code