use super::*;
use crate::{Command, apply_command};
use chrono::Utc;
use dpm_model::{ActorId, DecisionStatus, Key, WorkItemId, WorkStatus};
fn fixture() -> Plan {
serde_json::from_str(include_str!(
"../../../../tests/support/execution-plan.json"
))
.expect("fixture")
}
fn apply(plan: &mut Plan, proposed: Plan) -> Result<crate::Operation, EngineError> {
crate::apply_plan_change(
plan,
ActorId::human("planner"),
&proposed,
"clarify remaining work",
Utc::now(),
dpm_model::OperationId::new(),
)
}
#[test]
fn proposals_show_semantic_changes_and_apply_as_one_reviewed_operation() {
let mut plan = fixture();
let before = plan.clone();
let mut proposal = plan.clone();
proposal.find_work_by_key_mut("TEST-A").expect("task").title = "Clarified contract".into();
let mut added = proposal.find_work_by_key("TEST-A").expect("task").clone();
added.id = WorkItemId::new();
added.key = Key::new("NEW");
added.execution.status = WorkStatus::Proposed;
added.execution.artifact_ids.clear();
proposal.work_items.insert(added.id, added.clone());
let preview = propose_change(&plan, &proposal).expect("proposal");
assert_eq!(plan, before);
assert_eq!(preview.base_revision, 0);
assert_eq!(preview.changes.len(), 2);
assert!(preview.changes.iter().any(|c| c.fields == ["title"]));
assert!(
preview
.changes
.iter()
.any(|c| c.before.is_null() && c.after["key"] == "NEW")
);
let operation = apply(&mut plan, proposal).expect("apply");
assert_eq!(operation.base_revision, 0);
assert_eq!(plan.revision, 1);
assert!(!crate::is_ready(
&plan,
&plan.work_items[&added.id],
chrono::Utc::now()
));
assert_eq!(
plan.find_work_by_key("TEST-A").expect("work").title,
"Clarified contract"
);
}
#[test]
fn agent_approval_empty_reason_stale_revision_and_noop_fail_atomically() {
let mut plan = fixture();
let before = plan.clone();
let mut proposal = plan.clone();
proposal.workspace.name = "new name".into();
for (actor, reason) in [
(ActorId::agent("drafter"), "change"),
(ActorId::human("lead"), " "),
] {
assert!(
crate::apply_plan_change(
&mut plan,
actor,
&proposal.clone(),
reason,
Utc::now(),
dpm_model::OperationId::new()
)
.is_err()
);
assert_eq!(plan, before);
}
assert!(apply(&mut plan, before.clone()).is_err());
proposal.revision = 99;
assert!(matches!(
apply(&mut plan, proposal),
Err(EngineError::RevisionConflict { .. })
));
assert_eq!(plan, before);
}
#[test]
fn invalid_graphs_and_fabricated_execution_fail_without_changes() {
let mut plan = fixture();
let before = plan.clone();
for mutation in 0..5 {
let mut proposal = plan.clone();
match mutation {
0 => proposal.dependencies.push(dpm_model::Dependency::new(
proposal.dependencies[0].successor,
proposal.dependencies[0].predecessor,
dpm_model::DependencyKind::FinishStart,
0.0,
)),
1 => {
proposal
.find_work_by_key_mut("TEST-A")
.expect("task")
.execution
.owner = Some(ActorId::agent("fake"));
}
2 => {
let work = proposal.find_work_by_key_mut("TEST-A").expect("task");
work.execution.status = WorkStatus::Verified;
work.execution.owner = Some(ActorId::agent("fake"));
}
3 => {
let decision = proposal.decisions.values_mut().next().expect("gate");
decision.status = DecisionStatus::Decided;
decision.outcome = Some("bypass".into());
}
_ => {
proposal.workspace.id = dpm_model::WorkspaceId::new();
}
}
assert!(apply(&mut plan, proposal).is_err());
assert_eq!(plan, before);
}
}
#[test]
fn execution_locks_its_contract_context_and_dependencies_but_not_future_work() {
let mut plan = fixture();
let work = plan.find_work_by_key("TEST-A").expect("work").id;
apply_command(
&mut plan,
ActorId::agent("worker"),
Command::Claim { work },
Utc::now(),
dpm_model::OperationId::new(),
)
.expect("claim");
let before = plan.clone();
for mutation in 0..4 {
let mut proposal = plan.clone();
match mutation {
0 => proposal
.work_items
.get_mut(&work)
.expect("work")
.contract
.acceptance
.clear(),
1 => {
proposal
.requirements
.values_mut()
.next()
.expect("requirement")
.statement = "lowered bar".into();
}
2 => {
proposal.assets.values_mut().next().expect("asset").label =
"other repository".into();
}
_ => {
let mut gate = proposal
.decisions
.values()
.next()
.expect("decision")
.clone();
gate.id = dpm_model::DecisionId::new();
gate.key = Key::new("NEW-GATE");
gate.blocks.insert(work);
proposal.decisions.insert(gate.id, gate);
}
}
assert!(apply(&mut plan, proposal).is_err());
assert_eq!(plan, before);
}
let mut proposal = plan.clone();
proposal.find_work_by_key_mut("TEST-F").expect("work").title = "Future integration".into();
apply(&mut plan, proposal).expect("future work remains editable");
}
#[test]
fn deletion_is_explicit_in_diff_and_order_only_changes_are_not_semantic() {
let plan = fixture();
let mut proposal = plan.clone();
proposal.dependencies.reverse();
assert!(
propose_change(&plan, &proposal)
.expect("order independent")
.changes
.is_empty()
);
let id = proposal.find_work_by_key("TEST-F").expect("task").id;
proposal.work_items.remove(&id);
proposal
.dependencies
.retain(|d| d.predecessor != id && d.successor != id);
let preview = propose_change(&plan, &proposal).expect("explicit removal");
assert!(
preview
.changes
.iter()
.any(|c| c.after.is_null() && c.before["key"] == "TEST-F")
);
}
fn work(plan: &mut Plan, id: WorkItemId) -> &mut dpm_model::WorkItem {
plan.work_items.get_mut(&id).expect("work")
}
#[test]
fn dangling_references_and_illegal_milestones_fail_before_any_change() {
let mut plan = fixture();
let before = plan.clone();
let task = plan.find_work_by_key("TEST-F").expect("task").id;
let milestone = plan.find_work_by_key("TEST-M1").expect("milestone").id;
let estimate = plan.work_items[&task].schedule.estimate;
for case in 0..10 {
let mut proposal = plan.clone();
match case {
0 => proposal.dependencies.push(dpm_model::Dependency::new(
WorkItemId::new(),
task,
dpm_model::DependencyKind::FinishStart,
0.0,
)),
1 => drop(proposal.work_items.remove(&task)),
2 => {
let risk = proposal.risks.values_mut().next().expect("risk");
risk.related_work.insert(WorkItemId::new());
}
3 => drop(
work(&mut proposal, task)
.contract
.requirement_ids
.insert(dpm_model::RequirementId::new()),
),
4 => proposal.assets.clear(),
5 => work(&mut proposal, task).parent = Some(WorkItemId::new()),
6 => work(&mut proposal, milestone).schedule.estimate = estimate,
7 => work(&mut proposal, milestone).execution.owner = Some(ActorId::agent("fake")),
8 => work(&mut proposal, task).parent = Some(milestone),
_ => work(&mut proposal, milestone).kind = dpm_model::WorkKind::Task,
}
assert!(propose_change(&plan, &proposal).is_err(), "case {case}");
assert!(apply(&mut plan, proposal).is_err(), "case {case}");
assert_eq!(plan, before, "case {case}");
}
}
#[test]
fn plan_changes_cannot_author_or_rewrite_event_times() {
let mut plan = fixture();
let task = plan.find_work_by_key("TEST-A").expect("task").id;
let gate = plan.find_decision_by_key("TEST-GATE").expect("gate").id;
let worker = ActorId::agent("worker");
for command in [
Command::Claim { work: task },
Command::Start {
work: task,
occurred_at: None,
},
] {
apply_command(
&mut plan,
worker.clone(),
command,
Utc::now(),
dpm_model::OperationId::new(),
)
.expect("execute");
}
let decide = Command::Decide {
decision: gate,
outcome: "go".into(),
};
apply_command(
&mut plan,
ActorId::human("lead"),
decide,
Utc::now(),
dpm_model::OperationId::new(),
)
.expect("decide");
let before = plan.clone();
let earlier = Utc::now() - chrono::TimeDelta::days(30);
for case in 0..3 {
let mut proposal = plan.clone();
match case {
0 => {
let work = proposal.work_items.get_mut(&task).expect("task");
work.execution.events.started_at = Some(earlier);
}
1 => proposal.decisions.get_mut(&gate).expect("gate").resolved_at = Some(earlier),
_ => proposal.decisions.get_mut(&gate).expect("gate").resolved_at = None,
}
proposal
.validate()
.expect("valid shape; only protection refuses it");
assert!(apply(&mut plan, proposal).is_err(), "case {case}");
assert_eq!(plan, before, "case {case}");
}
}
#[test]
fn calendars_are_added_edited_and_removed_as_one_reviewed_value() {
let mut plan = fixture();
let mut proposal = plan.clone();
proposal.calendars = Some(dpm_model::Calendars::in_zone("Europe/Berlin"));
let preview = propose_change(&plan, &proposal).expect("add calendars");
assert_eq!(preview.changes.len(), 1);
let added = preview.changes.first().expect("change");
assert_eq!(
(added.collection.as_str(), added.id.as_ref()),
("calendars", None)
);
assert!(added.before.is_null());
let stale = patch(&proposal, &preview.changes);
assert!(matches!(stale, Err(EngineError::StaleChange { .. })));
apply(&mut plan, proposal.clone()).expect("apply calendars");
assert_eq!(plan.calendars, proposal.calendars);
let mut edited = plan.clone();
if let Some(calendars) = edited.calendars.as_mut() {
calendars.verifier = dpm_model::ActorKind::Agent;
}
let preview = propose_change(&plan, &edited).expect("edit calendars");
assert_eq!(
preview.changes.first().map(|c| c.fields.clone()),
Some(vec!["verifier".to_owned()])
);
let replayed = patch(&plan, &preview.changes).expect("replay");
assert_eq!(replayed.calendars, edited.calendars);
let mut removed = plan.clone();
removed.calendars = None;
apply(&mut plan, removed).expect("remove calendars");
assert!(plan.calendars.is_none());
}