1use schemars::JsonSchema;
36use serde::{Deserialize, Serialize};
37
38use crate::incident::{ControlState, IncidentReasonCode};
39use crate::schema_versions::{SchemaComponent, current_version};
40use crate::types::{WorkerSelectionDiagnostic, WorkerSelectionDiagnosticDecision};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
59#[serde(rename_all = "snake_case")]
60pub enum StrictRemotePolicy {
61 #[default]
63 Off,
64 ForceRemote,
66 RequireRemote,
68}
69
70impl StrictRemotePolicy {
71 #[must_use]
73 pub fn as_str(self) -> &'static str {
74 match self {
75 StrictRemotePolicy::Off => "off",
76 StrictRemotePolicy::ForceRemote => "force_remote",
77 StrictRemotePolicy::RequireRemote => "require_remote",
78 }
79 }
80
81 #[must_use]
83 pub fn fail_closed(self) -> bool {
84 matches!(self, StrictRemotePolicy::RequireRemote)
85 }
86
87 #[must_use]
90 pub fn forces_offload(self) -> bool {
91 matches!(
92 self,
93 StrictRemotePolicy::ForceRemote | StrictRemotePolicy::RequireRemote
94 )
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
101#[serde(rename_all = "snake_case")]
102pub enum QueuePolicy {
103 #[default]
105 QueueWhenBusy,
106 NoQueue,
108}
109
110impl QueuePolicy {
111 #[must_use]
113 pub fn as_str(self) -> &'static str {
114 match self {
115 QueuePolicy::QueueWhenBusy => "queue_when_busy",
116 QueuePolicy::NoQueue => "no_queue",
117 }
118 }
119
120 #[must_use]
122 pub fn waits(self) -> bool {
123 matches!(self, QueuePolicy::QueueWhenBusy)
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "snake_case")]
130pub enum VisibilityMode {
131 #[default]
133 Default,
134 None,
136 Summary,
138 Verbose,
140}
141
142impl VisibilityMode {
143 #[must_use]
145 pub fn as_str(self) -> &'static str {
146 match self {
147 VisibilityMode::Default => "default",
148 VisibilityMode::None => "none",
149 VisibilityMode::Summary => "summary",
150 VisibilityMode::Verbose => "verbose",
151 }
152 }
153
154 fn parse(value: &str) -> Option<VisibilityMode> {
158 match value.trim().to_ascii_lowercase().as_str() {
159 "none" | "quiet" => Some(VisibilityMode::None),
160 "summary" => Some(VisibilityMode::Summary),
161 "verbose" => Some(VisibilityMode::Verbose),
162 _ => None,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
169#[serde(rename_all = "snake_case")]
170pub enum TargetDirPolicy {
171 #[default]
173 Pooled,
174 PerJob,
176 WorkerScoped,
178}
179
180impl TargetDirPolicy {
181 #[must_use]
183 pub fn as_str(self) -> &'static str {
184 match self {
185 TargetDirPolicy::Pooled => "pooled",
186 TargetDirPolicy::PerJob => "per_job",
187 TargetDirPolicy::WorkerScoped => "worker_scoped",
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
198#[serde(rename_all = "snake_case")]
199pub enum ControlKind {
200 Placement,
202 StrictRemote,
204 Queue,
206 WaitTimeout,
208 Visibility,
210 TargetDir,
212 Profile,
214 SelfHealing,
216}
217
218#[derive(Debug, Clone, Copy)]
223pub struct PlacementControl {
224 pub canonical_env: &'static str,
226 pub aliases: &'static [&'static str],
228 pub flag: Option<&'static str>,
230 pub kind: ControlKind,
232 pub value_form: &'static str,
234 pub description: &'static str,
236}
237
238#[must_use]
243pub fn placement_controls() -> &'static [PlacementControl] {
244 const CONTROLS: &[PlacementControl] = &[
245 PlacementControl {
246 canonical_env: "RCH_WORKER",
247 aliases: &["RCH_WORKERS"],
248 flag: None,
249 kind: ControlKind::Placement,
250 value_form: "worker-id[,worker-id...]",
251 description: "Request specific worker(s) by id. Still passes capability/admission \
252 checks; an inadmissible requested worker is refused with a reason code.",
253 },
254 PlacementControl {
255 canonical_env: "RCH_PRESET",
256 aliases: &[],
257 flag: None,
258 kind: ControlKind::Profile,
259 value_form: "profile-name",
260 description: "Request a named execution profile (recorded as requested_profile).",
261 },
262 PlacementControl {
263 canonical_env: "RCH_REQUIRE_REMOTE",
264 aliases: &[],
265 flag: None,
266 kind: ControlKind::StrictRemote,
267 value_form: "0|1",
268 description: "Fail closed: refuse local fallback (proof mode). Takes precedence over \
269 RCH_FORCE_REMOTE.",
270 },
271 PlacementControl {
272 canonical_env: "RCH_FORCE_REMOTE",
273 aliases: &[],
274 flag: None,
275 kind: ControlKind::StrictRemote,
276 value_form: "0|1",
277 description: "Always attempt offload (bypass local-time/speedup gating) but still \
278 fail open to local. Distinct from RCH_REQUIRE_REMOTE.",
279 },
280 PlacementControl {
281 canonical_env: "RCH_QUEUE_WHEN_BUSY",
282 aliases: &[],
283 flag: None,
284 kind: ControlKind::Queue,
285 value_form: "0|1 (default 1)",
286 description: "Wait for a busy worker instead of immediately falling back to local. \
287 Set 0 to disable queueing.",
288 },
289 PlacementControl {
290 canonical_env: "RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS",
291 aliases: &["RCH_DAEMON_RESPONSE_TIMEOUT_SECS"],
292 flag: None,
293 kind: ControlKind::WaitTimeout,
294 value_form: "secs (>0)",
295 description: "Maximum time to wait for a queued worker before falling back.",
296 },
297 PlacementControl {
298 canonical_env: "RCH_VISIBILITY",
299 aliases: &["RCH_QUIET", "RCH_VERBOSE"],
300 flag: None,
301 kind: ControlKind::Visibility,
302 value_form: "none|summary|verbose",
303 description: "Hook output verbosity. RCH_QUIET=>none, RCH_VERBOSE=>verbose.",
304 },
305 PlacementControl {
306 canonical_env: "RCH_DISABLE_TARGET_REUSE",
307 aliases: &[],
308 flag: None,
309 kind: ControlKind::TargetDir,
310 value_form: "0|1",
311 description: "Use a legacy unique-per-job remote target dir instead of the pooled, \
312 reuse-friendly dir.",
313 },
314 PlacementControl {
315 canonical_env: "RCH_NO_SELF_HEALING",
316 aliases: &[],
317 flag: Some("--no-self-healing"),
318 kind: ControlKind::SelfHealing,
319 value_form: "0|1",
320 description: "Master switch disabling hook->daemon autostart and daemon hook install.",
321 },
322 ];
323 CONTROLS
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
332#[serde(rename_all = "snake_case")]
333pub enum ControlDiagnosticLevel {
334 Info,
336 Warning,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
344pub struct ControlDiagnostic {
345 pub control: String,
347 pub level: ControlDiagnosticLevel,
349 pub message: String,
351}
352
353impl ControlDiagnostic {
354 fn info(control: &str, message: impl Into<String>) -> Self {
355 Self {
356 control: control.to_string(),
357 level: ControlDiagnosticLevel::Info,
358 message: message.into(),
359 }
360 }
361
362 fn warning(control: &str, message: impl Into<String>) -> Self {
363 Self {
364 control: control.to_string(),
365 level: ControlDiagnosticLevel::Warning,
366 message: message.into(),
367 }
368 }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
379#[serde(rename_all = "snake_case")]
380pub enum RequestedWorkerStatus {
381 NotRequested,
383 Requested,
385 Honored,
387 Unavailable,
389 AdminDisabled,
391 TemporarilyBypassed,
393 WrongPlatform,
395 MissingRuntime,
397 ProjectExcluded,
399 NoFreeSlots,
401}
402
403impl RequestedWorkerStatus {
404 #[must_use]
406 pub fn as_str(self) -> &'static str {
407 match self {
408 RequestedWorkerStatus::NotRequested => "not_requested",
409 RequestedWorkerStatus::Requested => "requested",
410 RequestedWorkerStatus::Honored => "honored",
411 RequestedWorkerStatus::Unavailable => "unavailable",
412 RequestedWorkerStatus::AdminDisabled => "admin_disabled",
413 RequestedWorkerStatus::TemporarilyBypassed => "temporarily_bypassed",
414 RequestedWorkerStatus::WrongPlatform => "wrong_platform",
415 RequestedWorkerStatus::MissingRuntime => "missing_runtime",
416 RequestedWorkerStatus::ProjectExcluded => "project_excluded",
417 RequestedWorkerStatus::NoFreeSlots => "no_free_slots",
418 }
419 }
420
421 #[must_use]
424 pub fn is_refusal(self) -> bool {
425 !matches!(
426 self,
427 RequestedWorkerStatus::NotRequested
428 | RequestedWorkerStatus::Requested
429 | RequestedWorkerStatus::Honored
430 )
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct RequestedWorkerFacts {
439 pub requested: Option<String>,
441 pub exists: bool,
443 pub admin_disabled: bool,
445 pub draining_or_drained: bool,
447 pub reachable: bool,
449 pub temporarily_bypassed: bool,
451 pub platform_matches: bool,
453 pub has_required_runtime: bool,
455 pub project_excluded: bool,
457 pub has_total_capacity: bool,
459 pub has_free_slots: bool,
461 pub estimated_cores: u32,
465 pub total_slots: u32,
467 pub admission_denial_code: Option<IncidentReasonCode>,
469 pub admission_denial_reason: Option<String>,
471}
472
473impl RequestedWorkerFacts {
474 #[must_use]
476 pub fn none() -> Self {
477 Self {
478 requested: None,
479 exists: false,
480 admin_disabled: false,
481 draining_or_drained: false,
482 reachable: true,
483 temporarily_bypassed: false,
484 platform_matches: true,
485 has_required_runtime: true,
486 project_excluded: false,
487 has_total_capacity: true,
488 has_free_slots: true,
489 estimated_cores: 0,
490 total_slots: 0,
491 admission_denial_code: None,
492 admission_denial_reason: None,
493 }
494 }
495
496 #[must_use]
498 pub fn admissible(id: impl Into<String>) -> Self {
499 Self {
500 requested: Some(id.into()),
501 exists: true,
502 ..Self::none()
503 }
504 }
505
506 #[must_use]
508 pub fn from_diagnostic(
509 requested: impl Into<String>,
510 worker: &WorkerSelectionDiagnostic,
511 ) -> Self {
512 let toolchain_inadmissible = worker
513 .reason_codes
514 .iter()
515 .any(|code| code.starts_with("toolchain."));
516 let admission_denial_code =
517 (worker.final_decision == WorkerSelectionDiagnosticDecision::Deny).then(|| {
518 if worker
519 .reason_codes
520 .iter()
521 .any(|code| code == "pressure.critical")
522 {
523 IncidentReasonCode::CriticalPressure
524 } else if worker.reason_codes.iter().any(|code| {
525 code.starts_with("topology.")
526 || code.starts_with("preflight.")
527 || code.starts_with("convergence.")
528 || code.starts_with("reliability.")
529 }) {
530 IncidentReasonCode::HardPreflight
531 } else {
532 IncidentReasonCode::NoAdmissibleWorkers
533 }
534 });
535 Self {
536 requested: Some(requested.into()),
537 exists: true,
538 admin_disabled: worker.status.eq_ignore_ascii_case("disabled"),
539 draining_or_drained: worker.status.to_ascii_lowercase().contains("drain"),
540 reachable: !worker.status.eq_ignore_ascii_case("unreachable"),
541 temporarily_bypassed: worker.circuit_state.eq_ignore_ascii_case("open")
542 || worker.status.to_ascii_lowercase().contains("bypass"),
543 platform_matches: !worker
544 .reason_codes
545 .iter()
546 .any(|code| code.contains("os_arch") || code.contains("platform")),
547 has_required_runtime: worker.runtime_available && !toolchain_inadmissible,
548 project_excluded: worker.active_project_excluded,
549 has_total_capacity: worker.total_slots >= worker.estimated_cores,
550 has_free_slots: worker.available_slots >= worker.estimated_cores,
551 estimated_cores: worker.estimated_cores,
552 total_slots: worker.total_slots,
553 admission_denial_code,
554 admission_denial_reason: admission_denial_code.map(|_| worker.final_reason.clone()),
555 }
556 }
557}
558
559#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
561pub struct RequestedWorkerOutcome {
562 pub status: RequestedWorkerStatus,
564 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub reason_code: Option<String>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub next_action: Option<String>,
570}
571
572impl RequestedWorkerOutcome {
573 #[must_use]
575 pub fn not_requested() -> Self {
576 Self {
577 status: RequestedWorkerStatus::NotRequested,
578 reason_code: None,
579 next_action: None,
580 }
581 }
582
583 #[must_use]
585 pub fn requested() -> Self {
586 Self {
587 status: RequestedWorkerStatus::Requested,
588 reason_code: None,
589 next_action: None,
590 }
591 }
592
593 fn honored() -> Self {
594 Self {
595 status: RequestedWorkerStatus::Honored,
596 reason_code: None,
597 next_action: None,
598 }
599 }
600
601 fn refused(
602 status: RequestedWorkerStatus,
603 code: IncidentReasonCode,
604 next_action: impl Into<String>,
605 ) -> Self {
606 Self {
607 status,
608 reason_code: Some(code.code().to_string()),
609 next_action: Some(next_action.into()),
610 }
611 }
612}
613
614#[must_use]
621pub fn evaluate_requested_worker(facts: &RequestedWorkerFacts) -> RequestedWorkerOutcome {
622 let Some(id) = facts.requested.as_deref().filter(|s| !s.trim().is_empty()) else {
623 return RequestedWorkerOutcome::not_requested();
624 };
625
626 if !facts.exists {
627 return RequestedWorkerOutcome::refused(
628 RequestedWorkerStatus::Unavailable,
629 IncidentReasonCode::NoAdmissibleWorkers,
630 format!("'{id}' is not a configured worker; run `rch workers list` to see valid ids"),
631 );
632 }
633 if facts.admin_disabled {
634 return RequestedWorkerOutcome::refused(
635 RequestedWorkerStatus::AdminDisabled,
636 IncidentReasonCode::NoAdmissibleWorkers,
637 format!(
638 "'{id}' is operator-disabled; re-enable with `rch workers enable {id}` or request another worker"
639 ),
640 );
641 }
642 if facts.draining_or_drained || !facts.reachable {
643 return RequestedWorkerOutcome::refused(
644 RequestedWorkerStatus::Unavailable,
645 IncidentReasonCode::NoAdmissibleWorkers,
646 format!(
647 "'{id}' is draining/unreachable; wait for it to return or request another worker"
648 ),
649 );
650 }
651 if facts.temporarily_bypassed {
652 return RequestedWorkerOutcome::refused(
653 RequestedWorkerStatus::TemporarilyBypassed,
654 IncidentReasonCode::CircuitOpen,
655 format!(
656 "'{id}' is in transient quarantine; wait for auto-rejoin or request another worker"
657 ),
658 );
659 }
660 if !facts.platform_matches {
661 return RequestedWorkerOutcome::refused(
662 RequestedWorkerStatus::WrongPlatform,
663 IncidentReasonCode::OsArchMismatch,
664 format!("'{id}' OS/arch does not match the build target; request a matching worker"),
665 );
666 }
667 if !facts.has_required_runtime {
668 return RequestedWorkerOutcome::refused(
669 RequestedWorkerStatus::MissingRuntime,
670 IncidentReasonCode::MissingRuntimeToolchainTarget,
671 format!(
672 "'{id}' lacks the required runtime/toolchain/target; install it or request another worker"
673 ),
674 );
675 }
676 if facts.project_excluded {
677 return RequestedWorkerOutcome::refused(
678 RequestedWorkerStatus::ProjectExcluded,
679 IncidentReasonCode::ActiveProjectExclusion,
680 format!(
681 "'{id}' already runs this project (active-project exclusion); request another worker"
682 ),
683 );
684 }
685 if !facts.has_total_capacity {
686 let next_action = if facts.total_slots > 0 && facts.estimated_cores > 0 {
691 format!(
692 "'{id}' has {total} total slot(s) but the request is estimated at {cores} core(s); \
693 pass -j {total} (or fewer), or request a larger worker",
694 total = facts.total_slots,
695 cores = facts.estimated_cores,
696 )
697 } else {
698 format!(
699 "'{id}' cannot fit the requested core count; lower the request (-j) or request a larger worker"
700 )
701 };
702 return RequestedWorkerOutcome::refused(
703 RequestedWorkerStatus::NoFreeSlots,
704 IncidentReasonCode::InsufficientSlots,
705 next_action,
706 );
707 }
708 if !facts.has_free_slots {
709 return RequestedWorkerOutcome::refused(
710 RequestedWorkerStatus::NoFreeSlots,
711 IncidentReasonCode::InsufficientSlots,
712 format!(
713 "'{id}' has no free slots right now; queue with RCH_QUEUE_WHEN_BUSY=1 or request another worker"
714 ),
715 );
716 }
717 if let Some(code) = facts.admission_denial_code {
718 let reason = facts
719 .admission_denial_reason
720 .as_deref()
721 .unwrap_or("worker admission denied");
722 let next_action = match code {
723 IncidentReasonCode::CriticalPressure => format!(
724 "'{id}' failed admission under critical pressure ({reason}); remediate worker pressure or request another worker"
725 ),
726 IncidentReasonCode::HardPreflight => format!(
727 "'{id}' failed a hard preflight ({reason}); run `rch diagnose -- <command>`, remediate the preflight, or request another worker"
728 ),
729 _ => format!(
730 "'{id}' failed worker admission ({reason}); run `rch diagnose -- <command>` or request another worker"
731 ),
732 };
733 return RequestedWorkerOutcome::refused(
734 RequestedWorkerStatus::Unavailable,
735 code,
736 next_action,
737 );
738 }
739 RequestedWorkerOutcome::honored()
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
750pub struct PlacementPlan {
751 pub schema_version: String,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub requested_worker: Option<String>,
756 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub requested_profile: Option<String>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub effective_worker: Option<String>,
762 pub strict_remote_policy: StrictRemotePolicy,
764 pub queue_policy: QueuePolicy,
766 pub visibility_mode: VisibilityMode,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub wait_timeout_ms: Option<u64>,
771 pub target_dir_policy: TargetDirPolicy,
773 pub requested_worker_outcome: RequestedWorkerOutcome,
775 #[serde(default, skip_serializing_if = "Vec::is_empty")]
777 pub diagnostics: Vec<ControlDiagnostic>,
778}
779
780impl PlacementPlan {
781 #[must_use]
783 pub fn schema_version() -> &'static str {
784 current_version(SchemaComponent::PlacementPlan)
785 }
786
787 #[must_use]
789 pub fn with_effective_worker(mut self, worker: Option<String>) -> Self {
790 self.effective_worker = worker;
791 self
792 }
793
794 #[must_use]
796 pub fn with_requested_worker_outcome(mut self, outcome: RequestedWorkerOutcome) -> Self {
797 self.requested_worker_outcome = outcome;
798 self
799 }
800
801 #[must_use]
805 pub fn control_state(&self) -> ControlState {
806 ControlState {
807 requested_worker: self.requested_worker.clone(),
808 requested_profile: self.requested_profile.clone(),
809 strict_remote_policy: self.strict_remote_policy != StrictRemotePolicy::Off,
810 queue_policy: (self.queue_policy != QueuePolicy::default())
811 .then(|| self.queue_policy.as_str().to_string()),
812 visibility_mode: (self.visibility_mode != VisibilityMode::Default)
813 .then(|| self.visibility_mode.as_str().to_string()),
814 wait_timeout_ms: self.wait_timeout_ms,
815 target_dir_policy: (self.target_dir_policy != TargetDirPolicy::default())
816 .then(|| self.target_dir_policy.as_str().to_string()),
817 }
818 }
819}
820
821#[must_use]
828pub fn env_truthy(value: &str) -> bool {
829 matches!(
830 value.trim().to_ascii_lowercase().as_str(),
831 "1" | "true" | "yes" | "on" | "enabled"
832 )
833}
834
835fn env_falsy(value: &str) -> bool {
838 matches!(
839 value.trim().to_ascii_lowercase().as_str(),
840 "0" | "false" | "no" | "off" | "disabled"
841 )
842}
843
844pub fn resolve_placement<F>(get: F) -> PlacementPlan
856where
857 F: Fn(&str) -> Option<String>,
858{
859 let mut diagnostics = Vec::new();
860
861 let worker_primary = get("RCH_WORKER").filter(|v| !v.trim().is_empty());
863 let worker_alias = get("RCH_WORKERS").filter(|v| !v.trim().is_empty());
864 let requested_worker = match (&worker_primary, &worker_alias) {
865 (Some(p), Some(a)) => {
866 diagnostics.push(ControlDiagnostic::info(
867 "RCH_WORKER",
868 "RCH_WORKER and its alias RCH_WORKERS are both set; merging both into the requested set",
869 ));
870 Some(merge_worker_lists(p, a))
871 }
872 (Some(p), None) => Some(p.trim().to_string()),
873 (None, Some(a)) => {
874 diagnostics.push(ControlDiagnostic::info(
875 "RCH_WORKERS",
876 "RCH_WORKERS is an alias for the canonical RCH_WORKER",
877 ));
878 Some(a.trim().to_string())
879 }
880 (None, None) => None,
881 };
882
883 let requested_profile = get("RCH_PRESET").filter(|v| !v.trim().is_empty());
885
886 let require_remote = get("RCH_REQUIRE_REMOTE").is_some_and(|v| env_truthy(&v));
888 let force_remote = get("RCH_FORCE_REMOTE").is_some_and(|v| env_truthy(&v));
889 let strict_remote_policy = if require_remote {
890 if force_remote {
891 diagnostics.push(ControlDiagnostic::info(
892 "RCH_REQUIRE_REMOTE",
893 "RCH_REQUIRE_REMOTE (fail-closed) supersedes RCH_FORCE_REMOTE (fail-open)",
894 ));
895 }
896 StrictRemotePolicy::RequireRemote
897 } else if force_remote {
898 StrictRemotePolicy::ForceRemote
899 } else {
900 StrictRemotePolicy::Off
901 };
902
903 let queue_policy = match get("RCH_QUEUE_WHEN_BUSY") {
905 None => QueuePolicy::QueueWhenBusy,
906 Some(v) if env_falsy(&v) => QueuePolicy::NoQueue,
907 Some(v) if env_truthy(&v) || v.trim().is_empty() => QueuePolicy::QueueWhenBusy,
908 Some(v) => {
909 diagnostics.push(ControlDiagnostic::warning(
910 "RCH_QUEUE_WHEN_BUSY",
911 format!("unrecognized value {v:?}; interpreted as enabled (queue_when_busy)"),
912 ));
913 QueuePolicy::QueueWhenBusy
914 }
915 };
916
917 let (wait_timeout_ms, wait_diag) = resolve_wait_timeout(&get);
919 diagnostics.extend(wait_diag);
920
921 let (visibility_mode, vis_diag) = resolve_visibility(&get);
923 diagnostics.extend(vis_diag);
924
925 let target_dir_policy = if get("RCH_DISABLE_TARGET_REUSE").is_some_and(|v| env_truthy(&v)) {
927 TargetDirPolicy::PerJob
928 } else if get("CARGO_TARGET_DIR")
929 .map(|v| v.contains(".rch-target") || v.contains("rch-worker"))
930 .unwrap_or(false)
931 {
932 TargetDirPolicy::WorkerScoped
933 } else {
934 TargetDirPolicy::Pooled
935 };
936
937 let requested_worker_outcome = if requested_worker.is_some() {
938 RequestedWorkerOutcome::requested()
939 } else {
940 RequestedWorkerOutcome::not_requested()
941 };
942
943 PlacementPlan {
944 schema_version: PlacementPlan::schema_version().to_string(),
945 requested_worker,
946 requested_profile: requested_profile.map(|v| v.trim().to_string()),
947 effective_worker: None,
948 strict_remote_policy,
949 queue_policy,
950 visibility_mode,
951 wait_timeout_ms,
952 target_dir_policy,
953 requested_worker_outcome,
954 diagnostics,
955 }
956}
957
958pub fn resolve_placement_with_role<F>(get: F, role: crate::BoxRole) -> PlacementPlan
971where
972 F: Fn(&str) -> Option<String>,
973{
974 let strict_env_present =
975 get("RCH_REQUIRE_REMOTE").is_some() || get("RCH_FORCE_REMOTE").is_some();
976 let mut plan = resolve_placement(get);
977 if role == crate::BoxRole::Dispatcher
978 && !strict_env_present
979 && plan.strict_remote_policy == StrictRemotePolicy::Off
980 {
981 plan.strict_remote_policy = StrictRemotePolicy::RequireRemote;
982 plan.diagnostics.push(ControlDiagnostic::info(
983 "general.role",
984 "role=dispatcher defaults offloadable builds to fail-closed + queue \
985 (set RCH_REQUIRE_REMOTE=0 or RCH_FORCE_REMOTE=1 per call to override)",
986 ));
987 }
988 plan
989}
990
991fn merge_worker_lists(primary: &str, alias: &str) -> String {
994 let mut seen = Vec::new();
995 for part in primary
996 .split(',')
997 .chain(alias.split(','))
998 .map(str::trim)
999 .filter(|p| !p.is_empty())
1000 {
1001 if !seen.contains(&part) {
1002 seen.push(part);
1003 }
1004 }
1005 seen.join(",")
1006}
1007
1008fn resolve_wait_timeout<F>(get: &F) -> (Option<u64>, Vec<ControlDiagnostic>)
1010where
1011 F: Fn(&str) -> Option<String>,
1012{
1013 const CANONICAL: &str = "RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS";
1014 const ALIAS: &str = "RCH_DAEMON_RESPONSE_TIMEOUT_SECS";
1015 let mut diags = Vec::new();
1016
1017 let (name, raw) = match (get(CANONICAL), get(ALIAS)) {
1018 (Some(v), _) => (CANONICAL, Some(v)),
1019 (None, Some(v)) => {
1020 diags.push(ControlDiagnostic::info(
1021 ALIAS,
1022 "RCH_DAEMON_RESPONSE_TIMEOUT_SECS also applies to non-wait queries; \
1023 prefer RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS for the queue wait timeout",
1024 ));
1025 (ALIAS, Some(v))
1026 }
1027 (None, None) => (CANONICAL, None),
1028 };
1029
1030 let Some(raw) = raw else {
1031 return (None, diags);
1032 };
1033 match raw.trim().parse::<u64>() {
1034 Ok(secs) if secs > 0 => (Some(secs.saturating_mul(1000)), diags),
1035 _ => {
1036 diags.push(ControlDiagnostic::warning(
1037 name,
1038 format!("invalid timeout {raw:?}; expected a positive integer in seconds; using the default"),
1039 ));
1040 (None, diags)
1041 }
1042 }
1043}
1044
1045fn resolve_visibility<F>(get: &F) -> (VisibilityMode, Vec<ControlDiagnostic>)
1048where
1049 F: Fn(&str) -> Option<String>,
1050{
1051 let mut diags = Vec::new();
1052
1053 if let Some(raw) = get("RCH_VISIBILITY") {
1054 match VisibilityMode::parse(&raw) {
1055 Some(mode) => return (mode, diags),
1056 None => {
1057 diags.push(ControlDiagnostic::warning(
1058 "RCH_VISIBILITY",
1059 format!("unrecognized value {raw:?}; expected none|summary|verbose; deferring to config"),
1060 ));
1061 }
1063 }
1064 }
1065 if get("RCH_QUIET").is_some_and(|v| env_truthy(&v)) {
1066 return (VisibilityMode::None, diags);
1067 }
1068 if get("RCH_VERBOSE").is_some_and(|v| env_truthy(&v)) {
1069 return (VisibilityMode::Verbose, diags);
1070 }
1071 (VisibilityMode::Default, diags)
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077 use crate::types::{RequiredRuntime, WorkerId, WorkerSelectionDiagnosticDecision};
1078 use std::collections::HashMap;
1079
1080 fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
1083 let map: HashMap<String, String> = pairs
1084 .iter()
1085 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1086 .collect();
1087 move |k: &str| map.get(k).cloned()
1088 }
1089
1090 fn admissibility_diagnostic() -> WorkerSelectionDiagnostic {
1091 WorkerSelectionDiagnostic {
1092 worker_id: WorkerId::new("css"),
1093 status: "healthy".to_string(),
1094 circuit_state: "closed".to_string(),
1095 pressure_state: "healthy".to_string(),
1096 pressure_reason_code: "pressure.healthy".to_string(),
1097 success_rate: Some(1.0),
1098 min_success_rate: 0.8,
1099 fallback_min_success_rate: 0.5,
1100 required_runtime: RequiredRuntime::Rust,
1101 runtime_available: true,
1102 available_slots: 8,
1103 total_slots: 8,
1104 estimated_cores: 2,
1105 active_project_excluded: false,
1106 final_decision: WorkerSelectionDiagnosticDecision::Allow,
1107 final_reason: "eligible".to_string(),
1108 reason_codes: vec![],
1109 }
1110 }
1111
1112 #[test]
1115 fn strict_remote_off_by_default() {
1116 let plan = resolve_placement(env(&[]));
1117 assert_eq!(plan.strict_remote_policy, StrictRemotePolicy::Off);
1118 assert!(!plan.strict_remote_policy.fail_closed());
1119 assert!(!plan.strict_remote_policy.forces_offload());
1120 }
1121
1122 #[test]
1123 fn force_remote_is_fail_open() {
1124 let plan = resolve_placement(env(&[("RCH_FORCE_REMOTE", "1")]));
1125 assert_eq!(plan.strict_remote_policy, StrictRemotePolicy::ForceRemote);
1126 assert!(plan.strict_remote_policy.forces_offload());
1127 assert!(!plan.strict_remote_policy.fail_closed());
1128 }
1129
1130 #[test]
1133 fn dispatcher_role_defaults_fail_closed_with_env_override() {
1134 use crate::BoxRole;
1135 let plan = resolve_placement_with_role(env(&[]), BoxRole::Dispatcher);
1138 assert_eq!(
1139 plan.strict_remote_policy,
1140 StrictRemotePolicy::RequireRemote,
1141 "dispatcher must never repopulate local rustc by default"
1142 );
1143 assert_eq!(plan.queue_policy, QueuePolicy::QueueWhenBusy);
1144 assert!(
1145 plan.diagnostics.iter().any(|d| d.control == "general.role"),
1146 "the role default must be visible in diagnostics"
1147 );
1148 let opted_out =
1150 resolve_placement_with_role(env(&[("RCH_REQUIRE_REMOTE", "0")]), BoxRole::Dispatcher);
1151 assert_eq!(opted_out.strict_remote_policy, StrictRemotePolicy::Off);
1152 let forced =
1154 resolve_placement_with_role(env(&[("RCH_FORCE_REMOTE", "1")]), BoxRole::Dispatcher);
1155 assert_eq!(forced.strict_remote_policy, StrictRemotePolicy::ForceRemote);
1156 let required =
1158 resolve_placement_with_role(env(&[("RCH_REQUIRE_REMOTE", "1")]), BoxRole::Dispatcher);
1159 assert_eq!(
1160 required.strict_remote_policy,
1161 StrictRemotePolicy::RequireRemote
1162 );
1163 }
1164
1165 #[test]
1166 fn worker_and_hybrid_roles_change_nothing() {
1167 use crate::BoxRole;
1168 for role in [BoxRole::Worker, BoxRole::Hybrid] {
1169 let plan = resolve_placement_with_role(env(&[]), role);
1170 let baseline = resolve_placement(env(&[]));
1171 assert_eq!(plan.strict_remote_policy, baseline.strict_remote_policy);
1172 assert_eq!(plan.queue_policy, baseline.queue_policy);
1173 assert_eq!(
1174 plan.diagnostics.len(),
1175 baseline.diagnostics.len(),
1176 "{role:?} must add no diagnostics"
1177 );
1178 }
1179 }
1180
1181 #[test]
1182 fn require_remote_is_fail_closed() {
1183 let plan = resolve_placement(env(&[("RCH_REQUIRE_REMOTE", "true")]));
1184 assert_eq!(plan.strict_remote_policy, StrictRemotePolicy::RequireRemote);
1185 assert!(plan.strict_remote_policy.fail_closed());
1186 assert!(plan.strict_remote_policy.forces_offload());
1187 }
1188
1189 #[test]
1190 fn require_remote_supersedes_force_remote_with_diagnostic() {
1191 let plan = resolve_placement(env(&[
1192 ("RCH_REQUIRE_REMOTE", "1"),
1193 ("RCH_FORCE_REMOTE", "1"),
1194 ]));
1195 assert_eq!(plan.strict_remote_policy, StrictRemotePolicy::RequireRemote);
1196 assert!(
1197 plan.diagnostics
1198 .iter()
1199 .any(|d| d.control == "RCH_REQUIRE_REMOTE"
1200 && d.level == ControlDiagnosticLevel::Info),
1201 "expected a precedence diagnostic, got {:?}",
1202 plan.diagnostics
1203 );
1204 }
1205
1206 #[test]
1209 fn queue_defaults_enabled() {
1210 let plan = resolve_placement(env(&[]));
1211 assert_eq!(plan.queue_policy, QueuePolicy::QueueWhenBusy);
1212 assert!(plan.queue_policy.waits());
1213 }
1214
1215 #[test]
1216 fn queue_disabled_by_zero() {
1217 let plan = resolve_placement(env(&[("RCH_QUEUE_WHEN_BUSY", "0")]));
1218 assert_eq!(plan.queue_policy, QueuePolicy::NoQueue);
1219 assert!(!plan.queue_policy.waits());
1220 }
1221
1222 #[test]
1223 fn queue_unrecognized_value_warns_not_silently_ignored() {
1224 let plan = resolve_placement(env(&[("RCH_QUEUE_WHEN_BUSY", "maybe")]));
1225 assert_eq!(plan.queue_policy, QueuePolicy::QueueWhenBusy);
1226 assert!(
1227 plan.diagnostics
1228 .iter()
1229 .any(|d| d.control == "RCH_QUEUE_WHEN_BUSY"
1230 && d.level == ControlDiagnosticLevel::Warning)
1231 );
1232 }
1233
1234 #[test]
1237 fn wait_timeout_seconds_to_ms() {
1238 let plan = resolve_placement(env(&[("RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS", "120")]));
1239 assert_eq!(plan.wait_timeout_ms, Some(120_000));
1240 }
1241
1242 #[test]
1243 fn wait_timeout_alias_emits_info() {
1244 let plan = resolve_placement(env(&[("RCH_DAEMON_RESPONSE_TIMEOUT_SECS", "45")]));
1245 assert_eq!(plan.wait_timeout_ms, Some(45_000));
1246 assert!(
1247 plan.diagnostics
1248 .iter()
1249 .any(|d| d.control == "RCH_DAEMON_RESPONSE_TIMEOUT_SECS"
1250 && d.level == ControlDiagnosticLevel::Info)
1251 );
1252 }
1253
1254 #[test]
1255 fn wait_timeout_invalid_warns_and_falls_back() {
1256 let plan = resolve_placement(env(&[("RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS", "soon")]));
1257 assert_eq!(plan.wait_timeout_ms, None);
1258 assert!(
1259 plan.diagnostics
1260 .iter()
1261 .any(|d| d.level == ControlDiagnosticLevel::Warning)
1262 );
1263 }
1264
1265 #[test]
1268 fn visibility_canonical_values() {
1269 for (raw, expected) in [
1270 ("none", VisibilityMode::None),
1271 ("summary", VisibilityMode::Summary),
1272 ("verbose", VisibilityMode::Verbose),
1273 ] {
1274 let plan = resolve_placement(env(&[("RCH_VISIBILITY", raw)]));
1275 assert_eq!(plan.visibility_mode, expected);
1276 }
1277 }
1278
1279 #[test]
1280 fn visibility_quiet_and_verbose_aliases() {
1281 let quiet = resolve_placement(env(&[("RCH_QUIET", "1")]));
1282 assert_eq!(quiet.visibility_mode, VisibilityMode::None);
1283 let verbose = resolve_placement(env(&[("RCH_VERBOSE", "1")]));
1284 assert_eq!(verbose.visibility_mode, VisibilityMode::Verbose);
1285 }
1286
1287 #[test]
1288 fn visibility_invalid_warns_then_falls_back_to_alias() {
1289 let plan = resolve_placement(env(&[("RCH_VISIBILITY", "ultra"), ("RCH_QUIET", "1")]));
1290 assert_eq!(plan.visibility_mode, VisibilityMode::None);
1292 assert!(
1293 plan.diagnostics.iter().any(
1294 |d| d.control == "RCH_VISIBILITY" && d.level == ControlDiagnosticLevel::Warning
1295 )
1296 );
1297 }
1298
1299 #[test]
1302 fn requested_worker_and_profile_captured() {
1303 let plan = resolve_placement(env(&[("RCH_WORKER", "css"), ("RCH_PRESET", "fast")]));
1304 assert_eq!(plan.requested_worker.as_deref(), Some("css"));
1305 assert_eq!(plan.requested_profile.as_deref(), Some("fast"));
1306 assert_eq!(
1307 plan.requested_worker_outcome.status,
1308 RequestedWorkerStatus::Requested
1309 );
1310 }
1311
1312 #[test]
1313 fn worker_alias_merges_and_dedupes() {
1314 let plan = resolve_placement(env(&[
1315 ("RCH_WORKER", "css, ovh-a"),
1316 ("RCH_WORKERS", "ovh-a,ovh-b"),
1317 ]));
1318 assert_eq!(plan.requested_worker.as_deref(), Some("css,ovh-a,ovh-b"));
1319 assert!(plan.diagnostics.iter().any(|d| d.control == "RCH_WORKER"));
1320 }
1321
1322 #[test]
1323 fn target_dir_policy_variants() {
1324 let pooled = resolve_placement(env(&[]));
1325 assert_eq!(pooled.target_dir_policy, TargetDirPolicy::Pooled);
1326 let per_job = resolve_placement(env(&[("RCH_DISABLE_TARGET_REUSE", "1")]));
1327 assert_eq!(per_job.target_dir_policy, TargetDirPolicy::PerJob);
1328 let scoped = resolve_placement(env(&[("CARGO_TARGET_DIR", "/tmp/.rch-target/css")]));
1329 assert_eq!(scoped.target_dir_policy, TargetDirPolicy::WorkerScoped);
1330 }
1331
1332 #[test]
1335 fn evaluate_not_requested() {
1336 let out = evaluate_requested_worker(&RequestedWorkerFacts::none());
1337 assert_eq!(out.status, RequestedWorkerStatus::NotRequested);
1338 assert!(out.reason_code.is_none());
1339 }
1340
1341 #[test]
1342 fn evaluate_honored() {
1343 let out = evaluate_requested_worker(&RequestedWorkerFacts::admissible("css"));
1344 assert_eq!(out.status, RequestedWorkerStatus::Honored);
1345 assert!(out.reason_code.is_none());
1346 assert!(!out.status.is_refusal());
1347 }
1348
1349 #[test]
1350 fn evaluate_unknown_worker_refused() {
1351 let facts = RequestedWorkerFacts {
1352 requested: Some("ghost".into()),
1353 exists: false,
1354 ..RequestedWorkerFacts::none()
1355 };
1356 let out = evaluate_requested_worker(&facts);
1357 assert_eq!(out.status, RequestedWorkerStatus::Unavailable);
1358 assert_eq!(out.reason_code.as_deref(), Some("RCH-I001"));
1359 assert!(
1360 out.next_action
1361 .as_deref()
1362 .unwrap()
1363 .contains("rch workers list")
1364 );
1365 assert!(out.status.is_refusal());
1366 }
1367
1368 #[test]
1369 fn evaluate_admin_disabled_refused() {
1370 let facts = RequestedWorkerFacts {
1371 admin_disabled: true,
1372 ..RequestedWorkerFacts::admissible("css")
1373 };
1374 let out = evaluate_requested_worker(&facts);
1375 assert_eq!(out.status, RequestedWorkerStatus::AdminDisabled);
1376 assert_eq!(out.reason_code.as_deref(), Some("RCH-I001"));
1377 assert!(
1378 out.next_action
1379 .as_deref()
1380 .unwrap()
1381 .contains("rch workers enable")
1382 );
1383 }
1384
1385 #[test]
1386 fn evaluate_wrong_platform_refused() {
1387 let facts = RequestedWorkerFacts {
1388 platform_matches: false,
1389 ..RequestedWorkerFacts::admissible("css")
1390 };
1391 let out = evaluate_requested_worker(&facts);
1392 assert_eq!(out.status, RequestedWorkerStatus::WrongPlatform);
1393 assert_eq!(
1394 out.reason_code.as_deref(),
1395 Some(IncidentReasonCode::OsArchMismatch.code())
1396 );
1397 }
1398
1399 #[test]
1400 fn evaluate_missing_runtime_refused() {
1401 let facts = RequestedWorkerFacts {
1402 has_required_runtime: false,
1403 ..RequestedWorkerFacts::admissible("css")
1404 };
1405 let out = evaluate_requested_worker(&facts);
1406 assert_eq!(out.status, RequestedWorkerStatus::MissingRuntime);
1407 assert_eq!(
1408 out.reason_code.as_deref(),
1409 Some(IncidentReasonCode::MissingRuntimeToolchainTarget.code())
1410 );
1411 }
1412
1413 #[test]
1414 fn evaluate_project_excluded_refused() {
1415 let facts = RequestedWorkerFacts {
1416 project_excluded: true,
1417 ..RequestedWorkerFacts::admissible("css")
1418 };
1419 let out = evaluate_requested_worker(&facts);
1420 assert_eq!(out.status, RequestedWorkerStatus::ProjectExcluded);
1421 assert_eq!(
1422 out.reason_code.as_deref(),
1423 Some(IncidentReasonCode::ActiveProjectExclusion.code())
1424 );
1425 }
1426
1427 #[test]
1428 fn evaluate_no_slots_refused() {
1429 let facts = RequestedWorkerFacts {
1430 has_free_slots: false,
1431 ..RequestedWorkerFacts::admissible("css")
1432 };
1433 let out = evaluate_requested_worker(&facts);
1434 assert_eq!(out.status, RequestedWorkerStatus::NoFreeSlots);
1435 assert_eq!(
1436 out.reason_code.as_deref(),
1437 Some(IncidentReasonCode::InsufficientSlots.code())
1438 );
1439 }
1440
1441 #[test]
1442 fn evaluate_insufficient_capacity_refused_without_queue_advice() {
1443 let mut diagnostic = admissibility_diagnostic();
1444 diagnostic.worker_id = WorkerId::new("tiny");
1445 diagnostic.available_slots = 1;
1446 diagnostic.total_slots = 1;
1447 diagnostic.estimated_cores = 2;
1448 let facts = RequestedWorkerFacts::from_diagnostic("tiny", &diagnostic);
1449 let out = evaluate_requested_worker(&facts);
1450 assert_eq!(out.status, RequestedWorkerStatus::NoFreeSlots);
1451 assert_eq!(
1452 out.reason_code.as_deref(),
1453 Some(IncidentReasonCode::InsufficientSlots.code())
1454 );
1455 let next_action = out.next_action.expect("capacity refusal needs an action");
1456 assert!(
1459 next_action.contains("1 total slot"),
1460 "should name total slots: {next_action}"
1461 );
1462 assert!(
1463 next_action.contains("2 core"),
1464 "should name estimated cores: {next_action}"
1465 );
1466 assert!(
1467 next_action.contains("-j 1"),
1468 "should name the exact -j value that fits: {next_action}"
1469 );
1470 assert!(!next_action.contains("RCH_QUEUE_WHEN_BUSY"));
1471 }
1472
1473 #[test]
1474 fn diagnostic_toolchain_refusal_maps_to_missing_runtime_code() {
1475 let mut diagnostic = admissibility_diagnostic();
1476 diagnostic.final_decision = WorkerSelectionDiagnosticDecision::Deny;
1477 diagnostic.final_reason = "toolchain version mismatch".to_string();
1478 diagnostic.reason_codes = vec!["toolchain.version_mismatch".to_string()];
1479 let facts = RequestedWorkerFacts::from_diagnostic("css", &diagnostic);
1480 let out = evaluate_requested_worker(&facts);
1481 assert_eq!(out.status, RequestedWorkerStatus::MissingRuntime);
1482 assert_eq!(
1483 out.reason_code.as_deref(),
1484 Some(IncidentReasonCode::MissingRuntimeToolchainTarget.code())
1485 );
1486 }
1487
1488 #[test]
1489 fn remaining_diagnostic_denials_never_report_honored() {
1490 let cases = [
1491 (
1492 "critical pressure",
1493 "pressure.critical",
1494 IncidentReasonCode::CriticalPressure,
1495 ),
1496 (
1497 "topology preflight failed",
1498 "topology.preflight_failed",
1499 IncidentReasonCode::HardPreflight,
1500 ),
1501 (
1502 "success rate below fallback floor",
1503 "health.below_fallback_min_success_rate",
1504 IncidentReasonCode::NoAdmissibleWorkers,
1505 ),
1506 ];
1507
1508 for (reason, reason_code, expected_incident) in cases {
1509 let mut diagnostic = admissibility_diagnostic();
1510 diagnostic.final_decision = WorkerSelectionDiagnosticDecision::Deny;
1511 diagnostic.final_reason = reason.to_string();
1512 diagnostic.reason_codes = vec![reason_code.to_string()];
1513 let facts = RequestedWorkerFacts::from_diagnostic("css", &diagnostic);
1514 let out = evaluate_requested_worker(&facts);
1515 assert!(out.status.is_refusal(), "{reason_code} must be refused");
1516 assert_eq!(out.reason_code.as_deref(), Some(expected_incident.code()));
1517 assert!(out.next_action.is_some());
1518 }
1519 }
1520
1521 #[test]
1522 fn evaluate_temporary_bypass_refused() {
1523 let facts = RequestedWorkerFacts {
1524 temporarily_bypassed: true,
1525 ..RequestedWorkerFacts::admissible("css")
1526 };
1527 let out = evaluate_requested_worker(&facts);
1528 assert_eq!(out.status, RequestedWorkerStatus::TemporarilyBypassed);
1529 assert_eq!(
1530 out.reason_code.as_deref(),
1531 Some(IncidentReasonCode::CircuitOpen.code())
1532 );
1533 }
1534
1535 #[test]
1536 fn refusal_priority_is_most_specific_first() {
1537 let facts = RequestedWorkerFacts {
1540 requested: Some("ghost".into()),
1541 exists: false,
1542 has_required_runtime: false,
1543 ..RequestedWorkerFacts::none()
1544 };
1545 assert_eq!(
1546 evaluate_requested_worker(&facts).status,
1547 RequestedWorkerStatus::Unavailable
1548 );
1549 }
1550
1551 #[test]
1554 fn control_state_bridge_is_sparse() {
1555 let plan = resolve_placement(env(&[]));
1557 assert!(plan.control_state().is_empty());
1558 }
1559
1560 #[test]
1561 fn control_state_bridge_carries_set_fields() {
1562 let plan = resolve_placement(env(&[
1563 ("RCH_WORKER", "css"),
1564 ("RCH_REQUIRE_REMOTE", "1"),
1565 ("RCH_QUEUE_WHEN_BUSY", "0"),
1566 ]));
1567 let cs = plan.control_state();
1568 assert_eq!(cs.requested_worker.as_deref(), Some("css"));
1569 assert!(cs.strict_remote_policy);
1570 assert_eq!(cs.queue_policy.as_deref(), Some("no_queue"));
1571 assert!(!cs.is_empty());
1572 }
1573
1574 #[test]
1575 fn plan_round_trips_through_json() {
1576 let plan = resolve_placement(env(&[("RCH_WORKER", "css"), ("RCH_FORCE_REMOTE", "1")]))
1577 .with_effective_worker(Some("ovh-a".to_string()))
1578 .with_requested_worker_outcome(evaluate_requested_worker(
1579 &RequestedWorkerFacts::admissible("css"),
1580 ));
1581 let v = serde_json::to_value(&plan).unwrap();
1582 assert_eq!(v["strict_remote_policy"], "force_remote");
1583 assert_eq!(v["queue_policy"], "queue_when_busy");
1584 assert_eq!(v["requested_worker"], "css");
1585 assert_eq!(v["effective_worker"], "ovh-a");
1586 assert_eq!(v["requested_worker_outcome"]["status"], "honored");
1587 let back: PlacementPlan = serde_json::from_value(v).unwrap();
1588 assert_eq!(back, plan);
1589 }
1590
1591 #[test]
1592 fn schema_version_is_pinned() {
1593 let plan = resolve_placement(env(&[]));
1594 assert_eq!(plan.schema_version, PlacementPlan::schema_version());
1595 }
1596
1597 #[test]
1600 fn registry_canonical_names_unique_and_distinct_from_aliases() {
1601 let controls = placement_controls();
1602 let mut names = std::collections::HashSet::new();
1603 for c in controls {
1604 assert!(
1605 names.insert(c.canonical_env),
1606 "duplicate canonical env {}",
1607 c.canonical_env
1608 );
1609 }
1610 for c in controls {
1612 for alias in c.aliases {
1613 assert!(
1614 !names.contains(alias),
1615 "alias {alias} collides with a canonical name"
1616 );
1617 }
1618 }
1619 }
1620
1621 #[test]
1622 fn registry_covers_the_audited_controls() {
1623 let controls = placement_controls();
1624 let has = |n: &str| controls.iter().any(|c| c.canonical_env == n);
1625 for required in [
1626 "RCH_WORKER",
1627 "RCH_REQUIRE_REMOTE",
1628 "RCH_FORCE_REMOTE",
1629 "RCH_QUEUE_WHEN_BUSY",
1630 "RCH_VISIBILITY",
1631 "RCH_NO_SELF_HEALING",
1632 ] {
1633 assert!(has(required), "registry missing audited control {required}");
1634 }
1635 }
1636}