#![allow(clippy::field_reassign_with_default)]
use forjar::core::parser::{parse_config, validate_config};
use forjar::core::planner::hash_desired_state;
use forjar::core::types::{Resource, ResourceType};
use forjar::resources::overlay_interface::{apply_script, state_query_script};
fn overlay(ip: &str) -> Resource {
let mut r = Resource::default();
r.resource_type = ResourceType::OverlayInterface;
r.overlay_ip = Some(ip.into());
r.sudo = true;
r
}
#[test]
fn contract_service_never_remain_after_exit() {
let s = apply_script(&overlay("10.42.0.11/24"));
assert!(s.contains("Type=oneshot"), "{s}");
assert!(
!s.contains("RemainAfterExit"),
"CONTRACT VIOLATION: service must NEVER set RemainAfterExit: {s}"
);
}
#[test]
fn contract_timer_oncalendar_and_randomized_delay() {
let s = apply_script(&overlay("10.42.0.11/24"));
assert!(s.contains("OnCalendar=minutely"), "{s}");
assert!(
!s.contains("OnUnitActiveSec"),
"CONTRACT VIOLATION: timer must NEVER use OnUnitActiveSec: {s}"
);
assert!(
s.contains("RandomizedDelaySec=30"),
"CONTRACT VIOLATION (MUST-4): timer must carry RandomizedDelaySec jitter: {s}"
);
assert!(
s.contains("AccuracySec=1s"),
"CONTRACT VIOLATION (MUST-4): timer must set AccuracySec=1s so jitter is not coalesced: {s}"
);
}
#[test]
fn contract_arping_dad_probe_before_ip_addr_add() {
let s = apply_script(&overlay("10.42.0.11/24"));
let dad = s
.find("arping -D")
.expect("apply must contain arping -D DAD probe");
let add = s
.find("ip addr add")
.expect("apply must contain ip addr add");
assert!(
dad < add,
"CONTRACT VIOLATION (MUST-1): arping -D must appear BEFORE ip addr add: {s}"
);
assert!(s.contains("arping -D -c 3 -w 3 -I"), "{s}");
assert!(
s.contains("DUPLICATE ADDRESS"),
"CONTRACT VIOLATION (MUST-1): DAD conflict must be a loud refusal: {s}"
);
assert!(
s.contains("arping not installed") && s.contains("command -v arping"),
"DAD probe must be guarded for arping-absent: {s}"
);
}
#[test]
fn contract_writes_status_json_and_fails_on_persistent_absent() {
let s = apply_script(&overlay("10.42.0.11/24"));
assert!(s.contains("/run/fleet-overlay/status.json"), "{s}");
for field in [
"last_converged",
"ip_present_before",
"action_taken",
"repair_count",
"ip_present_after",
] {
assert!(
s.contains(field),
"CONTRACT VIOLATION (MUST-2): status.json must carry `{field}`: {s}"
);
}
assert!(s.contains("REPAIRS=$((REPAIRS + 1))"), "{s}");
assert!(s.contains("/run/fleet-overlay/repair_count"), "{s}");
assert!(
s.contains("STILL ABSENT") && s.contains("exit 1"),
"CONTRACT VIOLATION (MUST-2): persistent-absent must exit nonzero: {s}"
);
}
#[test]
fn contract_state_query_surfaces_heartbeat_without_false_drift() {
let s = state_query_script(&overlay("10.42.0.11/24"));
assert!(s.contains("overlay_heartbeat="), "{s}");
assert!(s.contains("overlay_heartbeat=stale"), "{s}");
assert!(s.contains("overlay_heartbeat=fresh"), "{s}");
let stderr_line = s
.lines()
.find(|l| l.contains("overlay_repair_count="))
.expect("repair_count line present");
assert!(
stderr_line.contains(">&2"),
"CONTRACT VIOLATION (MUST-2): raw repair_count/last_converged must go to stderr to avoid false drift: {s}"
);
}
#[test]
fn contract_duplicate_overlay_ip_rejected_at_validate() {
let yaml = r#"
version: "1.0"
name: test
machines:
intel:
hostname: intel
addr: 127.0.0.1
user: test
resources:
fleet-a:
type: overlay_interface
machine: intel
sudo: true
overlay_ip: "10.42.0.11/24"
fleet-b:
type: overlay_interface
machine: intel
sudo: true
overlay_ip: "10.42.0.11/16"
"#;
let config = parse_config(yaml).expect("parses as YAML");
let errors = validate_config(&config);
let dup = errors
.iter()
.find(|e| e.message.contains("injective") && e.message.contains("10.42.0.11"));
let msg = dup.map(|e| e.message.clone()).unwrap_or_default();
assert!(
dup.is_some(),
"CONTRACT VIOLATION (MUST-1): duplicate overlay_ip must be a validation error: {errors:?}"
);
assert!(
msg.contains("fleet-a") && msg.contains("fleet-b"),
"duplicate-IP error must name BOTH resources: {msg}"
);
}
#[test]
fn contract_distinct_overlay_ips_accepted() {
let yaml = r#"
version: "1.0"
name: test
machines:
intel:
hostname: intel
addr: 127.0.0.1
user: test
resources:
fleet-a:
type: overlay_interface
machine: intel
sudo: true
overlay_ip: "10.42.0.11/24"
fleet-b:
type: overlay_interface
machine: intel
sudo: true
overlay_ip: "10.42.0.12/24"
"#;
let config = parse_config(yaml).expect("parses as YAML");
let errors = validate_config(&config);
assert!(
!errors.iter().any(|e| e.message.contains("injective")),
"distinct overlay_ips must NOT trip the injectivity check: {errors:?}"
);
}
#[test]
fn contract_changing_overlay_hosts_changes_desired_hash() {
let mut a = overlay("10.42.0.11/24");
let mut ha = std::collections::HashMap::new();
ha.insert("intel".to_string(), "10.42.0.11".to_string());
a.overlay_hosts = Some(ha);
let mut b = overlay("10.42.0.11/24");
let mut hb = std::collections::HashMap::new();
hb.insert("intel".to_string(), "10.42.0.11".to_string());
hb.insert("mini".to_string(), "10.42.0.12".to_string());
b.overlay_hosts = Some(hb);
assert_ne!(
hash_desired_state(&a),
hash_desired_state(&b),
"CONTRACT VIOLATION (MATERIAL): overlay_hosts must affect the desired-state hash"
);
}
#[test]
fn contract_overlay_hosts_hash_is_order_independent() {
let mut a = overlay("10.42.0.11/24");
let mut ha = std::collections::HashMap::new();
ha.insert("intel".to_string(), "10.42.0.11".to_string());
ha.insert("mini".to_string(), "10.42.0.12".to_string());
ha.insert("lambda-labs".to_string(), "10.42.0.10".to_string());
a.overlay_hosts = Some(ha);
let mut b = overlay("10.42.0.11/24");
let mut hb = std::collections::HashMap::new();
hb.insert("lambda-labs".to_string(), "10.42.0.10".to_string());
hb.insert("mini".to_string(), "10.42.0.12".to_string());
hb.insert("intel".to_string(), "10.42.0.11".to_string());
b.overlay_hosts = Some(hb);
assert_eq!(
hash_desired_state(&a),
hash_desired_state(&b),
"same overlay_hosts entries must hash identically regardless of insertion order"
);
}
#[test]
fn contract_apply_idempotent_hash_stable() {
let mut r = overlay("10.42.0.15/24");
let mut h = std::collections::HashMap::new();
h.insert("intel".to_string(), "10.42.0.15".to_string());
r.overlay_hosts = Some(h);
r.overlay_firewall = Some(true);
assert_eq!(
hash_desired_state(&r),
hash_desired_state(&r),
"CONTRACT VIOLATION: re-running apply on an unchanged resource must remain a NoOp (stable hash)"
);
}