crates/gunnar-server/tests/capacity.rs
//! **`Admin.Capacity` on a REAL filesystem: the number is measured, the "no"
//! arm is reachable, and an unmeasured box does not say yes (measured, not asserted).**
//!
//! `capacity.rs`'s own unit tests drive the decision over recorded appliance
//! facts — that is the only way to reach `GROWTH_SEALED` on a developer box,
//! where gunnar is never PID 1. This file proves the half a fixture cannot:
//! that the bytes on the wire come from a live `statvfs` and not from a
//! constant.
//!
//! **How it tells a working `statvfs` from a hardcoded number.** Not by
//! comparing against an expected value — any constant satisfies a comparison
//! with itself, and an expected value of 0 satisfies almost anything. It
//! writes 64 MiB into the store root and asks again: a live measurement moves
//! by what was written, and no constant can follow it. That is applied output
//! off a real filesystem, measured before and after a real write.
use std::sync::Arc;
use gunnar_core::{NewRepo, RepoRef};
use gunnar_grpc::{ControlError, ControlPlane as _};
use gunnar_proto::{CapacityRequest, CapacityVerdict, SetGrowth};
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use gunnar_ssh::{DiskFloor, DiskHeadroomGate};
use prost::Message as _;
const MIB: u64 = 1024 * 1024;
const WRITTEN: u64 = 64 * MIB;
struct Rig {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
}
impl Rig {
fn new() -> Rig {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
stores
.catalog()
.create_namespace("team")
.expect("namespace");
Rig { _dir: dir, stores }
}
fn gate(&self, floor: DiskFloor) -> Arc<DiskHeadroomGate> {
Arc::new(DiskHeadroomGate::new(self.stores.root(), floor))
}
fn plane(&self, floor: DiskFloor) -> CoreControlPlane {
CoreControlPlane::new(Arc::clone(&self.stores)).with_headroom(self.gate(floor))
}
/// Actually consume `bytes` of the store root's filesystem, flushed.
fn burn(&self, name: &str, bytes: u64) {
use std::io::Write as _;
let path = self.stores.root().join(name);
let mut f = std::fs::File::create(&path).expect("create");
let chunk = vec![0x5au8; MIB as usize];
let mut left = bytes;
while left > 0 {
let n = left.min(MIB) as usize;
f.write_all(&chunk[..n]).expect("write");
left -= n as u64;
}
f.flush().expect("flush");
f.sync_all()
.expect("fsync: the bytes must be on the filesystem, not in a page cache");
}
}
fn ask(
plane: &CoreControlPlane,
requested: u64,
namespace: &str,
) -> gunnar_proto::CapacityResponse {
plane
.capacity(&CapacityRequest {
requested_bytes: requested,
namespace: namespace.to_string(),
disks: false,
})
.expect("a plane with a disk gate answers")
}
/// **The headroom number is the REAL one.** Proven by moving it: 64 MiB
/// written into the store root must show up as 64 MiB less free and 64 MiB
/// less servable. A hardcoded constant — or a `statvfs` of the wrong path —
/// cannot follow a write it did not see.
#[test]
fn the_free_bytes_are_a_live_statvfs_and_move_when_the_filesystem_moves() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let before = ask(&plane, 1, "");
// A real filesystem has a size, and it is not all free — if it were, the
// "identity value" trap would be wide open here.
assert!(before.total_bytes > 0, "{before:?}");
assert!(
before.free_bytes > 0 && before.free_bytes < before.total_bytes,
"{before:?}"
);
assert_eq!(
before.servable_bytes,
before.free_bytes - before.floor_bytes,
"servable is free minus the floor; quoting `free` would sell the reserve"
);
// The same measurement the push door refuses under, to the byte-ish.
let gate = rig.gate(DiskFloor {
percent: 0,
bytes: 1,
});
let room = gate.headroom().expect("statvfs on the store root");
assert_eq!(
before.total_bytes, room.total,
"the RPC and the disk gate must measure the SAME filesystem"
);
assert!(
before.free_bytes.abs_diff(room.free) < 4 * MIB,
"the RPC's free bytes and the gate's differ by more than a moment's noise: {} vs {}",
before.free_bytes,
room.free
);
rig.burn("ballast", WRITTEN);
let after = ask(&plane, 1, "");
let dropped = before.free_bytes.saturating_sub(after.free_bytes);
assert!(
dropped >= WRITTEN * 3 / 4,
"writing {WRITTEN} bytes must be visible in the answer; free went {} -> {} (a drop of \
{dropped}). A hardcoded constant cannot move, and neither can a statvfs of the wrong \
path.",
before.free_bytes,
after.free_bytes
);
assert_eq!(
after.total_bytes, before.total_bytes,
"the filesystem did not change size, only its free space"
);
assert_eq!(
before.servable_bytes.saturating_sub(after.servable_bytes),
dropped,
"servable must move with free, byte for byte"
);
assert_eq!(after.root, rig.stores.root().display().to_string());
}
/// **Both arms, on a real disk.** A raise that fits is YES; one that does not
/// is NO, with the shortfall named — and the shortfall is arithmetic over the
/// measured bytes, not a flag.
#[test]
fn a_raise_beyond_the_servable_bytes_is_answered_no_with_the_shortfall_named() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
// Green arm: one byte fits on any disk that has any room at all.
let yes = ask(&plane, 1, "");
assert_eq!(
yes.verdict,
CapacityVerdict::CapacityAvailable as i32,
"{}",
yes.reason
);
assert_eq!(yes.shortfall_bytes, 0);
assert!(yes.reason.contains("YES"), "{}", yes.reason);
// Red arm, reachable on a real filesystem: a petabyte is not there.
let huge = 1024 * 1024 * 1024 * 1024 * 1024u64;
let no = ask(&plane, huge, "");
assert_eq!(
no.verdict,
CapacityVerdict::CapacityExhausted as i32,
"{}",
no.reason
);
assert_eq!(
no.additional_bytes, huge,
"no namespace was named, so the whole cap is new"
);
assert_eq!(
no.shortfall_bytes,
huge - no.servable_bytes,
"the shortfall is what is MISSING, computed from the measured servable bytes"
);
assert!(no.shortfall_bytes > 0);
assert!(
no.reason.contains("NO."),
"the sentence must say no: {}",
no.reason
);
assert!(
no.reason
.contains(&gunnar_ssh::headroom::human(no.servable_bytes)),
"and must name what CAN be served: {}",
no.reason
);
// **The boundary is `servable`, not `free`: the floor is not for sale.**
// A second plane whose floor is half the free space must refuse three
// quarters of it — a request comfortably UNDER `free` and comfortably OVER
// `free - floor`, so the margin swamps the drift a live filesystem has
// between two calls. (The first draft of this test asked for exactly
// `servable_bytes` and was red on the honest answer: 444 KiB had moved
// under it between the two `statvfs` calls.)
let free = no.free_bytes;
let half = rig.plane(DiskFloor {
percent: 0,
bytes: free / 2,
});
let over = ask(&half, free / 4 * 3, "");
assert_eq!(
over.verdict,
CapacityVerdict::CapacityExhausted as i32,
"{} of {} free was accepted with a {} floor in force: the reserve was sold",
gunnar_ssh::headroom::human(free / 4 * 3),
gunnar_ssh::headroom::human(free),
gunnar_ssh::headroom::human(free / 2),
);
let under = ask(&half, free / 4, "");
assert_eq!(
under.verdict,
CapacityVerdict::CapacityAvailable as i32,
"and the same disk, asked for a quarter, must still say yes — or this test proves nothing but that the gate refuses everything: {}",
under.reason
);
assert_eq!(
under.floor_bytes,
free / 2,
"the floor reported is the one this plane enforces, not a default"
);
}
/// **A raise needs the DELTA.** The bytes a tenant already holds are already
/// spent out of free space, so a tenant holding N asking for N + 1 needs one
/// byte — and `held_bytes` is the real meter, measured off the disk, not 0.
#[test]
fn a_named_namespace_is_only_charged_for_the_bytes_it_does_not_already_hold() {
let rig = Rig::new();
let path = RepoRef::new("team", "proj").expect("legal");
rig.stores
.create_repo(NewRepo::new(path.clone()), false)
.expect("repo");
// Real bytes in the repository's own directory, so `Usage.Read`'s walk
// finds them. 8 MiB is far past any empty store's noise.
let git_dir = rig.stores.repo_dir(&path).expect("a path");
std::fs::write(git_dir.join("ballast.pack"), vec![0x5au8; 8 * MIB as usize]).expect("write");
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let held = plane
.usage(Some("team"))
.expect("usage")
.first()
.expect("one row")
.pack_bytes;
assert!(
held >= 8 * MIB,
"this test needs the namespace to really hold bytes, not zero; it holds {held}"
);
let a = ask(&plane, held + 1, "team");
assert_eq!(
a.held_bytes, held,
"the RPC must report the SAME meter Usage.Read does (one implementation)"
);
assert_eq!(
a.additional_bytes,
1,
"raising a tenant from {held} to {} needs ONE new byte, not {}",
held + 1,
held + 1
);
assert_eq!(
a.verdict,
CapacityVerdict::CapacityAvailable as i32,
"{}",
a.reason
);
assert!(
a.reason.contains("team"),
"the sentence names the tenant: {}",
a.reason
);
// And with no namespace the SAME cap is the harder question: all of it is
// new. If the delta were ignored these two would agree, and they must not.
let anonymous = ask(&plane, held + 1, "");
assert_eq!(anonymous.held_bytes, 0);
assert_eq!(anonymous.additional_bytes, held + 1);
assert_ne!(
anonymous.additional_bytes, a.additional_bytes,
"naming the namespace must change the arithmetic, or `held_bytes` is decoration"
);
// A namespace the catalog does not hold is NOT_FOUND, never a silent zero:
// quoting a raise against the wrong tenant's zero is how a seller
// oversells.
let err = plane
.capacity(&CapacityRequest {
requested_bytes: 1,
namespace: "no-such-tenant".into(),
disks: false,
})
.expect_err("an unknown namespace must not be treated as holding nothing");
assert!(matches!(err, ControlError::NotFound(_)), "{err:?}");
}
/// On this box gunnar is not PID 1, so the answer must NOT claim the set is
/// sealed. The sealed arm is driven over recorded appliance facts in
/// `capacity.rs`'s unit tests — this asserts the live reader does not fake it.
#[test]
fn the_growth_answer_on_this_developer_box_is_never_sealed() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let a = ask(&plane, 1, "");
assert_ne!(
a.growth,
SetGrowth::GrowthSealed as i32,
"gunnar is not PID 1 here ({}), so nothing may report a sealed appliance: {}",
std::process::id(),
a.reason
);
assert_ne!(
a.growth,
SetGrowth::Unspecified as i32,
"every answer carries a growth verdict; UNSPECIFIED is a field nobody filled"
);
assert!(
!a.reason.contains("UNUSED AND INVISIBLE"),
"and must not tell an operator on a shelled box that a disk is wasted: {}",
a.reason
);
}
/// **A plane with no disk gate declines rather than quoting a floor it is not
/// enforcing.** Zero bytes free would read as a full disk; a default floor
/// would read as a promise this server never made.
#[test]
fn a_plane_with_no_disk_gate_declines_instead_of_quoting_a_floor_it_does_not_enforce() {
let rig = Rig::new();
let plane = CoreControlPlane::new(Arc::clone(&rig.stores));
let err = plane
.capacity(&CapacityRequest {
requested_bytes: 1,
namespace: String::new(),
disks: false,
})
.expect_err("no gate wired, no quote");
let ControlError::Unimplemented(line) = err else {
panic!("Unimplemented, not {err:?}");
};
assert!(line.contains("--disk-floor"), "{line}");
}
// ───────────────────────────────────────────────────────────────────────────
// The box: cores and RAM are measured off THIS machine, absent when not
// ───────────────────────────────────────────────────────────────────────────
/// `/proc/meminfo`, read and parsed HERE — not through `probe::parse_meminfo`,
/// so the comparison is between two readers and not one reader twice.
fn meminfo_here() -> (u64, u64) {
let text = std::fs::read_to_string("/proc/meminfo").expect("/proc/meminfo on a Linux host");
let kib = |key: &str| -> u64 {
text.lines()
.find(|l| l.starts_with(key))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|n| n.parse::<u64>().ok())
.unwrap_or_else(|| panic!("{key} in /proc/meminfo"))
};
(kib("MemTotal:") * 1024, kib("MemAvailable:") * 1024)
}
/// This process's cgroup v2 `memory.max`, read HERE; `None` when unlimited.
fn cgroup_memory_max_here() -> Option<u64> {
let own = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = own
.lines()
.find_map(|l| l.strip_prefix("0::"))?
.trim()
.trim_start_matches('/');
let max = std::fs::read_to_string(format!("/sys/fs/cgroup/{rel}/memory.max")).ok()?;
max.trim().parse().ok()
}
/// The affinity mask's popcount out of `/proc/self/status`, counted HERE, and
/// the cgroup v2 `cpu.max` quota in whole CPUs when one is set.
fn cpus_here() -> (usize, Option<usize>) {
let status = std::fs::read_to_string("/proc/self/status").expect("/proc/self/status");
let list = status
.lines()
.find_map(|l| l.strip_prefix("Cpus_allowed_list:"))
.expect("Cpus_allowed_list")
.trim();
let count = |list: &str| {
let mut n = 0usize;
for range in list.split(',') {
n += match range.split_once('-') {
Some((lo, hi)) => hi.parse::<usize>().unwrap() - lo.parse::<usize>().unwrap() + 1,
None => 1,
};
}
n
};
// The kernel's mask may name every POSSIBLE CPU (0-127 on a 32-CPU host):
// what a task can run on is that mask ∩ the ONLINE CPUs.
let online = std::fs::read_to_string("/sys/devices/system/cpu/online")
.expect("/sys/devices/system/cpu/online");
let allowed = count(list).min(count(online.trim()));
let quota = (|| {
let own = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = own
.lines()
.find_map(|l| l.strip_prefix("0::"))?
.trim()
.trim_start_matches('/');
let raw = std::fs::read_to_string(format!("/sys/fs/cgroup/{rel}/cpu.max")).ok()?;
let mut parts = raw.split_whitespace();
let quota: f64 = parts.next()?.parse().ok()?;
let period: f64 = parts.next()?.parse().ok()?;
Some((quota / period).ceil().max(1.0) as usize)
})();
(allowed, quota)
}
/// **The RAM on the wire is `/proc/meminfo`'s, read independently, and not a
/// constant.** `MemTotal` must match to the byte (it does not move); `MemAvailable`
/// must be present, under the total, and within a moment's drift of a second
/// read — the tolerance is there because a live box moves between two reads,
/// and it is asserted because an absent or zero value must not pass.
#[test]
fn the_ram_fields_are_this_hosts_meminfo_read_by_a_second_reader() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let a = ask(&plane, 1, "");
let (mem_total, mem_available) = meminfo_here();
let expected_total = match cgroup_memory_max_here() {
Some(cap) if cap < mem_total => cap,
_ => mem_total,
};
assert_eq!(
a.ram_bytes_total,
Some(expected_total),
"ram_bytes_total must be what /proc/meminfo says on this host (MemTotal {mem_total}, \
cgroup cap {:?}): {}",
cgroup_memory_max_here(),
a.machine_reason
);
let available = a
.ram_bytes_available
.expect("MemAvailable is readable on this host");
assert!(
available > 0 && available <= expected_total,
"{available} of {expected_total}"
);
let drift = available.abs_diff(mem_available.min(expected_total));
assert!(
drift < expected_total / 5,
"MemAvailable on the wire ({available}) is not within a fifth of an independent read \
({mem_available}) — that is not two readings of one file"
);
assert!(
a.machine_reason.contains("/proc/meminfo"),
"{}",
a.machine_reason
);
}
/// **The cores on the wire are the affinity mask's popcount** (∩ the cgroup
/// quota when one is set), read here from `/proc/self/status`, ×1000.
#[test]
fn the_cores_on_the_wire_are_the_affinity_masks_popcount_in_millicores() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let a = ask(&plane, 1, "");
let (allowed, quota) = cpus_here();
let expected = quota.map_or(allowed, |q| q.min(allowed));
assert_eq!(
a.cpu_millicores_total,
Some(expected as u64 * 1000),
"Cpus_allowed_list counts {allowed} CPUs, cpu.max quota {quota:?}: {}",
a.machine_reason
);
assert!(expected >= 1, "a Linux box has a CPU");
// The busy figure is the 1-minute load ×1000: present on this host, and
// the reason line owns up to it being an approximation.
let busy = a
.cpu_millicores_busy
.expect("/proc/loadavg is readable on this host");
let load1: f64 = std::fs::read_to_string("/proc/loadavg")
.expect("loadavg")
.split_whitespace()
.next()
.unwrap()
.parse()
.unwrap();
assert!(
(busy as f64 - load1 * 1000.0).abs() < 1000.0 + load1 * 250.0,
"busy {busy} vs an independent load read of {load1}"
);
assert!(
a.machine_reason.contains("APPROXIMATION"),
"{}",
a.machine_reason
);
}
/// `CapacityResponse` **as an older client compiled it**: the thirteen disk
/// fields and nothing after them. Decoding today's bytes into this is exactly
/// what a monetize built before the box fields does.
#[derive(Clone, PartialEq, prost::Message)]
struct OlderCapacityResponse {
#[prost(enumeration = "CapacityVerdict", tag = "1")]
verdict: i32,
#[prost(enumeration = "SetGrowth", tag = "2")]
growth: i32,
#[prost(string, tag = "3")]
root: String,
#[prost(uint64, tag = "4")]
total_bytes: u64,
#[prost(uint64, tag = "5")]
free_bytes: u64,
#[prost(uint64, tag = "6")]
floor_bytes: u64,
#[prost(uint64, tag = "7")]
servable_bytes: u64,
#[prost(uint64, tag = "8")]
requested_bytes: u64,
#[prost(uint64, tag = "9")]
held_bytes: u64,
#[prost(uint64, tag = "10")]
additional_bytes: u64,
#[prost(uint64, tag = "11")]
shortfall_bytes: u64,
#[prost(string, tag = "12")]
data_set: String,
#[prost(string, tag = "13")]
reason: String,
}
/// **An older client gets the same disk answer, byte for byte.** Today's
/// response, encoded, is decoded by the message as it was before the box
/// fields existed; every one of its thirteen fields equals today's, and the
/// bytes the old client emits when it re-encodes are exactly today's bytes
/// with the box fields stripped — so the addition cost that client nothing.
#[test]
fn an_older_client_decodes_the_same_disk_answer_from_todays_bytes() {
let rig = Rig::new();
let plane = rig.plane(DiskFloor {
percent: 0,
bytes: 1,
});
let today = ask(&plane, 1, "");
assert!(
today.ram_bytes_total.is_some() && today.cpu_millicores_total.is_some(),
"this test needs the new fields PRESENT, or it proves nothing: {}",
today.machine_reason
);
let bytes = today.encode_to_vec();
let older = OlderCapacityResponse::decode(bytes.as_slice()).expect("an older client decodes");
assert_eq!(
(
older.verdict,
older.growth,
&older.root,
older.total_bytes,
older.free_bytes,
older.floor_bytes,
older.servable_bytes
),
(
today.verdict,
today.growth,
&today.root,
today.total_bytes,
today.free_bytes,
today.floor_bytes,
today.servable_bytes
)
);
assert_eq!(
(
older.requested_bytes,
older.held_bytes,
older.additional_bytes,
older.shortfall_bytes,
&older.data_set,
&older.reason
),
(
today.requested_bytes,
today.held_bytes,
today.additional_bytes,
today.shortfall_bytes,
&today.data_set,
&today.reason
)
);
// Byte-identical: today's message with EVERY field the older client does not know cleared
// encodes to exactly what the older client re-encodes — the disk half of the wire did not
// move. `OlderCapacityResponse` stops at tag 13, so everything above 13 is cleared here.
//
// 2026-09-16, lane AUDIT-TWIN: `grow_state` (19) and `hotplug` (20) were added after this
// list was written and not added to it, so this assertion held only on a machine that
// measured both as their default. It was RED on this box for `hotplug`, which
// `dashboard::probe::pci_hotplug` reads true on any kernel that has the ACPI/PCIe hot-plug
// driver — so the test proved the wire had not moved only where the box happened to be
// boring. A cleared-field list has to be the complement of the older message's tags, not a
// list of the fields somebody remembered.
let stripped = gunnar_proto::CapacityResponse {
cpu_millicores_total: None,
ram_bytes_total: None,
ram_bytes_available: None,
cpu_millicores_busy: None,
machine_reason: String::new(),
grow_state: None,
hotplug: false,
// 21-26: the disk test (T6, agent/weakest-is-twin), added after this list and
// not to it — the same miss the note above records for 19 and 20.
disk_test: 0,
disk_seq_write_bps: None,
disk_seq_read_bps: None,
disk_sync_milliops_per_s: None,
disk_read_how: String::new(),
disk_reason: String::new(),
..today.clone()
};
assert_eq!(stripped.encode_to_vec(), older.encode_to_vec());
assert!(
bytes.len() > older.encoded_len(),
"today's bytes carry more than the older message — else the new fields were absent"
);
}
/// **The same RPC, not a new one.** The box facts ride on `Admin.Capacity`;
/// `gunnar-grpc`'s `METHODS` census gains nothing — the `Admin.*` family is
/// exactly what it was.
#[test]
fn the_box_facts_ride_on_admin_capacity_and_add_no_rpc_to_the_census() {
let admin: Vec<&str> = gunnar_grpc::auth::METHODS
.iter()
.copied()
.filter(|m| m.starts_with("Admin."))
.collect();
// The three `Grow*` verbs are the runtime data-set growth (2026-09-14,
// `DATA-SET-GROWTH-FLOW.md` §3) — a deliberate addition with its own
// census entries and sweep rows, not the box facts sprouting an RPC.
//
// `Admin.IssuerInfo` is the same kind of addition: `2880a16` ("pki read: a
// running appliance can be asked which CA it mints with"), which added the
// verb and the census entry and did NOT add the row here. This gate has
// been red on master ever since that merge landed, which is how a gate that
// names a whole family fails — silently, on somebody else's branch, with a
// message about box facts that has nothing to do with the change.
assert_eq!(
admin,
[
// `Admin.AuditTrail` (lane T11, 2026-09-21) is the third addition of the
// same kind: a read that lets a RESTORED box be asked whether its audit
// trail came back. Named here on the branch that added it, rather than
// left to redden somebody else's, which is the mistake this comment
// records `Admin.IssuerInfo` having made.
"Admin.AuditTrail",
"Admin.Capacity",
"Admin.GrowResume",
"Admin.GrowStart",
"Admin.GrowStatus",
"Admin.Health",
"Admin.IssuerInfo",
// `Admin.SetTunnelAddress` (T17, the WireGuard address of an enrolled box) and
// `Admin.IssueTwinCertificate` (T9, the twin's pairing) — each a verb of its
// own with its own census entry and sweep row, in `METHODS` order.
"Admin.SetTunnelAddress",
"Admin.IssueTwinCertificate",
"Admin.Stats",
"Admin.StorageInfo",
// `Admin.Topology` (2026-10-04): the estate's boxes for the console's
// Machine card — a read of its own, not a box fact.
"Admin.Topology",
"Admin.VerifyChain",
// `Admin.GetSetting` / `Admin.SetSetting` (owner 2026-10-26): the generic server
// settings a console keeps its switches in — verbs of their own, not box facts.
"Admin.GetSetting",
"Admin.SetSetting",
],
"the Admin family changed; the box facts were meant to ride on Admin.Capacity"
);
assert_eq!(
gunnar_grpc::auth::METHODS
.iter()
.filter(|m| **m == "Admin.Capacity")
.count(),
1
);
}
/// **Every disk, only when asked** (`CapacityRequest.disks`, 2026-09-28, the
/// console's admin disk view). A quote (`disks: false`) carries no disk report
/// and so pays for no mount-table walk; the admin read (`disks: true`) gets
/// every storage mount with the store root placed on its mount under the label
/// `data`, measured by the same statvfs the data-set figures are — so the two
/// agree on the total — and the `--disk-floor` in force on it.
#[test]
fn every_disk_is_reported_only_when_asked_and_the_data_root_is_placed_on_its_mount() {
let rig = Rig::new();
let floor = DiskFloor {
percent: 5,
bytes: 2 * MIB,
};
let plane = rig.plane(floor);
assert!(
ask(&plane, 1, "").disks.is_none(),
"a quote paid for the disk report"
);
let answer = plane
.capacity(&CapacityRequest {
requested_bytes: 0,
namespace: String::new(),
disks: true,
})
.expect("a plane with a disk gate answers");
let disks = answer.disks.as_ref().expect("asked for, so reported");
assert!(disks.problems.is_empty(), "{:?}", disks.problems);
assert!(disks.measured_at_unix_ms > 0);
let data = disks
.mounts
.iter()
.find(|m| m.labels.iter().any(|l| l == "data"))
.unwrap_or_else(|| panic!("the store root is on no reported mount: {:?}", disks.mounts));
assert!(data.measured, "{data:?}");
assert_eq!(
data.total_bytes, answer.total_bytes,
"two statvfs of one filesystem disagree on its size"
);
let expected_floor = (data.total_bytes / 100 * 5)
.max(2 * MIB)
.min(data.total_bytes);
assert!(
data.floor_bytes.abs_diff(expected_floor) <= data.total_bytes / 1000 + 1,
"the floor on the data mount is not the --disk-floor in force: {} vs {expected_floor}",
data.floor_bytes
);
assert!(
disks
.mounts
.iter()
.any(|m| m.labels.iter().any(|l| l == "system")),
"`/` is not labelled system: {:?}",
disks.mounts
);
// The wire round-trips (an older decoder ignores field 27 entirely).
let bytes = answer.encode_to_vec();
let back = gunnar_proto::CapacityResponse::decode(bytes.as_slice()).expect("decodes");
assert_eq!(back.disks.as_ref(), Some(disks));
}