ref:main

test: cover the runner's untested surface, and fix what the tests found #58

merged colechristensen cole.christensen@gmail.com wants to merge 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_name asserted only is_err(), so its three distinct rules could have collapsed into one unhelpful message undetected. Each is now pinned to the diagnostic it produces.
  • verify_sha256 gained 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.rs so 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 — clean
  • cargo fmt applied
  • 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

Created Aug 05, 2026 at 05:57 UTC | Merged Aug 05, 2026 at 16:19 UTC by colechristensen cole.christensen@gmail.com