Merge NEXT: planning, generation map and portable configuration on top of the OIDC access model
NEXT predates the access work, so the identity layer follows master: access comes from configured groups, instance administrators replace the site administration, and accounts are created by SSO or by an administrator. Kept from NEXT: manual publication with the Review changes panel and shared generator drafts, portable import/export (meguri/v2), the layered generation map with binding provenance, JSM provider changes, contact/approval semantics and the account-owned Person. Dropped: site administration, local sign-up, and the mandatory unique email with its contact/account migrations. The manual-publication migration is renumbered m_0010 after m_0009_access. Accounts without an email still get a Person in every workspace they reach, a changed sign-in email updates the account's person, and an account that loses access keeps its linked person so schedules stay valid. Instance administrators may use Sync and shared drafts. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
38105c65c2
57 files changed
+6890
-431
No files matched your search
@@ -690,3 +690,107 @@ fn removed_fixed_assignee_is_rejected_in_documents_and_generation() {
|
||||
example.rules[0].blocks[0].kind = "backup".into();
|
||||
assert!(!example.expand().issues.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_team_step_uses_workspace_team_without_resolving_person() {
|
||||
let mut e = Example::new();
|
||||
let block = e
|
||||
.rules
|
||||
.iter_mut()
|
||||
.flat_map(|r| &mut r.blocks)
|
||||
.find(|b| b.kind == "escalation")
|
||||
.unwrap();
|
||||
let mut step = block.steps[0].clone();
|
||||
assert!(
|
||||
!step.team_all,
|
||||
"Legacy steps keep their schedule or person recipient"
|
||||
);
|
||||
step.team_all = true;
|
||||
step.delay = serde_json::from_value(json!({"kind":"literal","value":30})).unwrap();
|
||||
step.recipient =
|
||||
serde_json::from_value(json!({"kind":"variable","variable":Uuid::new_v4()})).unwrap();
|
||||
step.condition = "if-not-acked".into();
|
||||
block.steps.push(step);
|
||||
let out = e.expand();
|
||||
assert!(out.issues.is_empty(), "{:?}", out.issues);
|
||||
let steps: Vec<_> = out
|
||||
.document
|
||||
.escalations
|
||||
.iter()
|
||||
.flat_map(|e| &e.steps)
|
||||
.filter(|s| matches!(s.recipient, Recipient::TeamAll))
|
||||
.collect();
|
||||
assert!(!steps.is_empty());
|
||||
for step in steps {
|
||||
assert_eq!(step.delay_minutes, 30);
|
||||
assert!(matches!(step.condition, EscalationCondition::IfNotAcked));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provenance_preserves_iteration_paths_affixes_and_source_nodes() {
|
||||
let mut e = Example::new();
|
||||
(e.variables, e.rules) = demo::regional_platform(e.day, e.people[0], e.people[1]);
|
||||
let out = e.expand();
|
||||
assert!(out.issues.is_empty());
|
||||
let rows = out.trace.iter().flat_map(|t| t["rows"].as_array().unwrap());
|
||||
let mut saw_path = false;
|
||||
for row in rows {
|
||||
for trace in row["bindings"].as_array().unwrap() {
|
||||
let binding = &trace["binding"];
|
||||
if binding["kind"] == "item" && !binding["path"].as_array().unwrap().is_empty() {
|
||||
saw_path = true;
|
||||
assert!(!trace["parents"].as_array().unwrap().is_empty());
|
||||
assert_eq!(
|
||||
trace["stages"].as_array().unwrap().len(),
|
||||
binding["path"].as_array().unwrap().len() + 1
|
||||
);
|
||||
let last = trace["stages"].as_array().unwrap().last().unwrap();
|
||||
assert!(last["result"]["node_id"].is_string());
|
||||
if let Some(text) = last["result"]["value"].as_str() {
|
||||
assert_eq!(
|
||||
trace["result"]["value"],
|
||||
format!(
|
||||
"{}{}{}",
|
||||
binding["prefix"].as_str().unwrap(),
|
||||
text,
|
||||
binding["suffix"].as_str().unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(saw_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_team_step_ignores_binding_and_rejects_conflicting_types() {
|
||||
let mut e = Example::new();
|
||||
let block = e
|
||||
.rules
|
||||
.iter_mut()
|
||||
.flat_map(|r| &mut r.blocks)
|
||||
.find(|b| b.kind == "escalation")
|
||||
.unwrap();
|
||||
block.steps[0].team_random = true;
|
||||
block.steps[0].user = false;
|
||||
block.steps[0].recipient =
|
||||
serde_json::from_value(json!({"kind":"variable","variable":Uuid::new_v4()})).unwrap();
|
||||
let out = e.expand();
|
||||
assert!(out.issues.is_empty(), "{:?}", out.issues);
|
||||
assert!(
|
||||
out.document
|
||||
.escalations
|
||||
.iter()
|
||||
.any(|p| matches!(p.steps[0].recipient, Recipient::TeamRandom))
|
||||
);
|
||||
e.rules
|
||||
.iter_mut()
|
||||
.flat_map(|r| &mut r.blocks)
|
||||
.find(|b| b.kind == "escalation")
|
||||
.unwrap()
|
||||
.steps[0]
|
||||
.team_all = true;
|
||||
assert!(!e.expand().issues.is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user