# Test with the mock WhatsMiner
The mock miner is a test-only TCP server in `tests/mock_device/mock.rs`. It binds to `127.0.0.1:0`, speaks the WhatsMiner length-prefixed protocol, and lets integration tests exercise the actor without physical hardware.
The mock validates wire-observable behavior. It does not validate authentication tokens or decrypt encrypted parameters. Unit tests for `auth_data` cover those production responsibilities.
## Run mock tests
Run the complete mock integration suite in one process:
```sh
cargo test-mock
```
Run one scenario with output:
```sh
cargo test --locked --test mock_device \
test_three_mailboxes_concurrency -- --exact --nocapture
```
The suite covers 41 scenarios, including handshake, independent actor clones, waiting before connection, mailbox backpressure, reconnect reuse, device errors, malformed responses, large payloads, unsolicited reports, idle heartbeats, and short transfers.
## Build a mock miner
`MockMinerBuilder::new()` registers `get.device.info` automatically. The default handler supplies the salt and permission data needed by the actor handshake. Register only the handlers required by a test.
```rust
let (mock, server) = MockMinerBuilder::new()
.on("set.miner.fastboot", |request, state| {
let enabled = request["param"].as_str() == Some("enable");
state.set("fastboot", enabled);
ok_response("set.miner.fastboot")
})
.start()
.await;
```
Keep `server` alive for the test. Dropping `mock` cancels the listener and its connection tasks. The builder returns a `MockMiner` handle and a Tokio `JoinHandle`; tests can await the handle when they need to observe server shutdown.
## Register streaming handlers
A download handler returns a complete initial JSON envelope and the exact raw bytes that follow it. The initial `msg.logsize` value must equal the binary payload length when the test models a successful transfer.
```rust
let data = b"test archive bytes".to_vec();
let size = data.len();
let (mock, _server) = MockMinerBuilder::new()
.on_stream_download("get.log.download", move |_request, _state| {
let response = serde_json::json!({
"code": 0,
"when": 123,
"msg": { "logsize": size.to_string() },
"desc": "get.log.download"
});
(response.to_string(), data.clone())
})
.start()
.await;
```
An upload handler receives the decoded request, mutable mock state, and the exact payload bytes after the four-byte little-endian length prefix:
```rust
let (mock, _server) = MockMinerBuilder::new()
.on_stream_upload("set.system.update_firmware", |_request, state, data| {
state.set("firmware_received", data.to_vec());
ok_response("set.system.update_firmware")
})
.start()
.await;
```
The mock sends the framed `ready` response before reading upload bytes. A successful final response closes the connection to model the device reboot. A structured device error keeps the framed connection open.
## Handler contracts
| `on` | `Fn(&Value, &mut MockState) -> String` | Ordinary framed JSON command |
| `on_stream_download` | `Fn(&Value, &mut MockState) -> (String, Vec<u8>)` | Initial framed response plus raw archive bytes |
| `on_stream_upload` | `Fn(&Value, &mut MockState, &[u8]) -> String` | Raw firmware payload plus final framed response |
Return all responses as complete JSON envelopes containing `code`, `when`, `msg`, and `desc`. A missing field makes the response invalid for the production parser.
## Protocol behavior
The harness models these protocol boundaries:
- Each connection receives a unique salt such as `mocksalt_7`
- `get.device.info` supports separate salt and miner-permission requests
- Four zero bytes represent a heartbeat, which the mock consumes without replying
- Malformed JSON returns a framed `code: -3` response
- Unknown commands return a framed `code: -2` response with a bounded description
- Complete non-zero response codes remain framed and represent device errors
- Successful download responses switch to raw bytes after the initial envelope
- Uploads use a four-byte little-endian payload length and bounded incremental reads
- The listener supervises connection tasks and aborts and drains them on shutdown
The mock keeps shared test state behind `tokio::sync::Mutex`. That mutex belongs to the test harness, not the production actor architecture.
## Assert handler state
`MockState::set` stores JSON values under a string key. `MockState::get` deserializes a stored value into the requested type:
```rust
let state = mock.state().await;
assert_eq!(state.get::<bool>("fastboot"), Some(true));
assert_eq!(state.get::<Vec<u8>>("firmware_received"), Some(vec![1, 2, 3]));
```
The shared `salt` field is set per connection before each handler call. Avoid asserting implementation details that the wire protocol does not expose. Prefer command results, transferred bytes, state changes, and observed lifecycle status.
## Add a scenario
1. Add a focused test to `tests/mock_device/scenarios.rs` or the relevant streaming file
2. Register a handler with `MockMinerBuilder`
3. Drive the public API: `connect`, `send`, `download_log`, `upload_firmware`, or `shutdown`
4. Assert the observable result and lifecycle status
5. Run `cargo test-mock`
- [Real-device testing](E2E.md) covers hardware-test safety