test: add MockRuntime and unit tests for command modules#41
Conversation
Add a shared MockRuntime test double behind #[cfg(test)] and unit tests for 5 command modules that previously had zero test coverage. - MockRuntime: configurable test double with queued FIFO responses, call recording, and assertion helpers (22 trait methods) - logs.rs: extract get_logs() inner fn, add 5 tests - stop.rs: extract stop_container() inner fn, add 7 tests - list.rs: extract filter_sessions/format_json/format_plain, add 5 tests - cache.rs: add 4 tests for list/clear/gc subcommands - run/mod.rs: add 2 smoke tests for run_interactive/run_detached
- Use idiomatic bool::then() in MockRuntime::take_response - Remove redundant imports in test modules - Use HashMap::from() literals in cache tests - Extract setup_smoke_test helper to deduplicate run smoke tests
src/orchestration/mock.rs
Outdated
| self.take_unit("ensure_ready") | ||
| } | ||
|
|
||
| async fn run(&self, _config: &ContainerConfig, _command: &[String]) -> MinoResult<String> { |
There was a problem hiding this comment.
BLOCKING: Mock discards arguments
The run and create methods record an empty vec![] for args, discarding both ContainerConfig and command. This makes it impossible to use assert_called_with to verify the correct image, volumes, env vars, or command were passed.
Fix: Record at minimum the image and command:
async fn run(&self, config: &ContainerConfig, command: &[String]) -> MinoResult<String> {
let mut args = vec![config.image.clone()];
args.extend(command.iter().cloned());
self.record("run", args);
self.take_string("run", "mock-container-id")
}(Tests review, B-1)
src/orchestration/mock.rs
Outdated
| "mock" | ||
| } | ||
|
|
||
| async fn volume_create(&self, name: &str, _labels: &HashMap<String, String>) -> MinoResult<()> { |
There was a problem hiding this comment.
BLOCKING: volume_create discards labels
The volume_create method records only the volume name but ignores the labels HashMap entirely. Labels are critical to cache state management. Tests that exercise cache creation flows cannot verify that correct labels were set.
Fix: Serialize labels into the args recording:
async fn volume_create(&self, name: &str, labels: &HashMap<String, String>) -> MinoResult<()> {
let mut args = vec![name.to_string()];
let mut sorted_labels: Vec<_> = labels.iter().collect();
sorted_labels.sort_by_key(|(k, _)| *k);
for (k, v) in sorted_labels {
args.push(format!("{}={}", k, v));
}
self.record("volume_create", args);
self.take_unit("volume_create")
}(Tests review, B-2)
src/orchestration/mock.rs
Outdated
| fn take_response(&self, method: &str) -> Option<MinoResult<MockResponse>> { | ||
| let mut responses = self.responses.lock().unwrap(); | ||
| let queue = responses.get_mut(method)?; | ||
| (!queue.is_empty()).then(|| queue.remove(0)) |
There was a problem hiding this comment.
BLOCKING: Vec::remove(0) is O(n) -- use VecDeque for FIFO
The take_response method uses queue.remove(0) which requires shifting all remaining elements. This is semantically a FIFO dequeue and should use VecDeque for O(1) performance and idiomatic correctness.
Fix:
use std::collections::VecDeque;
// In MockRuntime struct:
responses: Mutex<HashMap<String, VecDeque<MinoResult<MockResponse>>>>,
// In take_response:
fn take_response(&self, method: &str) -> Option<MinoResult<MockResponse>> {
let mut responses = self.responses.lock().unwrap();
let queue = responses.get_mut(method)?;
queue.pop_front()
}
// In on():
.or_default()
.push_back(response);(Performance & Rust review, M-1)
| let mut config = Config::default(); | ||
| config.container.layers = vec!["rust".to_string()]; | ||
| // Clear MINO_LAYERS to avoid interference from environment | ||
| std::env::remove_var("MINO_LAYERS"); |
There was a problem hiding this comment.
BLOCKING: Missing #[serial] on env var tests
The resolve_layer_names_config_layers test calls std::env::remove_var("MINO_LAYERS") but lacks #[serial] annotation. Environment variables are process-global shared mutable state. Concurrent test execution can create data races and test flakiness in CI.
Since the PR introduced serial_test as a dependency and uses #[serial] on the smoke tests, this test should also be serialized for consistency.
Fix:
#[test]
#[serial]
fn resolve_layer_names_config_layers() {
// ...
}(Tests & Rust review, B-4/M-2)
src/cli/commands/stop.rs
Outdated
| } | ||
|
|
||
| // Remove container (already tolerates "no such container") | ||
| let _ = runtime.remove(container_id).await; |
There was a problem hiding this comment.
BLOCKING: Silent error discard should be logged
The extracted stop_container function uses let _ = runtime.remove(container_id).await; to silently discard errors from container removal. While the original code also did this, this extraction into a standalone testable function is the right time to add logging for the discarded error -- consistent with how run_interactive handles this at line 351-357.
Fix:
if let Err(e) = runtime.remove(container_id).await {
tracing::debug!("Failed to remove container {}: {}", container_id, e);
}This ensures there's a diagnostic trail if container removal fails due to timeout, permissions, or other issues.
(Rust review, M-3)
| (!queue.is_empty()).then(|| queue.remove(0)) | ||
| } | ||
|
|
||
| fn take_unit(&self, method: &str) -> MinoResult<()> { |
There was a problem hiding this comment.
Should-Fix: Repetitive take_ helper pattern (Complexity M-1)*
The nine take_* methods share an identical 4-arm match structure that only differs in the variant destructured and the default value returned. This is 79 lines of near-identical boilerplate. Adding new MockResponse variants in the future requires writing yet another copy-paste helper.
Suggested approach: Use a generic helper with a conversion closure to reduce per-type boilerplate to one line:
fn take<T>(
&self,
method: &str,
default: T,
extract: fn(MockResponse) -> Option<T>,
) -> MinoResult<T> {
match self.take_response(method) {
Some(Ok(resp)) => match extract(resp) {
Some(val) => Ok(val),
None => panic\!("wrong MockResponse variant for '{}'", method),
},
None => Ok(default),
Some(Err(e)) => Err(e),
}
}Then each helper becomes:
fn take_bool(&self, method: &str, default: bool) -> MinoResult<bool> {
self.take(method, default, |r| match r { MockResponse::Bool(b) => Some(b), _ => None })
}(Complexity review)
| runtime: &dyn ContainerRuntime, | ||
| force: bool, | ||
| ) -> MinoResult<bool> { | ||
| if !matches!( |
There was a problem hiding this comment.
Should-Fix: Duplicate status guard (Consistency SF-2)
The extracted stop_container function contains an early-return guard for non-Running/Starting sessions (lines 75-80), but execute already has its own guard (lines 22-35) that checks the same condition. The stop_container guard is redundant since execute will never call it with a Stopped/Failed session.
Recommendation: Remove the guard from stop_container and update the doc comment, since the only caller already prevents that case. This improves clarity about which layer is responsible for the status check.
(Consistency review, SF-2)
Cargo.toml
Outdated
| tempfile = "3.19" | ||
| assert_cmd = "2.0" | ||
| predicates = "3.1" | ||
| serial_test = "3.4" |
There was a problem hiding this comment.
Should-Fix: Disable unnecessary default features on serial_test (Dependencies)
The serial_test = "3.4" uses default features which include async and logging. The async feature brings in futures-executor which is unnecessary because this project uses #[tokio::test] for async test execution. Tokio handles the async runtime; serial_test's async feature provides its own executor which is never used here.
Fix:
serial_test = { version = "3.4", default-features = false }The #[serial] macro works with #[tokio::test] without the async feature. This eliminates futures-executor as an unnecessary transitive dependency (10 new crates added; removing one helps minimize build overhead).
(Dependencies review)
Summary: PR Review CommentsBlocking Issues FixedAll 5 blocking issues have been posted as inline comments:
Should-Fix Issues (Ready for Review)All 3 should-fix items have been posted as inline comments:
Pre-Existing Issues (Informational Only)These issues exist in the codebase but are not introduced by this PR: Architecture:
Regression:
Security:
Tests:
Performance:
Test Coverage Summary
RecommendationStatus: Request Changes Path Forward:
Review Scores:
Generated by Claude Code Review Automation |
Replace `let _ = runtime.remove()` with `if let Err(e)` + `warn!()`, matching the established pattern in run_interactive (run/mod.rs:351-357). Removal is still best-effort and does not fail the stop operation. Co-Authored-By: Claude <noreply@anthropic.com>
Only the #[serial] attribute macro is needed; the default `async` and `logging` features pull in futures-executor and log which are unused since all async tests use #[tokio::test]. Co-Authored-By: Claude <noreply@anthropic.com>
…onsumed - Replace Vec<MockResponse> with VecDeque for O(1) pop_front (SF-1) - Record image and command args in run/create mock methods (SF-3) - Record sorted labels in volume_create mock method (SF-4) - Add verify_all_consumed() to catch unconsumed queued responses (SF-7) - Add tests for new arg recording and verify_all_consumed behavior Co-Authored-By: Claude <noreply@anthropic.com>
- Add #[serial] to resolve_layer_names_config_layers and resolve_layer_names_none_when_empty tests that mutate MINO_LAYERS env var without serialization (SF-2) - Extract duplicated RunContext scaffolding from smoke_run_interactive and smoke_run_detached into SmokeTestFixture struct with run_ctx() method, eliminating ~17 repeated lines per test (SF-5) Co-Authored-By: Claude <noreply@anthropic.com>
Summary
MockRuntimetest double (src/orchestration/mock.rs) behind#[cfg(test)]with queued FIFO responses, call recording, and assertion helperslogs.rs,stop.rs,list.rsto extract testable inner functionsserial_testdev-dependency for smoke tests touching shared stateCloses #30
Test plan
cargo test— 339 tests pass (up from 313)cargo clippy --all-targets— clean (only pre-existing warnings)cargo fmt -- --check— clean#[serial])