use std::io::{self, Write};
use runner_manager_domain::attempt::{active_count, active_count_for};
use runner_manager_domain::model::{RefreshInterval, StartMode, Timestamp};
use runner_manager_domain::policy::{PolicyMode, ScalePolicy};
use runner_manager_domain::store::Store;
use runner_manager_github::rest::refreshes_per_hour;
use runner_manager_platform::launch_health::{LaunchesBlocked, launches_blocked};
use runner_manager_platform::service::{InstallRecord, github_credential_rejected_since};
use serde::Serialize;
use super::doctor::DoctorSummary;
use super::host::{FALLBACK_COST_MULTIPLE, HostBudget, local_host, max_repository_targets};
use super::workspace;
use super::{CliError, Context, Failure, StatusArgs, write_failed};
pub const SCHEMA_VERSION: u32 = 1;
#[cfg(test)]
const SCHEMA_V1_HOST_FIELDS: &[&str] = &[
"architecture",
"capacity",
"configured",
"display_name",
"headroom",
"id",
"in_use",
"os",
"refresh_interval_secs",
"service_start_mode",
];
#[cfg(test)]
const SCHEMA_V1_POLICY_FIELDS: &[&str] = &[
"active_attempts",
"enabled",
"id",
"max_capacity",
"min_capacity",
"mode",
"routing_labels",
"scope",
"state",
"target",
];
#[derive(Debug, Clone, Serialize)]
pub struct StatusDocument {
pub schema_version: u32,
pub generated_at: Timestamp,
pub product: Product,
pub github_contacted: bool,
pub credential: Credential,
pub host: HostSnapshot,
pub budget: BudgetSnapshot,
pub polling: super::polling::PollingSnapshot,
pub policies: Vec<PolicySnapshot>,
pub doctor: DoctorSummary,
pub caches: super::cache::CacheSnapshot,
}
#[derive(Debug, Clone, Serialize)]
pub struct Product {
pub name: &'static str,
pub version: &'static str,
pub build_version: &'static str,
pub service_binary_version: Option<String>,
pub service_credential_rejected_since: Option<Timestamp>,
pub service_definition_outdated_since_version: Option<String>,
pub service_launches_blocked: Option<LaunchesBlocked>,
pub service_stalled: Option<String>,
pub service_installed_from: Option<String>,
}
struct ServiceView {
paths: runner_manager_platform::paths::AppPaths,
binary: Option<std::path::PathBuf>,
start_mode: Option<StartMode>,
foreign: Option<runner_manager_platform::service::ForeignService>,
}
impl ServiceView {
fn of(context: &Context) -> Self {
let Some(foreign) = super::service::operations(context).foreign_service() else {
let record = InstallRecord::read(context.paths()).ok().flatten();
return Self {
paths: context.paths().clone(),
start_mode: record.as_ref().map(|record| record.start_mode),
binary: record.map(|record| record.binary),
foreign: None,
};
};
Self {
binary: None,
start_mode: None,
paths: foreign.directories.paths(),
foreign: Some(foreign),
}
}
}
pub(super) fn binary_version(path: &std::path::Path) -> Option<String> {
let output = std::process::Command::new(path)
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()?
.split_whitespace()
.last()
.map(str::to_string)
}
fn outdated_service_definition(context: &Context) -> Option<String> {
super::service::operations(context)
.definition_drift()
.ok()
.flatten()
.map(|drift| drift.installed_by_version)
}
#[derive(Debug, Clone, Serialize)]
pub struct Credential {
pub present: bool,
pub unreadable: Option<String>,
pub store_scope: String,
pub store_location: String,
pub service_reached_github_secs_ago: Option<u64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HostSnapshot {
pub configured: bool,
pub id: Option<String>,
pub display_name: Option<String>,
pub os: Option<String>,
pub architecture: Option<String>,
pub capacity: u16,
pub in_use: u16,
pub headroom: u16,
pub service_start_mode: String,
pub refresh_interval_secs: u16,
pub runner_root: Option<String>,
pub runner_root_source: String,
pub configured_runner_root: Option<String>,
pub runner_root_unavailable: Option<String>,
pub active_ephemeral_attempts: u16,
pub cleanup_blocked_ephemeral_attempts: u16,
}
#[derive(Debug, Clone, Serialize)]
pub struct BudgetSnapshot {
pub interval_secs: u16,
pub refreshes_per_hour: u32,
pub projected_requests_per_hour: u32,
pub projection_is_floor: bool,
pub allowance_requests_per_hour: u32,
pub ceiling_requests_per_hour: u32,
pub headroom_requests_per_hour: u32,
pub exceeds_allowance: bool,
pub max_repository_targets: u32,
pub best_case_multiple_when_paging: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct PolicySnapshot {
pub id: String,
pub target: String,
pub profile_name: String,
pub scope: String,
pub mode: String,
pub state: String,
pub enabled: bool,
pub min_capacity: u16,
pub max_capacity: Option<u16>,
pub routing_labels: Vec<String>,
pub active_attempts: u16,
pub cleanup_blocked_attempts: u16,
pub workspace_mode: String,
pub workspace_root: Option<String>,
pub workspace_effective_root: Option<String>,
pub workspace_root_source: String,
pub workspace_slots: Vec<SlotSnapshot>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SlotSnapshot {
pub slot: u16,
pub attempt: String,
pub state: String,
pub cleanup_blocked: bool,
}
impl From<&workspace::SlotLease> for SlotSnapshot {
fn from(lease: &workspace::SlotLease) -> Self {
Self {
slot: lease.slot,
attempt: lease.attempt.clone(),
state: lease.state.clone(),
cleanup_blocked: lease.cleanup_blocked,
}
}
}
impl PolicySnapshot {
fn of(
policy: &ScalePolicy,
active_attempts: u16,
workspace: &workspace::RepositoryWorkspace,
) -> Self {
Self {
id: policy.id.to_string(),
target: policy.target.slug(),
profile_name: policy.profile_name().to_string(),
scope: workspace::scope_token(policy.target.scope()).to_string(),
mode: match policy.mode() {
PolicyMode::MonitorOnly => "monitor_only",
PolicyMode::Autoscale(_) => "autoscale",
}
.to_string(),
state: policy.state().to_string(),
enabled: policy.enabled(),
min_capacity: policy.min_capacity(),
max_capacity: policy.max_capacity().map(std::num::NonZeroU16::get),
routing_labels: policy
.routing_labels()
.map(registration_labels)
.unwrap_or_default(),
active_attempts,
cleanup_blocked_attempts: workspace.attempts.cleanup_blocked,
workspace_mode: workspace.kind().to_string(),
workspace_root: workspace
.policy
.root()
.map(|root| root.as_str().to_string()),
workspace_effective_root: workspace.effective_root().map(str::to_string),
workspace_root_source: workspace.root_source().to_string(),
workspace_slots: workspace.leases.iter().map(SlotSnapshot::from).collect(),
}
}
}
fn registration_labels(labels: &runner_manager_domain::policy::RoutingLabels) -> Vec<String> {
labels.as_registration_labels()
}
pub fn snapshot(context: &Context) -> Result<StatusDocument, CliError> {
snapshot_with(context, true)
}
pub fn snapshot_without_doctor(context: &Context) -> Result<StatusDocument, CliError> {
snapshot_with(context, false)
}
fn snapshot_with(context: &Context, with_doctor: bool) -> Result<StatusDocument, CliError> {
let store = context.store()?;
let host = local_host(&store)?;
let attempts = store.attempts().map_err(|source| {
CliError::new(
Failure::LocalState,
format!("cannot read this host's attempt journal: {source}"),
)
})?;
let policies = store.policies().map_err(|source| {
CliError::with_remedy(
Failure::LocalState,
format!("cannot read this host's policies: {source}"),
"runner-manager host show",
)
})?;
let start_mode = host
.as_ref()
.map_or_else(StartMode::default, |h| h.service_start_mode);
let interval = host
.as_ref()
.map_or_else(RefreshInterval::default, |h| h.refresh_interval);
let capacity = host
.as_ref()
.map_or(super::DEFAULT_HOST_CAPACITY, |h| h.host_capacity());
let in_use = active_count(attempts.iter());
let secrets = context.secret_store(start_mode)?;
let (present, unreadable) = match secrets.load() {
Ok(value) => (value.is_some(), None),
Err(source) => (false, Some(source.to_string())),
};
let service = ServiceView::of(context);
let service_reached_github_secs_ago = (unreadable.is_some()
&& service.foreign.is_none()
&& github_credential_rejected_since(context.paths())
.ok()
.flatten()
.is_none())
.then(|| super::doctor::installed_service_contact_age(context))
.flatten();
let targets: Vec<_> = policies.iter().map(|p| p.target.clone()).collect();
let budget = HostBudget::of(interval, &targets);
let configured = super::polling::configured(context, &store).unwrap_or_else(|error| {
(
super::polling::Intervals::with_default_idle(interval),
Some(error.to_string()),
)
});
let polling = super::polling::snapshot(context, configured, &targets);
let runner_root = workspace::host_root(context.paths(), host.as_ref());
let ephemeral = workspace::host_affected_attempts(&store)?;
let workspaces = policies
.iter()
.map(|policy| workspace::repository_workspace(&store, &runner_root, policy))
.collect::<Result<Vec<_>, CliError>>()?;
let doctor = if with_doctor {
super::doctor::status_summary(context, host.as_ref(), &policies, &runner_root)
} else {
super::doctor::pending_summary(context)
};
Ok(StatusDocument {
schema_version: SCHEMA_VERSION,
generated_at: context.clock().now(),
product: Product {
name: env!("CARGO_PKG_NAME"),
version: env!("CARGO_PKG_VERSION"),
build_version: env!("RUNNER_MANAGER_BUILD_VERSION"),
service_binary_version: service.binary.as_deref().and_then(binary_version),
service_credential_rejected_since: github_credential_rejected_since(&service.paths)
.ok()
.flatten(),
service_definition_outdated_since_version: service
.foreign
.is_none()
.then(|| outdated_service_definition(context))
.flatten(),
service_launches_blocked: launches_blocked(&service.paths, context.clock().now()),
service_stalled: runner_manager_platform::daemon_heartbeat::liveness(
&service.paths,
context.clock().now(),
)
.stall(
runner_manager_platform::service::last_github_contact(&service.paths)
.ok()
.flatten(),
&super::service::operations(context)
.identity()
.restart_command(service.start_mode.unwrap_or(start_mode)),
),
service_installed_from: service
.foreign
.as_ref()
.map(runner_manager_platform::service::ForeignService::label),
},
github_contacted: false,
credential: Credential {
present,
unreadable,
store_scope: secrets.scope().to_string(),
store_location: secrets.location(),
service_reached_github_secs_ago,
},
host: HostSnapshot {
configured: host.is_some(),
id: host.as_ref().map(|h| h.id.to_string()),
display_name: host.as_ref().map(|h| h.display_name.clone()),
os: host.as_ref().map(|h| h.os.to_string()),
architecture: host.as_ref().map(|h| h.architecture.to_string()),
capacity,
in_use,
headroom: capacity.saturating_sub(in_use),
service_start_mode: start_mode.to_string(),
refresh_interval_secs: interval.as_secs(),
runner_root: runner_root.effective_text().map(str::to_string),
runner_root_source: runner_root.source().as_token().to_string(),
configured_runner_root: runner_root
.configured
.as_ref()
.map(|root| root.as_str().to_string()),
runner_root_unavailable: runner_root.unavailable.clone(),
active_ephemeral_attempts: ephemeral.active,
cleanup_blocked_ephemeral_attempts: ephemeral.cleanup_blocked,
},
budget: BudgetSnapshot {
interval_secs: interval.as_secs(),
refreshes_per_hour: refreshes_per_hour(interval),
projected_requests_per_hour: budget.requests_per_hour(),
projection_is_floor: budget.is_floor(),
allowance_requests_per_hour: budget.allowance(),
ceiling_requests_per_hour: budget.ceiling(),
headroom_requests_per_hour: budget.headroom(),
exceeds_allowance: budget.exceeds_allowance(),
max_repository_targets: max_repository_targets(interval),
best_case_multiple_when_paging: FALLBACK_COST_MULTIPLE,
},
polling,
caches: super::cache::snapshot(context, host.as_ref()),
policies: policies
.iter()
.zip(workspaces.iter())
.map(|(policy, workspace)| {
PolicySnapshot::of(
policy,
active_count_for(policy.id, attempts.iter()),
workspace,
)
})
.collect(),
doctor,
})
}
pub fn dispatch(context: &Context, args: &StatusArgs, out: &mut dyn Write) -> Result<(), CliError> {
let failed = write_failed("this host's status");
let document = snapshot(context)?;
if args.json {
write_json(out, &document)
} else {
write_text(out, &document)
}
.map_err(failed)
}
fn write_json(out: &mut dyn Write, document: &StatusDocument) -> io::Result<()> {
serde_json::to_writer_pretty(&mut *out, document)
.map_err(|source| io::Error::other(source.to_string()))?;
writeln!(out)
}
fn write_text(out: &mut dyn Write, document: &StatusDocument) -> io::Result<()> {
writeln!(
out,
"{} {}",
document.product.name, document.product.version
)?;
writeln!(out, " as of {}", document.generated_at)?;
writeln!(out)?;
if document.host.configured {
writeln!(
out,
"Host: {} ({} {})",
document.host.display_name.as_deref().unwrap_or("unnamed"),
document.host.os.as_deref().unwrap_or("unknown"),
document.host.architecture.as_deref().unwrap_or("unknown"),
)?;
} else {
writeln!(
out,
"Host: not configured yet; the values below are the defaults."
)?;
}
writeln!(
out,
" capacity {} in use of {} ({} free)",
document.host.in_use, document.host.capacity, document.host.headroom
)?;
writeln!(
out,
" service start mode {}",
document.host.service_start_mode
)?;
if let Some(root) = &document.product.service_installed_from {
writeln!(
out,
" machine service installed from {root}, another installation's: the \
policies and capacity here are this account's own, which it does not run. \
`runner-manager service status` describes it."
)?;
}
writeln!(
out,
" runner root {} ({})",
document
.host
.runner_root
.as_deref()
.unwrap_or("unavailable"),
document.host.runner_root_source.replace('_', "-"),
)?;
if let Some(reason) = &document.host.runner_root_unavailable {
writeln!(out, " runner root problem {reason}")?;
}
writeln!(
out,
" ephemeral paths {} active, {} awaiting cleanup",
document.host.active_ephemeral_attempts, document.host.cleanup_blocked_ephemeral_attempts
)?;
let service_minutes = document
.credential
.service_reached_github_secs_ago
.filter(|age| *age <= super::doctor::RECENT_CONTACT_SECS)
.map(|age| age / 60);
let state = match (
service_minutes,
&document.credential.unreadable,
document.credential.present,
) {
(Some(minutes), ..) => {
format!("in use by the service, which reached GitHub {minutes} minute(s) ago,")
}
(None, Some(_), _) => "not readable by this account".to_owned(),
(None, None, true) => "present".to_owned(),
(None, None, false) => "absent".to_owned(),
};
writeln!(
out,
" {:<26}{state} in the {}-scoped store",
"credential", document.credential.store_scope
)?;
if let Some(reason) = &document.credential.unreadable {
let label = if service_minutes.is_some() {
"not readable here"
} else {
"credential problem"
};
writeln!(out, " {label:<26}{reason}")?;
}
writeln!(
out,
" GitHub contacted no (this is a local snapshot; `auth status` asks GitHub)"
)?;
writeln!(
out,
" host doctor {}",
document.doctor.line()
)?;
if let Some(unfit) = &document.doctor.daemon_host_unfit {
writeln!(
out,
" daemon starts no runner since {}: host unfit ({})",
unfit.since,
unfit.checks.join(", ")
)?;
writeln!(out, " host unfit because {}", unfit.detail)?;
}
if let Some(blocked) = &document.product.service_launches_blocked {
writeln!(out, " daemon starts no runner {blocked}")?;
writeln!(out, " launches remedy {}", blocked.remedy)?;
}
if let Some(stall) = &document.product.service_stalled {
writeln!(out, " daemon stalled {stall}")?;
}
writeln!(
out,
" dependency caches {}",
document.caches.line()
)?;
writeln!(out)?;
writeln!(out, "Policies ({})", document.policies.len())?;
if document.policies.is_empty() {
writeln!(out, " none yet -- `repo add` or `org add` creates one.")?;
}
for policy in &document.policies {
writeln!(
out,
" {:<40} {:<13} {:<10} {} active",
policy.target, policy.mode, policy.state, policy.active_attempts
)?;
writeln!(
out,
" {:<40} {} attempt in {}",
"",
policy.workspace_mode,
policy
.workspace_effective_root
.as_deref()
.unwrap_or("an unresolved root"),
)?;
for slot in &policy.workspace_slots {
writeln!(
out,
" {:<40} slot s{} {}",
"",
slot.slot,
if slot.cleanup_blocked {
"cleanup blocked; quarantined until remediation"
} else {
"leased by a live attempt"
}
)?;
}
}
writeln!(out)?;
super::polling::write_section(out, &document.polling)?;
writeln!(out)?;
writeln!(
out,
"Shared REST budget: {} requests/hour projected if no poll were answered 304, of {} \
this host may spend",
document.budget.projected_requests_per_hour, document.budget.allowance_requests_per_hour
)?;
writeln!(
out,
" about {} repository targets fit at a {}s interval (best case; a repository whose",
document.budget.max_repository_targets, document.budget.interval_secs
)?;
writeln!(
out,
" counts have to walk pages costs up to {}x that)",
document.budget.best_case_multiple_when_paging
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn document() -> StatusDocument {
StatusDocument {
schema_version: SCHEMA_VERSION,
generated_at: chrono::DateTime::from_timestamp(1_787_270_400, 0).unwrap(),
product: Product {
name: "runner-manager",
version: "0.1.0",
build_version: "0.1.0+git.0123456789ab",
service_binary_version: None,
service_credential_rejected_since: None,
service_definition_outdated_since_version: None,
service_launches_blocked: None,
service_stalled: None,
service_installed_from: None,
},
github_contacted: false,
credential: Credential {
present: true,
unreadable: None,
store_scope: "machine".to_string(),
store_location: "C:/ProgramData/runner-manager/secrets".to_string(),
service_reached_github_secs_ago: None,
},
host: HostSnapshot {
configured: true,
id: Some("00000000-0000-0000-0000-000000000001".to_string()),
display_name: Some("home-win".to_string()),
os: Some("windows".to_string()),
architecture: Some("x64".to_string()),
capacity: 2,
in_use: 1,
headroom: 1,
service_start_mode: "boot".to_string(),
refresh_interval_secs: 60,
runner_root: Some("C:/rman".to_string()),
runner_root_source: "platform_default".to_string(),
configured_runner_root: None,
runner_root_unavailable: None,
active_ephemeral_attempts: 1,
cleanup_blocked_ephemeral_attempts: 0,
},
budget: BudgetSnapshot {
interval_secs: 60,
refreshes_per_hour: 60,
projected_requests_per_hour: 180,
projection_is_floor: false,
allowance_requests_per_hour: 2500,
ceiling_requests_per_hour: 5000,
headroom_requests_per_hour: 2320,
exceeds_allowance: false,
max_repository_targets: 13,
best_case_multiple_when_paging: FALLBACK_COST_MULTIPLE,
},
polling: super::super::polling::PollingSnapshot {
idle_interval_secs: 10,
effective_idle_interval_secs: 10,
idle_interval_unreadable: None,
active_interval_secs: 60,
idle_requests_per_minute: 12,
secondary_limit_points_per_minute: 900,
measured: Some(super::super::polling::MeasuredPolling {
written_at: chrono::DateTime::from_timestamp(1_787_270_400, 0)
.expect("a valid instant"),
stale: false,
observed_minutes: 60,
full_responses: 40,
not_modified_responses: 680,
failed_responses: 0,
full_responses_per_hour: 40,
not_modified_responses_per_hour: 680,
failed_responses_per_hour: 0,
not_modified_percent: Some(94),
peak_requests_per_minute: 14,
secondary_limit_percent: 1,
pace: "idle".to_string(),
targets_idle: 1,
targets_total: 1,
throttled: false,
throttled_because: None,
rate_limit_limit: Some(5000),
rate_limit_remaining: Some(4200),
rate_limit_used: Some(800),
rate_limit_reset: None,
demand_reads: 360,
demand_unchanged_reads: 350,
demand_shared_reads: 0,
demand_suppressed: 0,
}),
},
policies: vec![PolicySnapshot {
id: "00000000-0000-0000-0000-000000000010".to_string(),
target: "owner/repo".to_string(),
profile_name: "default".to_string(),
scope: "repository".to_string(),
mode: "autoscale".to_string(),
state: "active".to_string(),
enabled: true,
min_capacity: 0,
max_capacity: Some(1),
routing_labels: vec!["rm-home-win-x64".to_string()],
active_attempts: 1,
cleanup_blocked_attempts: 0,
workspace_mode: "persistent".to_string(),
workspace_root: Some("D:/ci-cache/project".to_string()),
workspace_effective_root: Some("D:/ci-cache/project".to_string()),
workspace_root_source: "repository".to_string(),
workspace_slots: vec![SlotSnapshot {
slot: 2,
attempt: "00000000-0000-0000-0000-000000000020".to_string(),
state: "busy".to_string(),
cleanup_blocked: false,
}],
}],
doctor: DoctorSummary {
checked: 9,
failing: vec![super::super::doctor::FindingSummary {
id: "windows.symlink_privilege".to_string(),
severity: super::super::doctor::Severity::Required,
title: "Symbolic links for the runner account".to_string(),
detail: "the login service runs with a standard token".to_string(),
remedy: Some("runner-manager service install --start-at boot".to_string()),
fixable: true,
needs_admin: true,
consent_flag: Some("--allow-developer-mode".to_string()),
}],
unknown: vec!["windows.defender_exclusion".to_string()],
daemon_host_unfit: Some(super::super::doctor::HostUnfitSummary {
since: chrono::DateTime::from_timestamp(1_787_270_000, 0).unwrap(),
checks: vec!["windows.symlink_privilege".to_string()],
detail: "windows.symlink_privilege: symbolic links are refused. Fix: turn \
on Developer Mode"
.to_string(),
}),
},
caches: super::super::cache::CacheSnapshot {
enabled: true,
root: Some("/rman/_cache".to_string()),
root_source: Some("runner-root"),
problem: None,
max_bytes: Some(21_474_836_480),
used_bytes: Some(1_073_741_824),
namespaces: Some(2),
measured_at: Some(chrono::DateTime::from_timestamp(1_787_270_000, 0).unwrap()),
},
}
}
fn emitted() -> Value {
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).expect("writing to a Vec");
serde_json::from_slice(&buffer).expect("the document must be valid JSON")
}
fn keys(value: &Value, pointer: &str) -> Vec<String> {
let at = value
.pointer(pointer)
.unwrap_or_else(|| panic!("the document must carry {pointer}"));
let object = at
.as_object()
.unwrap_or_else(|| panic!("{pointer} must be an object, and is {at}"));
let mut names: Vec<String> = object.keys().cloned().collect();
names.sort();
names
}
#[test]
fn the_documented_schema_is_the_one_that_is_emitted() {
let emitted = emitted();
assert_eq!(
keys(&emitted, ""),
[
"budget",
"caches",
"credential",
"doctor",
"generated_at",
"github_contacted",
"host",
"policies",
"polling",
"product",
"schema_version",
]
);
assert_eq!(
keys(&emitted, "/polling"),
[
"active_interval_secs",
"effective_idle_interval_secs",
"idle_interval_secs",
"idle_interval_unreadable",
"idle_requests_per_minute",
"measured",
"secondary_limit_points_per_minute",
]
);
assert_eq!(
keys(&emitted, "/polling/measured"),
[
"demand_reads",
"demand_shared_reads",
"demand_suppressed",
"demand_unchanged_reads",
"failed_responses",
"failed_responses_per_hour",
"full_responses",
"full_responses_per_hour",
"not_modified_percent",
"not_modified_responses",
"not_modified_responses_per_hour",
"observed_minutes",
"pace",
"peak_requests_per_minute",
"rate_limit_limit",
"rate_limit_remaining",
"rate_limit_reset",
"rate_limit_used",
"secondary_limit_percent",
"stale",
"targets_idle",
"targets_total",
"throttled",
"throttled_because",
"written_at",
]
);
assert_eq!(
keys(&emitted, "/doctor"),
["checked", "daemon_host_unfit", "failing", "unknown"]
);
assert_eq!(
keys(&emitted, "/doctor/failing/0"),
[
"consent_flag",
"detail",
"fixable",
"id",
"needs_admin",
"remedy",
"severity",
"title"
]
);
assert_eq!(
keys(&emitted, "/doctor/daemon_host_unfit"),
["checks", "detail", "since"]
);
assert_eq!(
keys(&emitted, "/caches"),
[
"enabled",
"max_bytes",
"measured_at",
"namespaces",
"problem",
"root",
"root_source",
"used_bytes"
]
);
assert_eq!(
keys(&emitted, "/product"),
[
"build_version",
"name",
"service_binary_version",
"service_credential_rejected_since",
"service_definition_outdated_since_version",
"service_installed_from",
"service_launches_blocked",
"service_stalled",
"version"
]
);
assert_eq!(
keys(&emitted, "/credential"),
[
"present",
"service_reached_github_secs_ago",
"store_location",
"store_scope",
"unreadable"
]
);
assert_eq!(
keys(&emitted, "/host"),
[
"active_ephemeral_attempts",
"architecture",
"capacity",
"cleanup_blocked_ephemeral_attempts",
"configured",
"configured_runner_root",
"display_name",
"headroom",
"id",
"in_use",
"os",
"refresh_interval_secs",
"runner_root",
"runner_root_source",
"runner_root_unavailable",
"service_start_mode",
]
);
assert_eq!(
keys(&emitted, "/budget"),
[
"allowance_requests_per_hour",
"best_case_multiple_when_paging",
"ceiling_requests_per_hour",
"exceeds_allowance",
"headroom_requests_per_hour",
"interval_secs",
"max_repository_targets",
"projected_requests_per_hour",
"projection_is_floor",
"refreshes_per_hour",
]
);
assert_eq!(
keys(&emitted, "/policies/0"),
[
"active_attempts",
"cleanup_blocked_attempts",
"enabled",
"id",
"max_capacity",
"min_capacity",
"mode",
"profile_name",
"routing_labels",
"scope",
"state",
"target",
"workspace_effective_root",
"workspace_mode",
"workspace_root",
"workspace_root_source",
"workspace_slots",
]
);
assert_eq!(
keys(&emitted, "/policies/0/workspace_slots/0"),
["attempt", "cleanup_blocked", "slot", "state"]
);
}
#[test]
fn the_workspace_fields_are_additive_so_the_version_does_not_move() {
let emitted = emitted();
let host = keys(&emitted, "/host");
for field in SCHEMA_V1_HOST_FIELDS {
assert!(
host.iter().any(|name| name == field),
"host.{field} was promised at schema v1 and is gone; that is a breaking \
change and needs SCHEMA_VERSION bumped, not this list edited"
);
}
let policy = keys(&emitted, "/policies/0");
for field in SCHEMA_V1_POLICY_FIELDS {
assert!(
policy.iter().any(|name| name == field),
"policies[].{field} was promised at schema v1 and is gone"
);
}
assert_eq!(
SCHEMA_VERSION, 1,
"the workspace fields are additions, and an addition is compatible: a consumer \
reading only the fields it knows is unaffected"
);
}
#[test]
fn the_workspace_fields_are_structured_rather_than_rendered() {
let emitted = emitted();
assert_eq!(
emitted["host"]["runner_root_source"],
Value::from("platform_default"),
"the source is a token, not the hyphenated badge `host show` prints"
);
assert_eq!(emitted["host"]["configured_runner_root"], Value::Null);
assert!(emitted["host"]["active_ephemeral_attempts"].is_u64());
assert!(emitted["host"]["cleanup_blocked_ephemeral_attempts"].is_u64());
let policy = &emitted["policies"][0];
assert_eq!(policy["workspace_mode"], Value::from("persistent"));
assert_eq!(
policy["workspace_root_source"],
Value::from("repository"),
"the third source `d1` names: this repository's own setting, not the host's"
);
assert_eq!(
policy["workspace_root"],
Value::from("D:/ci-cache/project"),
"a persistent policy's root is its own, not the host's"
);
let slots = policy["workspace_slots"].as_array().expect("an array");
assert_eq!(slots.len(), 1);
assert_eq!(
slots[0]["slot"],
Value::from(2),
"the slot is a number, so a consumer never parses `s2`"
);
assert!(
slots[0]["cleanup_blocked"].as_bool().is_some(),
"quarantine is a boolean, not a state string a consumer has to know the \
vocabulary of"
);
let mut ephemeral = document();
ephemeral.policies[0].workspace_mode = "ephemeral".to_string();
ephemeral.policies[0].workspace_root = None;
ephemeral.policies[0].workspace_effective_root = ephemeral.host.runner_root.clone();
ephemeral.policies[0].workspace_root_source = "platform_default".to_string();
ephemeral.policies[0].workspace_slots.clear();
let mut buffer = Vec::new();
write_json(&mut buffer, &ephemeral).unwrap();
let value: Value = serde_json::from_slice(&buffer).unwrap();
assert_eq!(value["policies"][0]["workspace_root"], Value::Null);
assert_eq!(
value["policies"][0]["workspace_root_source"], value["host"]["runner_root_source"],
"an ephemeral policy inherits the host's source, and says which one it is"
);
assert_eq!(
value["policies"][0]["workspace_effective_root"],
value["host"]["runner_root"]
);
assert!(
value["policies"][0]["workspace_slots"]
.as_array()
.expect("an array")
.is_empty()
);
}
#[test]
fn no_rendering_enumerates_what_is_inside_a_workspace() {
let mut buffer = Vec::new();
write_text(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains("persistent attempt in D:/ci-cache/project"),
"the human rendering names the root: {text}"
);
assert!(
!text.contains("_work"),
"a status line that named the retained directory would be one step from \
listing it: {text}"
);
}
#[test]
fn an_unreadable_store_is_reported_rather_than_read_as_absent() {
let mut document = document();
document.credential.present = false;
document.credential.unreadable = Some("only root may read /var/db/SystemKey".to_string());
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains("credential not readable by this account"),
"{text}"
);
assert!(
text.contains("credential problem only root may read /var/db/SystemKey"),
"the store's own words are what name the remedy: {text}"
);
assert!(
!text.contains("credential absent"),
"an unreadable store has not answered the question, and `absent` is the one \
answer that sends an operator to overwrite it: {text}"
);
assert!(text.contains("Policies (1)"), "{text}");
}
#[test]
fn a_store_only_the_service_can_read_defers_to_its_recent_contact() {
let mut document = document();
document.credential.present = false;
document.credential.unreadable =
Some("the login keychain is locked for this session".to_string());
document.credential.service_reached_github_secs_ago = Some(130);
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains(
"credential in use by the service, which reached GitHub 2 \
minute(s) ago"
),
"{text}"
);
assert!(!text.contains("credential problem"), "{text}");
assert!(!text.contains("not readable by this account"), "{text}");
assert!(
text.contains("not readable here the login keychain is locked"),
"the session's own limit is still said: {text}"
);
document.credential.service_reached_github_secs_ago =
Some(super::super::doctor::RECENT_CONTACT_SECS + 1);
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(text.contains("not readable by this account"), "{text}");
assert!(text.contains("credential problem"), "{text}");
}
#[test]
fn a_stalled_daemon_is_reported() {
let mut document = document();
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
assert!(
!String::from_utf8(buffer)
.unwrap()
.contains("daemon stalled")
);
document.product.service_stalled =
Some("the service (process 7) has made no progress since then".into());
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains(" daemon stalled the service (process 7) has made"),
"{text}"
);
}
#[test]
fn blocked_launches_are_reported_with_their_reason_and_remedy() {
let mut document = document();
document.product.service_launches_blocked =
Some(runner_manager_platform::launch_health::LaunchesBlocked {
since: chrono::DateTime::from_timestamp(1_790_000_000, 0).unwrap(),
reason: "a runner launch by process 189 has held the WSL launch fence".into(),
remedy: "restart the service".into(),
});
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains("daemon starts no runner since 2026-09-21")
&& text.contains("process 189"),
"{text}"
);
assert!(
text.contains("launches remedy restart the service"),
"{text}"
);
let json = serde_json::to_value(&document).unwrap();
assert_eq!(
json["product"]["service_launches_blocked"]["remedy"],
"restart the service"
);
}
#[test]
fn the_doctor_verdict_and_the_daemons_refusal_are_one_line_each() {
let mut buffer = Vec::new();
write_text(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains(
" host doctor 1 required, 0 recommended failing \
(windows.symlink_privilege); runner-manager host doctor"
),
"{text}"
);
assert!(
text.contains(" daemon starts no runner since ")
&& text.contains("host unfit (windows.symlink_privilege)"),
"{text}"
);
assert!(
text.contains(
" host unfit because windows.symlink_privilege: symbolic links are refused. Fix: turn on Developer Mode"
),
"what the daemon found, and what fixes it: {text}"
);
let mut healthy = document();
healthy.doctor.failing.clear();
healthy.doctor.daemon_host_unfit = None;
let mut buffer = Vec::new();
write_text(&mut buffer, &healthy).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains(" host doctor ok (9 checks)"),
"{text}"
);
assert!(!text.contains("daemon starts no runner"), "{text}");
}
#[test]
fn the_document_carries_its_own_version() {
assert_eq!(emitted()["schema_version"], Value::from(SCHEMA_VERSION));
}
#[test]
fn a_scripted_consumer_reads_it_without_special_casing() {
let emitted = emitted();
let capacity = emitted["host"]["capacity"]
.as_u64()
.expect("capacity is a number, not a string");
let in_use = emitted["host"]["in_use"].as_u64().expect("a number");
assert_eq!(capacity, 2);
assert_eq!(in_use, 1);
assert_eq!(
emitted["host"]["headroom"].as_u64().expect("a number"),
capacity - in_use,
"headroom must be derivable and consistent, so a consumer can trust either"
);
assert!(
emitted["credential"]["present"]
.as_bool()
.expect("a boolean, so a consumer never string-matches on it")
);
assert!(
!emitted["github_contacted"].as_bool().expect("a boolean"),
"the document must say plainly that it is a local snapshot"
);
let policies = emitted["policies"].as_array().expect("an array");
assert_eq!(policies.len(), 1);
assert_eq!(policies[0]["target"], Value::from("owner/repo"));
assert_eq!(policies[0]["max_capacity"], Value::from(1));
assert!(
policies[0]["routing_labels"].is_array(),
"labels are an array, not a comma-separated string a consumer has to split"
);
let mut monitor_only = document();
monitor_only.policies[0].max_capacity = None;
monitor_only.policies[0].mode = "monitor_only".to_string();
let mut buffer = Vec::new();
write_json(&mut buffer, &monitor_only).unwrap();
let value: Value = serde_json::from_slice(&buffer).unwrap();
assert_eq!(value["policies"][0]["max_capacity"], Value::Null);
}
#[test]
fn the_target_ceiling_carries_its_caveat_into_both_renderings() {
assert_eq!(
emitted()["budget"]["best_case_multiple_when_paging"],
Value::from(FALLBACK_COST_MULTIPLE),
"a consumer reading `max_repository_targets` must be able to read the multiple \
it can be wrong by from the same document"
);
let mut buffer = Vec::new();
write_text(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(text.contains("best case"), "{text}");
assert!(
text.contains(&format!("{FALLBACK_COST_MULTIPLE}x")),
"{text}"
);
}
#[test]
fn nothing_is_printed_around_the_json() {
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(text.starts_with('{'), "got: {text}");
assert!(
text.ends_with("}\n"),
"got the tail: {:?}",
&text[text.len().saturating_sub(8)..]
);
serde_json::from_str::<Value>(&text).expect("parses whole");
}
#[test]
fn no_field_of_the_document_is_a_place_to_put_a_credential() {
fn walk(value: &Value, path: &str, found: &mut Vec<String>) {
match value {
Value::Object(fields) => {
for (name, child) in fields {
let here = format!("{path}/{name}");
let lowered = name.to_ascii_lowercase();
for forbidden in ["token", "secret", "device_code", "password", "key"] {
if lowered.contains(forbidden) {
found.push(here.clone());
}
}
walk(child, &here, found);
}
}
Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
walk(child, &format!("{path}/{index}"), found);
}
}
_ => {}
}
}
let mut offenders = Vec::new();
walk(&emitted(), "", &mut offenders);
assert!(
offenders.is_empty(),
"these fields name a credential, and `f1` requires this document to carry none: \
{offenders:?}"
);
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
for prefix in ["ghu_", "gho_", "ghs_", "ghp_"] {
assert!(!text.contains(prefix), "found {prefix:?} in: {text}");
}
}
}