use super::{Plan, StepStatus};
use crate::services::lifecycle::site::{
SiteChange,
test_support::{Fixture, TestResult, limits, spec},
};
#[test]
fn one_doing_step_and_finished_ordinals_are_preserved() -> TestResult {
let limits = limits()?;
let mut plan = Plan::default();
plan.set(&["Design".into(), "Build".into()], "builder", 1, &limits)?;
plan.step(1, StepStatus::Doing, None, "builder", 2, &limits)?;
let before = serde_json::to_value(&plan)?;
let refused = plan
.step(2, StepStatus::Doing, None, "builder", 3, &limits)
.err()
.ok_or("two doing steps admitted")?;
assert_eq!(refused.to_string(), "step 1 is already doing");
assert_eq!(serde_json::to_value(&plan)?, before);
plan.step(1, StepStatus::Done, None, "builder", 4, &limits)?;
let refused = plan
.set(&["Changed".into()], "builder", 5, &limits)
.err()
.ok_or("done ordinal removed")?;
assert_eq!(
refused.to_string(),
"done step 1 cannot be removed or renamed"
);
assert_eq!(plan.revision, 3);
assert_eq!(plan.transitions.len(), 3);
Ok(())
}
#[test]
fn builder_plan_is_durable_and_watchers_read_current_before_changes() -> TestResult {
let mut fixture = Fixture::new()?;
fixture.execute("create", SiteChange::CreateSite(spec()))?;
fixture.execute(
"bind",
SiteChange::BindBuilder {
agent: "builder".into(),
},
)?;
let titles = vec!["Design".into(), "Build".into()];
fixture.lifecycle.plan_set("echo", "builder", &titles, 20)?;
let mut watch = fixture
.lifecycle
.subscribe_plan(11, "services.echo.plan", &[], None)?;
let initial = watch
.try_next()
.ok_or("current plan missing before changes")?;
let initial: serde_json::Value = serde_json::from_slice(&initial.payload)?;
assert_eq!(initial["revision"], 1);
fixture.lifecycle.plan_step(
"echo",
"builder",
1,
StepStatus::Doing,
Some("Writing contracts".into()),
21,
)?;
let next = watch.try_next().ok_or("plan change not delivered")?;
let next: serde_json::Value = serde_json::from_slice(&next.payload)?;
assert_eq!(next["revision"], 2);
assert_eq!(next["current"], 1);
let refused = fixture
.lifecycle
.plan_set("echo", "assistant", &titles, 22)
.err()
.ok_or("assistant wrote a plan")?;
assert_eq!(
refused.to_string(),
"pass holder assistant is not the builder of echo"
);
assert!(!watch.has_pending());
let before = fixture.lifecycle.record("echo")?;
drop(watch);
let directory = std::mem::replace(&mut fixture.directory, tempfile::tempdir()?);
let issuer = fixture.issuer.clone();
drop(fixture);
let reopened = crate::services::lifecycle::Lifecycle::open(
&[],
directory.path().join("services"),
"fixture-network".into(),
Some(issuer),
)?;
let scheduler = liminal::channel::ChannelSupervisor::new()?;
let retained = crate::agents::retained::Retained::new(
crate::agents::config::AgentsConfig {
max_event_bytes: 65536,
..crate::agents::config::AgentsConfig::default()
},
scheduler.clone(),
"fixture",
);
reopened.configure_sites(
limits()?,
crate::services::lifecycle::site::test_support::open_store(
&directory.path().join("store"),
)?,
None,
std::sync::Arc::new(crate::services::lifecycle::site::test_support::Issuer::default()),
retained,
)?;
assert_eq!(reopened.record("echo")?, before);
assert_eq!(reopened.record("echo")?.plan.revision, 2);
assert_eq!(reopened.record("echo")?.plan.transitions.len(), 2);
scheduler.shutdown();
Ok(())
}
#[test]
fn plan_capacity_refuses_without_discarding_history() -> TestResult {
let mut limits = limits()?;
limits.transitions = std::num::NonZeroUsize::new(1).ok_or("bad limit")?;
let mut plan = Plan::default();
plan.set(&["One".into()], "builder", 1, &limits)?;
assert!(
plan.step(1, StepStatus::Done, None, "builder", 2, &limits)
.is_err()
);
assert_eq!(plan.revision, 1);
assert_eq!(plan.transitions.len(), 1);
assert_eq!(plan.steps[0].status, StepStatus::Todo);
Ok(())
}
#[test]
fn malformed_restored_ordinals_and_history_refuse_before_serving() -> TestResult {
let limits = limits()?;
let mut plan = Plan::default();
plan.set(&["One".into()], "builder", 1, &limits)?;
plan.validate(&limits)?;
let mut invalid = plan.clone();
invalid.steps[0].id = 0;
assert_eq!(
invalid
.validate(&limits)
.err()
.ok_or("zero ordinal restored")?
.to_string(),
"site_invalid: stored plan ordinal"
);
let mut invalid = plan.clone();
invalid.current = Some(1);
assert!(invalid.validate(&limits).is_err());
plan.transitions.clear();
assert!(plan.validate(&limits).is_err());
Ok(())
}
#[test]
fn completion_is_refused_while_a_plan_step_is_not_done() -> TestResult {
let fixture = Fixture::new()?;
fixture.execute("create", SiteChange::CreateSite(spec()))?;
fixture.execute(
"bind",
SiteChange::BindBuilder {
agent: "builder".into(),
},
)?;
let titles = vec!["Design".into(), "Build".into(), "Prove".into()];
fixture.lifecycle.plan_set("echo", "builder", &titles, 20)?;
fixture
.lifecycle
.plan_step("echo", "builder", 1, StepStatus::Done, None, 21)?;
fixture
.lifecycle
.plan_step("echo", "builder", 3, StepStatus::Blocked, None, 22)?;
let outcome = fixture.execute("complete", SiteChange::Complete)?;
let lys_pass::drafts::Execution::RefusedOnExecution { reason, .. } = outcome else {
return Err("a site completed with steps not done".into());
};
assert_eq!(
reason,
"plan steps not done: 2, 3; mark them done before completing"
);
for step in [2, 3] {
fixture
.lifecycle
.plan_step("echo", "builder", step, StepStatus::Done, None, 23)?;
}
let outcome = fixture.execute("complete-again", SiteChange::Complete)?;
let lys_pass::drafts::Execution::RefusedOnExecution { reason, .. } = outcome else {
return Err("a site completed without a template".into());
};
assert_eq!(reason, "site_invalid: site has no approved template");
Ok(())
}