1+ use std:: collections:: HashMap ;
12use std:: path:: PathBuf ;
23
34use ignore:: WalkBuilder ;
@@ -101,10 +102,27 @@ fn sort_workspaces(list: &mut Vec<WorkspaceInfo>) {
101102 list. sort_by ( |a, b| {
102103 let a_order = a. settings . sort_order . unwrap_or ( u32:: MAX ) ;
103104 let b_order = b. settings . sort_order . unwrap_or ( u32:: MAX ) ;
104- a_order. cmp ( & b_order) . then_with ( || a. name . cmp ( & b. name ) )
105+ a_order
106+ . cmp ( & b_order)
107+ . then_with ( || a. name . cmp ( & b. name ) )
108+ . then_with ( || a. id . cmp ( & b. id ) )
105109 } ) ;
106110}
107111
112+ fn apply_workspace_settings_update (
113+ workspaces : & mut HashMap < String , WorkspaceEntry > ,
114+ id : & str ,
115+ settings : WorkspaceSettings ,
116+ ) -> Result < WorkspaceEntry , String > {
117+ match workspaces. get_mut ( id) {
118+ Some ( entry) => {
119+ entry. settings = settings. clone ( ) ;
120+ Ok ( entry. clone ( ) )
121+ }
122+ None => Err ( "workspace not found" . to_string ( ) ) ,
123+ }
124+ }
125+
108126async fn run_git_command ( repo_path : & PathBuf , args : & [ & str ] ) -> Result < String , String > {
109127 let output = Command :: new ( "git" )
110128 . args ( args)
@@ -445,13 +463,7 @@ pub(crate) async fn update_workspace_settings(
445463) -> Result < WorkspaceInfo , String > {
446464 let ( entry_snapshot, list) = {
447465 let mut workspaces = state. workspaces . lock ( ) . await ;
448- let entry_snapshot = match workspaces. get_mut ( & id) {
449- Some ( entry) => {
450- entry. settings = settings. clone ( ) ;
451- entry. clone ( )
452- }
453- None => return Err ( "workspace not found" . to_string ( ) ) ,
454- } ;
466+ let entry_snapshot = apply_workspace_settings_update ( & mut workspaces, & id, settings) ?;
455467 let list: Vec < _ > = workspaces. values ( ) . cloned ( ) . collect ( ) ;
456468 ( entry_snapshot, list)
457469 } ;
@@ -570,19 +582,43 @@ pub(crate) async fn open_workspace_in(
570582
571583#[ cfg( test) ]
572584mod tests {
573- use super :: { sanitize_worktree_name, sort_workspaces} ;
574- use crate :: types:: { WorkspaceInfo , WorkspaceKind , WorkspaceSettings } ;
585+ use std:: collections:: HashMap ;
586+ use std:: path:: PathBuf ;
587+
588+ use super :: { apply_workspace_settings_update, sanitize_worktree_name, sort_workspaces} ;
589+ use crate :: storage:: { read_workspaces, write_workspaces} ;
590+ use crate :: types:: { WorktreeInfo , WorkspaceEntry , WorkspaceInfo , WorkspaceKind , WorkspaceSettings } ;
591+ use uuid:: Uuid ;
575592
576593 fn workspace ( name : & str , sort_order : Option < u32 > ) -> WorkspaceInfo {
594+ workspace_with_id_and_kind ( name, name, sort_order, WorkspaceKind :: Main )
595+ }
596+
597+ fn workspace_with_id_and_kind (
598+ name : & str ,
599+ id : & str ,
600+ sort_order : Option < u32 > ,
601+ kind : WorkspaceKind ,
602+ ) -> WorkspaceInfo {
603+ let ( parent_id, worktree) = if kind. is_worktree ( ) {
604+ (
605+ Some ( "parent" . to_string ( ) ) ,
606+ Some ( WorktreeInfo {
607+ branch : name. to_string ( ) ,
608+ } ) ,
609+ )
610+ } else {
611+ ( None , None )
612+ } ;
577613 WorkspaceInfo {
578- id : name . to_string ( ) ,
614+ id : id . to_string ( ) ,
579615 name : name. to_string ( ) ,
580616 path : "/tmp" . to_string ( ) ,
581617 connected : false ,
582618 codex_bin : None ,
583- kind : WorkspaceKind :: Main ,
584- parent_id : None ,
585- worktree : None ,
619+ kind,
620+ parent_id,
621+ worktree,
586622 settings : WorkspaceSettings {
587623 sidebar_collapsed : false ,
588624 sort_order,
@@ -599,6 +635,12 @@ mod tests {
599635 assert_eq ! ( sanitize_worktree_name( "--branch--" ) , "branch" ) ;
600636 }
601637
638+ #[ test]
639+ fn sanitize_worktree_name_allows_safe_chars ( ) {
640+ assert_eq ! ( sanitize_worktree_name( "release_1.2.3" ) , "release_1.2.3" ) ;
641+ assert_eq ! ( sanitize_worktree_name( "feature--x" ) , "feature--x" ) ;
642+ }
643+
602644 #[ test]
603645 fn sort_workspaces_orders_by_sort_then_name ( ) {
604646 let mut items = vec ! [
@@ -613,4 +655,107 @@ mod tests {
613655 let names: Vec < _ > = items. into_iter ( ) . map ( |item| item. name ) . collect ( ) ;
614656 assert_eq ! ( names, vec![ "gamma" , "delta" , "alpha" , "beta" ] ) ;
615657 }
658+
659+ #[ test]
660+ fn sort_workspaces_places_unordered_last_and_names_tie_break ( ) {
661+ let mut items = vec ! [
662+ workspace( "delta" , None ) ,
663+ workspace( "beta" , Some ( 1 ) ) ,
664+ workspace( "alpha" , Some ( 1 ) ) ,
665+ workspace( "gamma" , None ) ,
666+ ] ;
667+
668+ sort_workspaces ( & mut items) ;
669+
670+ let names: Vec < _ > = items. into_iter ( ) . map ( |item| item. name ) . collect ( ) ;
671+ assert_eq ! ( names, vec![ "alpha" , "beta" , "delta" , "gamma" ] ) ;
672+ }
673+
674+ #[ test]
675+ fn sort_workspaces_ignores_group_ids ( ) {
676+ let mut first = workspace ( "beta" , Some ( 2 ) ) ;
677+ first. settings . group_id = Some ( "group-b" . to_string ( ) ) ;
678+ let mut second = workspace ( "alpha" , Some ( 1 ) ) ;
679+ second. settings . group_id = Some ( "group-a" . to_string ( ) ) ;
680+ let mut third = workspace ( "gamma" , None ) ;
681+ third. settings . group_id = Some ( "group-a" . to_string ( ) ) ;
682+
683+ let mut items = vec ! [ first, second, third] ;
684+ sort_workspaces ( & mut items) ;
685+
686+ let names: Vec < _ > = items. into_iter ( ) . map ( |item| item. name ) . collect ( ) ;
687+ assert_eq ! ( names, vec![ "alpha" , "beta" , "gamma" ] ) ;
688+ }
689+
690+ #[ test]
691+ fn sort_workspaces_breaks_ties_by_id ( ) {
692+ let mut items = vec ! [
693+ workspace_with_id_and_kind( "alpha" , "b-id" , Some ( 1 ) , WorkspaceKind :: Main ) ,
694+ workspace_with_id_and_kind( "alpha" , "a-id" , Some ( 1 ) , WorkspaceKind :: Main ) ,
695+ ] ;
696+
697+ sort_workspaces ( & mut items) ;
698+
699+ let ids: Vec < _ > = items. into_iter ( ) . map ( |item| item. id ) . collect ( ) ;
700+ assert_eq ! ( ids, vec![ "a-id" , "b-id" ] ) ;
701+ }
702+
703+ #[ test]
704+ fn sort_workspaces_does_not_bias_kind ( ) {
705+ let mut items = vec ! [
706+ workspace_with_id_and_kind( "main" , "main" , Some ( 2 ) , WorkspaceKind :: Main ) ,
707+ workspace_with_id_and_kind( "worktree" , "worktree" , Some ( 1 ) , WorkspaceKind :: Worktree ) ,
708+ ] ;
709+
710+ sort_workspaces ( & mut items) ;
711+
712+ let kinds: Vec < _ > = items. into_iter ( ) . map ( |item| item. kind ) . collect ( ) ;
713+ assert ! ( matches!(
714+ kinds. as_slice( ) ,
715+ [ WorkspaceKind :: Worktree , WorkspaceKind :: Main ]
716+ ) ) ;
717+ }
718+
719+ #[ test]
720+ fn update_workspace_settings_persists_sort_and_group ( ) {
721+ let id = "workspace-1" . to_string ( ) ;
722+ let entry = WorkspaceEntry {
723+ id : id. clone ( ) ,
724+ name : "Workspace" . to_string ( ) ,
725+ path : "/tmp" . to_string ( ) ,
726+ codex_bin : None ,
727+ kind : WorkspaceKind :: Main ,
728+ parent_id : None ,
729+ worktree : None ,
730+ settings : WorkspaceSettings :: default ( ) ,
731+ } ;
732+ let mut workspaces = HashMap :: from ( [ ( id. clone ( ) , entry) ] ) ;
733+
734+ let mut settings = WorkspaceSettings :: default ( ) ;
735+ settings. sort_order = Some ( 3 ) ;
736+ settings. group_id = Some ( "group-1" . to_string ( ) ) ;
737+ settings. sidebar_collapsed = true ;
738+ settings. git_root = Some ( "/tmp" . to_string ( ) ) ;
739+
740+ let updated =
741+ apply_workspace_settings_update ( & mut workspaces, & id, settings. clone ( ) ) . expect ( "update" ) ;
742+ assert_eq ! ( updated. settings. sort_order, Some ( 3 ) ) ;
743+ assert_eq ! ( updated. settings. group_id. as_deref( ) , Some ( "group-1" ) ) ;
744+ assert ! ( updated. settings. sidebar_collapsed) ;
745+ assert_eq ! ( updated. settings. git_root. as_deref( ) , Some ( "/tmp" ) ) ;
746+
747+ let temp_dir = std:: env:: temp_dir ( )
748+ . join ( format ! ( "codex-monitor-test-{}" , Uuid :: new_v4( ) ) ) ;
749+ std:: fs:: create_dir_all ( & temp_dir) . expect ( "create temp dir" ) ;
750+ let path = PathBuf :: from ( temp_dir. join ( "workspaces.json" ) ) ;
751+ let list: Vec < _ > = workspaces. values ( ) . cloned ( ) . collect ( ) ;
752+ write_workspaces ( & path, & list) . expect ( "write workspaces" ) ;
753+
754+ let read = read_workspaces ( & path) . expect ( "read workspaces" ) ;
755+ let stored = read. get ( & id) . expect ( "stored workspace" ) ;
756+ assert_eq ! ( stored. settings. sort_order, Some ( 3 ) ) ;
757+ assert_eq ! ( stored. settings. group_id. as_deref( ) , Some ( "group-1" ) ) ;
758+ assert ! ( stored. settings. sidebar_collapsed) ;
759+ assert_eq ! ( stored. settings. git_root. as_deref( ) , Some ( "/tmp" ) ) ;
760+ }
616761}
0 commit comments