@@ -368,3 +368,160 @@ fn chrono_like_epoch(value: &str) -> Option<u64> {
368368 . ok ( )
369369 . map ( |dt| dt. timestamp ( ) . max ( 0 ) as u64 )
370370}
371+
372+ /// Hydrate a single session from the DB into a `DiscoveredSessionInfo`.
373+ /// Returns `Ok(None)` if the session doesn't exist or has no directory.
374+ #[ cfg( test) ]
375+ fn hydrate_session (
376+ reader : & DbReader ,
377+ session_id : & str ,
378+ process_pid : Option < u32 > ,
379+ serve_port : Option < u16 > ,
380+ source : DiscoverySource ,
381+ ) -> anyhow:: Result < Option < DiscoveredSessionInfo > > {
382+ let Some ( session) = reader. get_session_by_id ( session_id) ? else {
383+ return Ok ( None ) ;
384+ } ;
385+ if session. directory . as_os_str ( ) . is_empty ( ) {
386+ return Ok ( None ) ;
387+ }
388+ let status = reader. get_session_status ( session_id) ?;
389+ Ok ( Some ( DiscoveredSessionInfo {
390+ session_id : session_id. to_string ( ) ,
391+ cwd : session. directory . clone ( ) ,
392+ title : session. title . clone ( ) ,
393+ status,
394+ process_pid,
395+ model : reader. get_session_model ( session_id) ?,
396+ preview : reader
397+ . get_last_message_preview ( session_id) ?
398+ . map ( |p| p. text ) ,
399+ time_updated : Some ( session. time_updated ) ,
400+ has_children : reader. has_child_sessions ( session_id) ?,
401+ children : collect_children ( reader, session_id, 2 ) ?,
402+ serve_port,
403+ source,
404+ } ) )
405+ }
406+
407+ #[ cfg( test) ]
408+ mod tests {
409+ use super :: * ;
410+ use crate :: app:: sessions:: SessionStatus ;
411+ use rusqlite:: Connection ;
412+ use std:: path:: PathBuf ;
413+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
414+
415+ fn temp_db_path ( label : & str ) -> PathBuf {
416+ let nanos = SystemTime :: now ( )
417+ . duration_since ( UNIX_EPOCH )
418+ . unwrap ( )
419+ . as_nanos ( ) ;
420+ std:: env:: temp_dir ( ) . join ( format ! ( "ocmux-rs-{label}-{nanos}.db" ) )
421+ }
422+
423+ fn init_db ( path : & PathBuf ) -> Connection {
424+ let conn = Connection :: open ( path) . unwrap ( ) ;
425+ conn. execute_batch (
426+ r#"
427+ CREATE TABLE project (
428+ id TEXT PRIMARY KEY,
429+ worktree TEXT NOT NULL,
430+ name TEXT,
431+ time_created INTEGER,
432+ time_updated INTEGER
433+ );
434+ CREATE TABLE session (
435+ id TEXT PRIMARY KEY,
436+ project_id TEXT NOT NULL,
437+ parent_id TEXT,
438+ title TEXT,
439+ directory TEXT,
440+ permission TEXT,
441+ time_created INTEGER,
442+ time_updated INTEGER,
443+ time_archived INTEGER
444+ );
445+ CREATE TABLE message (
446+ id TEXT PRIMARY KEY,
447+ session_id TEXT NOT NULL,
448+ data TEXT NOT NULL,
449+ time_created INTEGER
450+ );
451+ CREATE TABLE part (
452+ id TEXT PRIMARY KEY,
453+ session_id TEXT NOT NULL,
454+ message_id TEXT NOT NULL,
455+ data TEXT NOT NULL,
456+ time_created INTEGER
457+ );
458+ "# ,
459+ )
460+ . unwrap ( ) ;
461+ conn
462+ }
463+
464+ #[ test]
465+ fn hydrate_session_builds_info_from_db ( ) {
466+ let db_path = temp_db_path ( "hydrate" ) ;
467+ let conn = init_db ( & db_path) ;
468+ conn. execute (
469+ "INSERT INTO project VALUES ('proj1', '/tmp/proj', 'proj', 100, 200)" ,
470+ [ ] ,
471+ )
472+ . unwrap ( ) ;
473+ conn. execute (
474+ "INSERT INTO session VALUES ('sess1', 'proj1', NULL, 'My Title', '/tmp/proj', NULL, 100, 200, NULL)" ,
475+ [ ] ,
476+ )
477+ . unwrap ( ) ;
478+ conn. execute (
479+ r#"INSERT INTO message VALUES ('msg1', 'sess1', '{"role":"assistant","time":{"completed":200}}', 200)"# ,
480+ [ ] ,
481+ )
482+ . unwrap ( ) ;
483+
484+ let reader = DbReader :: open ( & db_path) . unwrap ( ) ;
485+ let info = hydrate_session ( & reader, "sess1" , Some ( 123 ) , Some ( 4200 ) , DiscoverySource :: Serve )
486+ . unwrap ( )
487+ . unwrap ( ) ;
488+ assert_eq ! ( info. session_id, "sess1" ) ;
489+ assert_eq ! ( info. title, "My Title" ) ;
490+ assert_eq ! ( info. status, SessionStatus :: Idle ) ;
491+ assert_eq ! ( info. process_pid, Some ( 123 ) ) ;
492+ assert_eq ! ( info. serve_port, Some ( 4200 ) ) ;
493+ assert_eq ! ( info. source, DiscoverySource :: Serve ) ;
494+ assert_eq ! ( info. cwd, PathBuf :: from( "/tmp/proj" ) ) ;
495+ }
496+
497+ #[ test]
498+ fn hydrate_session_returns_none_for_missing_session ( ) {
499+ let db_path = temp_db_path ( "hydrate-miss" ) ;
500+ let _conn = init_db ( & db_path) ;
501+ let reader = DbReader :: open ( & db_path) . unwrap ( ) ;
502+ let result =
503+ hydrate_session ( & reader, "nonexistent" , None , None , DiscoverySource :: Serve ) . unwrap ( ) ;
504+ assert ! ( result. is_none( ) ) ;
505+ }
506+
507+ #[ test]
508+ fn hydrate_session_returns_none_for_empty_directory ( ) {
509+ let db_path = temp_db_path ( "hydrate-nodir" ) ;
510+ let conn = init_db ( & db_path) ;
511+ conn. execute (
512+ "INSERT INTO project VALUES ('proj1', '/tmp/proj', 'proj', 100, 200)" ,
513+ [ ] ,
514+ )
515+ . unwrap ( ) ;
516+ conn. execute (
517+ "INSERT INTO session VALUES ('sess1', 'proj1', NULL, 'Title', '', NULL, 100, 200, NULL)" ,
518+ [ ] ,
519+ )
520+ . unwrap ( ) ;
521+
522+ let reader = DbReader :: open ( & db_path) . unwrap ( ) ;
523+ let result =
524+ hydrate_session ( & reader, "sess1" , None , None , DiscoverySource :: Serve ) . unwrap ( ) ;
525+ assert ! ( result. is_none( ) ) ;
526+ }
527+ }
0 commit comments