@@ -152,6 +152,9 @@ pub struct Facts {
152152 /// unit file exists. Presence is not enablement.
153153 pub service : ServiceEnablement ,
154154 pub sync_service : ServiceEnablement ,
155+ /// Whether the managed install path has a sibling `fabric-sync` binary.
156+ /// `None` means the path could not be established.
157+ pub sync_companion_binary : Option < bool > ,
155158 pub sync_runtime : crate :: control:: SyncRuntimeStatus ,
156159 pub own_version : String ,
157160 pub peers : Vec < PeerFact > ,
@@ -175,9 +178,19 @@ impl Facts {
175178 }
176179}
177180
181+ fn is_fabric_only_transition ( facts : & Facts ) -> bool {
182+ facts. manages_service
183+ && facts. daemon_running
184+ && facts. sync_service == ServiceEnablement :: NotInstalled
185+ && facts. sync_companion_binary == Some ( false )
186+ && facts. sync_runtime . owner == "embedded"
187+ && facts. sync_runtime . companion == "absent"
188+ }
189+
178190/// Turn facts into findings. Pure, so every outcome is testable.
179191pub fn diagnose ( facts : & Facts ) -> Vec < Finding > {
180192 let fresh = facts. never_configured ( ) ;
193+ let fabric_only_transition = is_fabric_only_transition ( facts) ;
181194 let mut out = Vec :: new ( ) ;
182195
183196 out. push ( if facts. has_identity {
@@ -239,29 +252,65 @@ pub fn diagnose(facts: &Facts) -> Vec<Finding> {
239252 } ) ;
240253
241254 if facts. manages_service {
242- out. push ( match facts . sync_service {
243- ServiceEnablement :: Enabled => Finding :: new (
255+ out. push ( if fabric_only_transition {
256+ Finding :: new (
244257 "sync service" ,
245258 Verdict :: Ok ,
246- "the sync companion is installed and enabled" ,
247- ) ,
248- ServiceEnablement :: PresentNotEnabled => Finding :: new (
249- "sync service" ,
250- if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
251- "the sync companion is installed but not enabled" ,
252- )
253- . with_action ( "fabric service install" ) ,
254- ServiceEnablement :: NotInstalled => Finding :: new (
255- "sync service" ,
256- if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
257- "the sync companion service is absent" ,
259+ "the fabric-only transition does not install a sync companion service" ,
258260 )
259- . with_action ( "fabric service install" ) ,
260- ServiceEnablement :: Unknown => Finding :: new (
261- "sync service" ,
262- Verdict :: Unknown ,
263- "could not tell whether the sync companion is enabled" ,
264- ) ,
261+ } else {
262+ match ( facts. sync_service , facts. sync_companion_binary ) {
263+ ( ServiceEnablement :: Enabled , Some ( true ) ) => Finding :: new (
264+ "sync service" ,
265+ Verdict :: Ok ,
266+ "the sync companion is installed and enabled" ,
267+ ) ,
268+ ( ServiceEnablement :: Enabled , Some ( false ) ) => Finding :: new (
269+ "sync service" ,
270+ Verdict :: Problem ,
271+ "the sync companion service is enabled, but its binary is absent" ,
272+ )
273+ . with_action ( "install a matched fabric and fabric-sync pair" ) ,
274+ ( ServiceEnablement :: Enabled , None ) => Finding :: new (
275+ "sync service" ,
276+ Verdict :: Unknown ,
277+ "could not verify the binary used by the sync companion service" ,
278+ ) ,
279+ ( ServiceEnablement :: PresentNotEnabled , Some ( false ) ) => Finding :: new (
280+ "sync service" ,
281+ if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
282+ "the sync companion service is not enabled, and its binary is absent" ,
283+ )
284+ . with_action ( "install a matched fabric and fabric-sync pair" ) ,
285+ ( ServiceEnablement :: PresentNotEnabled , _) => Finding :: new (
286+ "sync service" ,
287+ if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
288+ "the sync companion is installed but not enabled" ,
289+ )
290+ . with_action ( "fabric service install" ) ,
291+ ( ServiceEnablement :: NotInstalled , Some ( true ) ) => Finding :: new (
292+ "sync service" ,
293+ if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
294+ "the sync companion service is absent" ,
295+ )
296+ . with_action ( "fabric service install" ) ,
297+ ( ServiceEnablement :: NotInstalled , Some ( false ) ) => Finding :: new (
298+ "sync service" ,
299+ if fresh { Verdict :: Setup } else { Verdict :: Problem } ,
300+ "the sync companion binary and service are absent" ,
301+ )
302+ . with_action ( "install a matched fabric and fabric-sync pair" ) ,
303+ ( ServiceEnablement :: NotInstalled , None ) => Finding :: new (
304+ "sync service" ,
305+ Verdict :: Unknown ,
306+ "could not tell whether the absent sync service has a companion binary" ,
307+ ) ,
308+ ( ServiceEnablement :: Unknown , _) => Finding :: new (
309+ "sync service" ,
310+ Verdict :: Unknown ,
311+ "could not tell whether the sync companion is enabled" ,
312+ ) ,
313+ }
265314 } ) ;
266315 }
267316
@@ -272,16 +321,30 @@ pub fn diagnose(facts: &Facts) -> Vec<Finding> {
272321 "the sync runtime is unavailable" ,
273322 )
274323 . with_action ( "start the fabric daemon and sync companion" )
275- } else if facts . manages_service && facts . sync_runtime . companion == "absent" {
324+ } else if fabric_only_transition {
276325 Finding :: new (
326+ "sync runtime" ,
327+ Verdict :: Ok ,
328+ "embedded sync is active; this fabric-only transition has no companion" ,
329+ )
330+ } else if facts. manages_service && facts. sync_runtime . companion == "absent" {
331+ let finding = Finding :: new (
277332 "sync runtime" ,
278333 Verdict :: Problem ,
279334 format ! (
280335 "{} sync is active, but the supervised companion is absent" ,
281336 facts. sync_runtime. owner
282337 ) ,
283- )
284- . with_action ( "fabric service install" )
338+ ) ;
339+ match facts. sync_companion_binary {
340+ Some ( true ) => finding. with_action ( "fabric service install" ) ,
341+ Some ( false ) => finding. with_action ( "install a matched fabric and fabric-sync pair" ) ,
342+ None => Finding :: new (
343+ "sync runtime" ,
344+ Verdict :: Unknown ,
345+ "the sync companion is absent, and its binary could not be checked" ,
346+ ) ,
347+ }
285348 } else if facts. sync_runtime . companion == "incompatible" {
286349 Finding :: new (
287350 "sync runtime" ,
@@ -739,6 +802,7 @@ mod tests {
739802 daemon_running : true ,
740803 service : ServiceEnablement :: Enabled ,
741804 sync_service : ServiceEnablement :: Enabled ,
805+ sync_companion_binary : Some ( true ) ,
742806 sync_runtime : crate :: control:: SyncRuntimeStatus {
743807 owner : "embedded" . to_string ( ) ,
744808 companion : "standby" . to_string ( ) ,
@@ -790,6 +854,82 @@ mod tests {
790854 assert ! ( opening( & configured( ) ) . is_none( ) ) ;
791855 }
792856
857+ #[ test]
858+ fn a_fabric_only_transition_does_not_offer_a_stranding_action ( ) {
859+ let mut facts = configured ( ) ;
860+ facts. sync_service = ServiceEnablement :: NotInstalled ;
861+ facts. sync_companion_binary = Some ( false ) ;
862+ facts. sync_runtime . companion = "absent" . to_string ( ) ;
863+
864+ let findings = diagnose ( & facts) ;
865+ for finding in find ( & findings, "sync service" )
866+ . into_iter ( )
867+ . chain ( find ( & findings, "sync runtime" ) )
868+ {
869+ assert_eq ! ( finding. verdict, Verdict :: Ok , "wrong finding: {finding:?}" ) ;
870+ assert ! (
871+ finding. action. is_none( ) ,
872+ "the transition offered an unsafe action: {finding:?}"
873+ ) ;
874+ }
875+ assert_eq ! ( exit_code( & findings) , 0 ) ;
876+ }
877+
878+ #[ test]
879+ fn a_paired_binary_without_its_service_stays_loud ( ) {
880+ let mut facts = configured ( ) ;
881+ facts. sync_service = ServiceEnablement :: NotInstalled ;
882+ facts. sync_runtime . companion = "absent" . to_string ( ) ;
883+
884+ let findings = diagnose ( & facts) ;
885+ for finding in find ( & findings, "sync service" )
886+ . into_iter ( )
887+ . chain ( find ( & findings, "sync runtime" ) )
888+ {
889+ assert_eq ! ( finding. verdict, Verdict :: Problem ) ;
890+ assert_eq ! ( finding. action. as_deref( ) , Some ( "fabric service install" ) ) ;
891+ }
892+ }
893+
894+ #[ test]
895+ fn an_enabled_companion_service_without_its_binary_stays_loud ( ) {
896+ let mut facts = configured ( ) ;
897+ facts. sync_companion_binary = Some ( false ) ;
898+ facts. sync_runtime . companion = "absent" . to_string ( ) ;
899+
900+ let findings = diagnose ( & facts) ;
901+ for finding in find ( & findings, "sync service" )
902+ . into_iter ( )
903+ . chain ( find ( & findings, "sync runtime" ) )
904+ {
905+ assert_eq ! ( finding. verdict, Verdict :: Problem ) ;
906+ assert ! (
907+ finding
908+ . action
909+ . as_deref( )
910+ . is_some_and( |action| action. contains( "matched" ) ) ,
911+ "the missing binary got the wrong action: {finding:?}"
912+ ) ;
913+ assert_ne ! ( finding. action. as_deref( ) , Some ( "fabric service install" ) ) ;
914+ }
915+ }
916+
917+ #[ test]
918+ fn an_unknown_companion_binary_offers_no_action ( ) {
919+ let mut facts = configured ( ) ;
920+ facts. sync_companion_binary = None ;
921+ facts. sync_runtime . companion = "absent" . to_string ( ) ;
922+
923+ let findings = diagnose ( & facts) ;
924+ for finding in find ( & findings, "sync service" )
925+ . into_iter ( )
926+ . chain ( find ( & findings, "sync runtime" ) )
927+ {
928+ assert_eq ! ( finding. verdict, Verdict :: Unknown ) ;
929+ assert ! ( finding. action. is_none( ) , "unknown state offered an action" ) ;
930+ }
931+ }
932+
793933 #[ test]
794934 fn a_peer_with_no_grants_is_an_actionable_problem ( ) {
795935 let mut facts = configured ( ) ;
@@ -820,6 +960,7 @@ mod tests {
820960 daemon_running : false ,
821961 service : ServiceEnablement :: NotInstalled ,
822962 sync_service : ServiceEnablement :: NotInstalled ,
963+ sync_companion_binary : Some ( false ) ,
823964 sync_runtime : crate :: control:: SyncRuntimeStatus {
824965 owner : "unavailable" . to_string ( ) ,
825966 companion : "unknown" . to_string ( ) ,
@@ -871,6 +1012,7 @@ mod tests {
8711012 // what the gatherer sees.
8721013 service : ServiceEnablement :: Enabled ,
8731014 sync_service : ServiceEnablement :: Enabled ,
1015+ sync_companion_binary : Some ( true ) ,
8741016 sync_runtime : crate :: control:: SyncRuntimeStatus {
8751017 owner : "unavailable" . to_string ( ) ,
8761018 companion : "unknown" . to_string ( ) ,
@@ -1438,6 +1580,10 @@ where
14381580
14391581 let service = crate :: service:: service_enablement ( ) ;
14401582 let sync_service = crate :: service:: sync_service_enablement ( ) ;
1583+ let sync_companion_binary = crate :: update:: managed_binary_path ( )
1584+ . and_then ( |path| crate :: update:: companion_binary_path ( & path) )
1585+ . map ( |path| path. exists ( ) )
1586+ . ok ( ) ;
14411587
14421588 let mut own_version = env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ;
14431589 let mut daemon_running = false ;
@@ -1535,6 +1681,7 @@ where
15351681 daemon_running,
15361682 service,
15371683 sync_service,
1684+ sync_companion_binary,
15381685 sync_runtime,
15391686 own_version,
15401687 peers,
0 commit comments