feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.
Identity
seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
secp256k1 key, distinct from the Nostr-node key for crypto isolation
but still seed-recoverable
identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors
Transport
TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
→ router prefers FIPS over Tor for all peer traffic
PeerRecord gains fips_npub + last_fips fields (serde(default) for
backward-compat with older nodes)
transport/fips.rs: NodeTransport stub, reports unavailable until the
daemon is live so router falls through to Tor cleanly
Federation invites
FederatedNode and FederationInvite carry optional fips_npub
create_invite / accept_invite / peer-joined callback thread it end
to end; signature domain deliberately unchanged — FIPS Noise does
its own session auth, so the unsigned hint only affects path
selection
crate::fips
config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
service.rs: systemctl status/activate/restart/mask wrappers
update.rs: GitHub API check against upstream main; apply stubbed
until per-commit .deb artefact source is decided
RPC + dashboard
fips.status / fips.check-update / fips.apply-update / fips.install /
fips.restart registered in dispatcher
HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
when ready); shows state pill, version, FIPS npub, update button,
activate button when key is present but service is down
ISO + systemd
archipelago-fips.service: conditional on key presence, masked by
default — backend unmasks after onboarding writes the key
build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
.deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
installs it so trixie resolves deps; unit copied + masked
Version bump: 1.3.5 → 1.4.0
Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).
Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
|
|
|
//! systemctl + dpkg-query helpers for the FIPS daemon.
|
|
|
|
|
//!
|
|
|
|
|
//! Read-only queries (`is-active`, `--version`, `dpkg-query`) run as the
|
|
|
|
|
//! archipelago service user. Write operations (`unmask`, `start`, `stop`,
|
|
|
|
|
//! `restart`) go through `sudo`, matching the pattern established in
|
|
|
|
|
//! `src/vpn.rs` and `src/api/rpc/vpn.rs`. The sudoers rule shipped in the
|
|
|
|
|
//! ISO whitelists exactly these invocations.
|
|
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
2026-04-19 00:42:56 -04:00
|
|
|
use nostr_sdk::ToBech32;
|
feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.
Identity
seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
secp256k1 key, distinct from the Nostr-node key for crypto isolation
but still seed-recoverable
identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors
Transport
TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
→ router prefers FIPS over Tor for all peer traffic
PeerRecord gains fips_npub + last_fips fields (serde(default) for
backward-compat with older nodes)
transport/fips.rs: NodeTransport stub, reports unavailable until the
daemon is live so router falls through to Tor cleanly
Federation invites
FederatedNode and FederationInvite carry optional fips_npub
create_invite / accept_invite / peer-joined callback thread it end
to end; signature domain deliberately unchanged — FIPS Noise does
its own session auth, so the unsigned hint only affects path
selection
crate::fips
config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
service.rs: systemctl status/activate/restart/mask wrappers
update.rs: GitHub API check against upstream main; apply stubbed
until per-commit .deb artefact source is decided
RPC + dashboard
fips.status / fips.check-update / fips.apply-update / fips.install /
fips.restart registered in dispatcher
HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
when ready); shows state pill, version, FIPS npub, update button,
activate button when key is present but service is down
ISO + systemd
archipelago-fips.service: conditional on key presence, masked by
default — backend unmasks after onboarding writes the key
build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
.deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
installs it so trixie resolves deps; unit copied + masked
Version bump: 1.3.5 → 1.4.0
Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).
Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
|
|
|
use tokio::process::Command;
|
|
|
|
|
|
2026-04-19 00:42:56 -04:00
|
|
|
use super::DAEMON_PUB_PATH;
|
|
|
|
|
|
feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.
Identity
seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
secp256k1 key, distinct from the Nostr-node key for crypto isolation
but still seed-recoverable
identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors
Transport
TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
→ router prefers FIPS over Tor for all peer traffic
PeerRecord gains fips_npub + last_fips fields (serde(default) for
backward-compat with older nodes)
transport/fips.rs: NodeTransport stub, reports unavailable until the
daemon is live so router falls through to Tor cleanly
Federation invites
FederatedNode and FederationInvite carry optional fips_npub
create_invite / accept_invite / peer-joined callback thread it end
to end; signature domain deliberately unchanged — FIPS Noise does
its own session auth, so the unsigned hint only affects path
selection
crate::fips
config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
service.rs: systemctl status/activate/restart/mask wrappers
update.rs: GitHub API check against upstream main; apply stubbed
until per-commit .deb artefact source is decided
RPC + dashboard
fips.status / fips.check-update / fips.apply-update / fips.install /
fips.restart registered in dispatcher
HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
when ready); shows state pill, version, FIPS npub, update button,
activate button when key is present but service is down
ISO + systemd
archipelago-fips.service: conditional on key presence, masked by
default — backend unmasks after onboarding writes the key
build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
.deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
installs it so trixie resolves deps; unit copied + masked
Version bump: 1.3.5 → 1.4.0
Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).
Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
|
|
|
/// `systemctl is-active <unit>` → "active" / "inactive" / "failed" / "masked"
|
|
|
|
|
/// / "unknown". Never errors; returns "unknown" on any failure.
|
|
|
|
|
pub async fn unit_state(unit: &str) -> String {
|
|
|
|
|
match Command::new("systemctl")
|
|
|
|
|
.args(["is-active", unit])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(out) => {
|
|
|
|
|
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
|
|
|
if s.is_empty() {
|
|
|
|
|
"unknown".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => "unknown".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether the `fips` debian package is installed on the host.
|
|
|
|
|
pub async fn package_installed() -> bool {
|
|
|
|
|
// dpkg-query -W -f='${Status}' fips → "install ok installed" when present.
|
|
|
|
|
let out = Command::new("dpkg-query")
|
|
|
|
|
.args(["-W", "-f=${Status}", "fips"])
|
|
|
|
|
.output()
|
|
|
|
|
.await;
|
|
|
|
|
match out {
|
|
|
|
|
Ok(o) if o.status.success() => {
|
|
|
|
|
String::from_utf8_lossy(&o.stdout).contains("install ok installed")
|
|
|
|
|
}
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `fipsctl --version` output stripped of the "fipsctl " prefix if present.
|
|
|
|
|
pub async fn daemon_version() -> Result<String> {
|
|
|
|
|
let out = Command::new("fipsctl")
|
|
|
|
|
.arg("--version")
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("fipsctl --version failed to launch")?;
|
|
|
|
|
if !out.status.success() {
|
|
|
|
|
anyhow::bail!("fipsctl exited with non-zero status");
|
|
|
|
|
}
|
|
|
|
|
let raw = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
|
|
|
Ok(raw
|
|
|
|
|
.strip_prefix("fipsctl ")
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.unwrap_or(raw))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `sudo systemctl <verb> <unit>` — returns stderr on non-zero exit.
|
|
|
|
|
async fn sudo_systemctl(verb: &str, unit: &str) -> Result<()> {
|
|
|
|
|
let out = Command::new("sudo")
|
|
|
|
|
.args(["systemctl", verb, unit])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("sudo systemctl {} {} failed to launch", verb, unit))?;
|
|
|
|
|
if !out.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
|
|
|
anyhow::bail!("systemctl {} {}: {}", verb, unit, stderr);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Unmask + start + enable the FIPS service. Idempotent — safe to call
|
|
|
|
|
/// on every backend startup once the key is on disk.
|
|
|
|
|
pub async fn activate(unit: &str) -> Result<()> {
|
|
|
|
|
// Order matters: unmask before enable/start, otherwise enable fails
|
|
|
|
|
// on a masked unit.
|
|
|
|
|
sudo_systemctl("unmask", unit).await?;
|
|
|
|
|
sudo_systemctl("enable", unit).await?;
|
|
|
|
|
sudo_systemctl("start", unit).await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn stop(unit: &str) -> Result<()> {
|
|
|
|
|
sudo_systemctl("stop", unit).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn restart(unit: &str) -> Result<()> {
|
|
|
|
|
sudo_systemctl("restart", unit).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 07:08:26 -04:00
|
|
|
/// Resolve which systemd unit is actually supervising the fips daemon
|
|
|
|
|
/// on this host. Nodes installed from the archipelago ISO run
|
|
|
|
|
/// `archipelago-fips.service`; nodes that were apt-installed (or had
|
|
|
|
|
/// fips running before archipelago took over) may only have the
|
|
|
|
|
/// upstream `fips.service`. Restart/Reconnect must operate on whichever
|
|
|
|
|
/// one is running, otherwise the UI button is a silent no-op.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the archipelago-managed unit name if it's active,
|
|
|
|
|
/// else the upstream unit name if that's active,
|
|
|
|
|
/// else the archipelago-managed name as a default (so activate() can
|
|
|
|
|
/// bring it up).
|
|
|
|
|
pub async fn active_unit() -> &'static str {
|
|
|
|
|
if unit_state(super::SERVICE_UNIT).await == "active" {
|
|
|
|
|
return super::SERVICE_UNIT;
|
|
|
|
|
}
|
|
|
|
|
if unit_state(super::UPSTREAM_SERVICE_UNIT).await == "active" {
|
|
|
|
|
return super::UPSTREAM_SERVICE_UNIT;
|
|
|
|
|
}
|
|
|
|
|
super::SERVICE_UNIT
|
|
|
|
|
}
|
|
|
|
|
|
feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.
Identity
seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
secp256k1 key, distinct from the Nostr-node key for crypto isolation
but still seed-recoverable
identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors
Transport
TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
→ router prefers FIPS over Tor for all peer traffic
PeerRecord gains fips_npub + last_fips fields (serde(default) for
backward-compat with older nodes)
transport/fips.rs: NodeTransport stub, reports unavailable until the
daemon is live so router falls through to Tor cleanly
Federation invites
FederatedNode and FederationInvite carry optional fips_npub
create_invite / accept_invite / peer-joined callback thread it end
to end; signature domain deliberately unchanged — FIPS Noise does
its own session auth, so the unsigned hint only affects path
selection
crate::fips
config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
service.rs: systemctl status/activate/restart/mask wrappers
update.rs: GitHub API check against upstream main; apply stubbed
until per-commit .deb artefact source is decided
RPC + dashboard
fips.status / fips.check-update / fips.apply-update / fips.install /
fips.restart registered in dispatcher
HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
when ready); shows state pill, version, FIPS npub, update button,
activate button when key is present but service is down
ISO + systemd
archipelago-fips.service: conditional on key presence, masked by
default — backend unmasks after onboarding writes the key
build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
.deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
installs it so trixie resolves deps; unit copied + masked
Version bump: 1.3.5 → 1.4.0
Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).
Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
|
|
|
pub async fn mask(unit: &str) -> Result<()> {
|
|
|
|
|
let _ = sudo_systemctl("stop", unit).await;
|
|
|
|
|
let _ = sudo_systemctl("disable", unit).await;
|
|
|
|
|
sudo_systemctl("mask", unit).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 08:40:31 -04:00
|
|
|
/// Known public anchor npub (fips.v0l.io as of 2026-04). Used to decide
|
|
|
|
|
/// whether the `anchor_connected` badge in the dashboard lights up.
|
|
|
|
|
pub const PUBLIC_ANCHOR_NPUB: &str =
|
|
|
|
|
"npub1zv58cn7v83mxvttl70w5fwjwuclfmntv9cnmv5wmz2nzz88u5urqvdx96n";
|
|
|
|
|
|
2026-04-21 07:08:26 -04:00
|
|
|
/// Summarise peer connectivity from `fipsctl show peers`. Returns
|
|
|
|
|
/// `(authenticated_peer_count, anchor_connected)`.
|
|
|
|
|
///
|
|
|
|
|
/// `anchor_candidates` is the operator-controlled list of npubs this
|
|
|
|
|
/// node considers a valid mesh anchor — always includes the hard-coded
|
|
|
|
|
/// public anchor, plus any entries from `seed-anchors.json`. A node is
|
|
|
|
|
/// "anchor connected" when at least one currently-authenticated peer
|
|
|
|
|
/// matches one of these npubs. We used to check the identity cache
|
|
|
|
|
/// (which includes transient hearsay from other peers), but a cache
|
|
|
|
|
/// hit on `fips.v0l.io` didn't mean we could actually route through
|
|
|
|
|
/// it, and the card lied to users whose mesh was federated through
|
|
|
|
|
/// their own seed anchors instead.
|
|
|
|
|
pub async fn peer_connectivity_summary(anchor_candidates: &[String]) -> (u32, bool) {
|
2026-04-19 08:40:31 -04:00
|
|
|
let peers_json = match Command::new("sudo")
|
|
|
|
|
.args(["-n", "fipsctl", "show", "peers"])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(o) if o.status.success() => o.stdout,
|
|
|
|
|
_ => return (0, false),
|
|
|
|
|
};
|
2026-04-28 15:00:58 -04:00
|
|
|
let parsed: serde_json::Value = match serde_json::from_slice(&peers_json) {
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(_) => return (0, false),
|
|
|
|
|
};
|
2026-04-21 07:08:26 -04:00
|
|
|
let peers = parsed
|
|
|
|
|
.get("peers")
|
|
|
|
|
.and_then(|p| p.as_array())
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let authenticated_peer_count = peers.len() as u32;
|
|
|
|
|
let anchor_connected = peers.iter().any(|p| {
|
|
|
|
|
let npub = p.get("npub").and_then(|n| n.as_str()).unwrap_or_default();
|
|
|
|
|
let connected = p
|
|
|
|
|
.get("connectivity")
|
|
|
|
|
.and_then(|c| c.as_str())
|
|
|
|
|
.map(|s| s == "connected")
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
connected && anchor_candidates.iter().any(|a| a == npub)
|
|
|
|
|
});
|
2026-04-19 08:40:31 -04:00
|
|
|
(authenticated_peer_count, anchor_connected)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:42:56 -04:00
|
|
|
/// Read the upstream daemon's public key at `/etc/fips/fips.pub` and return
|
|
|
|
|
/// it as a bech32 npub. Returns `Ok(None)` if the file doesn't exist — used
|
|
|
|
|
/// as a fallback on legacy/dev nodes where no seed-derived key exists.
|
|
|
|
|
///
|
|
|
|
|
/// Upstream writes the key as a bech32 string (`npub1…`); older builds may
|
|
|
|
|
/// have written 32 raw bytes, so we accept either form.
|
|
|
|
|
pub async fn read_upstream_npub() -> Result<Option<String>> {
|
|
|
|
|
let bytes = match tokio::fs::read(DAEMON_PUB_PATH).await {
|
|
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
|
|
|
|
Err(e) => return Err(e).context("read /etc/fips/fips.pub"),
|
|
|
|
|
};
|
|
|
|
|
if let Ok(s) = std::str::from_utf8(&bytes) {
|
|
|
|
|
let trimmed = s.trim();
|
|
|
|
|
if trimmed.starts_with("npub1") {
|
|
|
|
|
if let Ok(pk) = nostr_sdk::PublicKey::parse(trimmed) {
|
|
|
|
|
return Ok(pk.to_bech32().ok());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let pk = nostr_sdk::PublicKey::from_slice(&bytes)
|
|
|
|
|
.context("parse /etc/fips/fips.pub as secp256k1 public key")?;
|
|
|
|
|
Ok(pk.to_bech32().ok())
|
|
|
|
|
}
|
|
|
|
|
|
feat(fips): integrate jmcorgan/fips as preferred non-Tor transport + v1.4.0
Bakes the FIPS (Free Internetworking Peering System) mesh daemon into
the node stack, supervised by archipelago alongside Tor. Runs as a
system service, identity derives from the same BIP-39 master seed, and
user-triggered updates track upstream main.
Identity
seed.rs: new HKDF label archipelago/fips/secp256k1/v1 → dedicated
secp256k1 key, distinct from the Nostr-node key for crypto isolation
but still seed-recoverable
identity.rs: writes fips_key[.pub] to /data/identity on onboarding,
chmod 0600; fips_key_exists / load_fips_keys / fips_npub accessors
Transport
TransportKind::Fips=3 inserted between LAN and Tor (Tor bumps to 4)
→ router prefers FIPS over Tor for all peer traffic
PeerRecord gains fips_npub + last_fips fields (serde(default) for
backward-compat with older nodes)
transport/fips.rs: NodeTransport stub, reports unavailable until the
daemon is live so router falls through to Tor cleanly
Federation invites
FederatedNode and FederationInvite carry optional fips_npub
create_invite / accept_invite / peer-joined callback thread it end
to end; signature domain deliberately unchanged — FIPS Noise does
its own session auth, so the unsigned hint only affects path
selection
crate::fips
config.rs: renders /etc/fips/fips.yaml and sudo-installs key material
service.rs: systemctl status/activate/restart/mask wrappers
update.rs: GitHub API check against upstream main; apply stubbed
until per-commit .deb artefact source is decided
RPC + dashboard
fips.status / fips.check-update / fips.apply-update / fips.install /
fips.restart registered in dispatcher
HomeNetworkCard.vue shipped standalone (unmounted — place in Home.vue
when ready); shows state pill, version, FIPS npub, update button,
activate button when key is present but service is down
ISO + systemd
archipelago-fips.service: conditional on key presence, masked by
default — backend unmasks after onboarding writes the key
build-auto-installer-iso.sh: multi-stage Dockerfile builds the FIPS
.deb from jmcorgan/fips main (fail-loud), COPYs it into rootfs, apt
installs it so trixie resolves deps; unit copied + masked
Version bump: 1.3.5 → 1.4.0
Tests: 33 new/updated passing (seed, identity, transport, federation,
fips module, transport::fips).
Known gaps: fips.apply-update returns a clear stub error until
upstream publishes per-commit .deb artefacts; HomeNetworkCard is not
mounted in Home.vue by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:57:51 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_unit_state_returns_string_for_bogus_unit() {
|
|
|
|
|
// Nonexistent unit: systemctl returns "inactive" or "unknown" — we
|
|
|
|
|
// just care that the helper doesn't panic and returns *something*.
|
|
|
|
|
let s = unit_state("archipelago-bogus-test.service").await;
|
|
|
|
|
assert!(!s.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_package_installed_is_bool() {
|
|
|
|
|
// Must not panic regardless of host state.
|
|
|
|
|
let _ = package_installed().await;
|
|
|
|
|
}
|
|
|
|
|
}
|