use super::test_support::{Fixture, TestResult, limits, spec};
use super::{SiteChange, validate_contracts};
use crate::services::lifecycle::ServiceState;
use lys_pass::drafts::Execution;
fn private_tempdir() -> std::io::Result<tempfile::TempDir> {
use std::os::unix::fs::PermissionsExt;
tempfile::Builder::new()
.permissions(std::fs::Permissions::from_mode(0o700))
.tempdir()
}
#[test]
fn contracts_are_required_before_a_builder_can_exist() -> TestResult {
let mut missing = spec();
missing.contracts.clear();
let refusal = validate_contracts(&missing, &limits()?)
.err()
.ok_or("missing contract admitted")?;
assert_eq!(refusal.to_string(), "subject echo.request has no schema");
validate_contracts(&spec(), &limits()?)?;
Ok(())
}
#[test]
fn site_approval_bind_refusal_and_unbind_keep_exact_grants_and_history() -> TestResult {
let fixture = Fixture::new()?;
assert!(matches!(
fixture.execute("create", SiteChange::CreateSite(spec()))?,
Execution::Executed { .. }
));
let record = fixture.lifecycle.record("echo")?;
assert_eq!(record.state, ServiceState::Building);
assert!(record.template.is_none());
assert!(record.builder.is_none());
assert!(record.contracts == spec().contracts);
fixture.execute(
"bind",
SiteChange::BindBuilder {
agent: "builder".into(),
},
)?;
let grants = fixture
.issuer
.grants
.lock()
.map_err(|_| "fixture grants poisoned")?;
let rights: Vec<_> = grants.values().cloned().collect();
drop(grants);
assert_eq!(rights.len(), 2);
assert!(rights.contains(&("builder".into(), "echo".into(), "plan".into())));
assert!(rights.contains(&("builder".into(), "echo".into(), "serve".into())));
let refused = fixture.execute(
"second",
SiteChange::BindBuilder {
agent: "second".into(),
},
)?;
let Execution::RefusedOnExecution { reason, .. } = refused else {
return Err("second bind admitted".into());
};
assert_eq!(reason, "site echo has builder builder");
fixture.execute("unbind", SiteChange::UnbindBuilder)?;
assert!(
fixture
.issuer
.grants
.lock()
.map_err(|_| "fixture grants poisoned")?
.is_empty()
);
assert_eq!(fixture.lifecycle.record("echo")?.transitions.len(), 3);
Ok(())
}
#[test]
fn old_install_migrates_once_and_keeps_every_old_transition() -> TestResult {
let directory = private_tempdir()?;
let old = serde_json::json!({"id":"svc-old","name":"old","template":"echo","subjects":["echo.old"],
"state":"held","created_by":"person-owner","transitions":[{"from":"asleep","to":"held","holder":"person-owner","at_ms":1}]});
std::fs::write(
directory.path().join("svc-old.json"),
serde_json::to_vec(&old)?,
)?;
let first = crate::services::lifecycle::Lifecycle::open_bounded(
&[],
directory.path().into(),
"net".into(),
None,
&limits()?,
)?;
let scheduler = liminal::channel::ChannelSupervisor::new()?;
let retained = crate::agents::retained::Retained::new(
crate::agents::config::AgentsConfig::default(),
scheduler.clone(),
"fixture",
);
first.configure_sites(
limits()?,
super::test_support::open_store(&directory.path().join("store"))?,
None,
std::sync::Arc::new(super::test_support::Issuer::default()),
retained.clone(),
)?;
let record = first.record("svc-old")?;
assert_eq!(record.template.as_deref(), Some("echo"));
assert_eq!(record.transitions.len(), 1);
assert!(record.purpose.is_none());
assert!(record.contracts.is_empty());
drop(first);
std::fs::write(
directory.path().join("svc-old.json"),
b"archived bytes are not replayed",
)?;
let second = crate::services::lifecycle::Lifecycle::open_bounded(
&[],
directory.path().into(),
"net".into(),
None,
&limits()?,
)?;
second.configure_sites(
limits()?,
super::test_support::open_store(&directory.path().join("store"))?,
None,
std::sync::Arc::new(super::test_support::Issuer::default()),
retained,
)?;
assert_eq!(second.record("svc-old")?, record);
scheduler.shutdown();
Ok(())
}
#[test]
fn approved_act_has_one_mutation_batch_and_one_flush_beside_preparation() -> TestResult {
let fixture = Fixture::new()?;
let before = fixture.counts.snapshot();
fixture.execute("create", SiteChange::CreateSite(spec()))?;
let counts = fixture.counts.snapshot().since(&before);
assert_eq!(counts.apply_routed_batch_calls, 2);
assert_eq!(counts.apply_routed_batch_items, 2);
assert_eq!(counts.flushes, 2);
assert_eq!(counts.append_calls, 0);
assert_eq!(counts.append_all_calls, 0);
let before = fixture.counts.snapshot();
fixture.execute("create", SiteChange::CreateSite(spec()))?;
let replay = fixture.counts.snapshot().since(&before);
assert_eq!(replay.apply_routed_batch_calls, 0);
assert_eq!(replay.flushes, 0);
assert_eq!(fixture.lifecycle.record("echo")?.transitions.len(), 1);
Ok(())
}
#[test]
fn construction_migration_is_bounded_before_parsing_old_bytes() -> TestResult {
let directory = private_tempdir()?;
std::fs::write(directory.path().join("legacy.json"), [b' '; 128])?;
let mut limits = limits()?;
limits.snapshot_bytes = std::num::NonZeroUsize::new(64).ok_or("invalid bound")?;
let refused = crate::services::lifecycle::Lifecycle::open_bounded(
&[],
directory.path().into(),
"net".into(),
None,
&limits,
)
.err()
.ok_or("oversized old record admitted")?;
assert_eq!(
refused.to_string(),
"registry_unavailable: snapshot_bytes_bound_exceeded"
);
assert!(!directory.path().join("construction/registry.json").exists());
Ok(())
}
#[test]
fn construction_projection_preserves_agents_and_other_devices() -> TestResult {
let fixture = Fixture::new()?;
fixture.execute("create", SiteChange::CreateSite(spec()))?;
let document = serde_json::json!({"rev":1,"stations":[
{"id":"local-worker","device":"fixture","kind":"worker","subjects":["echo.request"]},
{"id":"remote-worker","device":"peer","kind":"worker","subjects":["echo.request"]},
{"id":"agents.builder.inbox","device":"fixture","kind":"agent"}],
"lines":[{"id":"echo.request","kind":"call","stops":["local-worker","remote-worker"]}]});
let bytes = crate::console::topology::with_sites(
&serde_json::to_vec(&document)?,
&fixture.lifecycle,
"fixture",
)?;
let after: serde_json::Value = serde_json::from_slice(&bytes)?;
let stations = after["stations"].as_array().ok_or("stations missing")?;
assert_eq!(stations.len(), 3);
assert!(
stations
.iter()
.any(|station| station["id"] == "remote-worker")
);
assert!(
stations
.iter()
.any(|station| station["id"] == "agents.builder.inbox")
);
assert_eq!(
after["lines"][0]["stops"],
serde_json::json!(["services.echo", "remote-worker"])
);
let site = stations
.iter()
.find(|station| station["service"] == "echo")
.ok_or("site missing")?;
assert_eq!(site["state"], "building");
assert_eq!(site["contracts"][0]["subject"], "echo.request");
Ok(())
}
#[test]
fn a_fresh_registry_is_created_private_whatever_the_umask() -> TestResult {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir()?;
let bounded = root.path().join("bounded");
crate::services::lifecycle::Lifecycle::open_bounded(
&[],
bounded.clone(),
"net".into(),
None,
&limits()?,
)?;
let plain = root.path().join("plain");
crate::services::lifecycle::Lifecycle::open(&[], plain.clone(), "net".into(), None)?;
for dir in [bounded, plain] {
let mode = std::fs::metadata(&dir)?.permissions().mode() & 0o777;
assert_eq!(
mode,
0o700,
"{} was created with mode {mode:o}",
dir.display()
);
}
Ok(())
}