diff --git a/server.go b/server.go index 8266336..a3e5ab5 100644 --- a/server.go +++ b/server.go @@ -29,7 +29,7 @@ type ( StoreEntry = store.StoreEntry Store = store.Store Session = store.Session - SessionStore = store.SessionStore + SessionManager = store.SessionManager syntheticFileInfo = store.SyntheticFileInfo ) @@ -170,7 +170,7 @@ type Server struct { utilStore Store pluginStore Store skillStore Store - sessionStore *SessionStore + sessionStore *SessionManager transcriptStore Store tmpStore Store rootStore *store.DirStore @@ -215,7 +215,7 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server { for _, o := range opts { o(s) } - s.sessionStore = store.NewSessionStore(store.SessionStoreConfig{ + s.sessionStore = store.NewSessionManager(store.SessionManagerConfig{ AgentsDir: agentsDir, SessionsDir: sessionsDir, Log: s.log, @@ -262,7 +262,7 @@ func (s *Server) storeRoutes() []storeRoute { {"/a/", func() Store { return s.agentStore }}, {"/p/", func() Store { return s.promptStore }}, {"/m/", func() Store { return s.memStore }}, - {"/s/", func() Store { return s.sessionStore }}, + {"/s/", func() Store { return s.sessionStore.Store() }}, {"/sk/", func() Store { return s.skillStore }}, {"/u/", func() Store { return s.utilStore }}, {"/x/", func() Store { return s.pluginStore }}, @@ -342,7 +342,7 @@ func (s *Server) InGroup(group, user string) bool { func (s *Server) fileOwnerGroup(path string) (uid, gid string) { if strings.HasPrefix(path, "/s/") { parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2) - if len(parts) >= 1 && parts[0] != "new" && !isSessionStoreFile(path) { + if len(parts) >= 1 && parts[0] != "new" && !isSessionManagerFile(path) { if sess := s.sessionStore.Session(parts[0]); sess != nil { return sess.Uname(), "agent" } @@ -557,14 +557,14 @@ func (s *Server) handle(cs *connState, fc *plan9.Fcall, ctx context.Context) *pl } } -// isSessionStoreFile reports whether path is a fixed file directly under /s/ +// isSessionManagerFile reports whether path is a fixed file directly under /s/ // (i.e. /s/ where name is in sessionStoreFiles). -func isSessionStoreFile(path string) bool { +func isSessionManagerFile(path string) bool { name, ok := strings.CutPrefix(path, "/s/") if !ok || strings.Contains(name, "/") { return false } - _, ok = store.SessionStoreFileMode(name) + _, ok = store.SessionManagerFileMode(name) return ok } @@ -1174,8 +1174,8 @@ func (s *Server) makeStat(path string) plan9.Dir { default: if path == "/backends" || path == "/help" { mode = 0444 - } else if isSessionStoreFile(path) { - mode_, _ := store.SessionStoreFileMode(base) + } else if isSessionManagerFile(path) { + mode_, _ := store.SessionManagerFileMode(base) mode = plan9.Perm(mode_) } else if strings.HasPrefix(path, "/a/") { mode = 0666 @@ -1245,8 +1245,8 @@ func (s *Server) makeStat(path string) plan9.Dir { // that check stat before reading (cat, 9pfuse, etc.) see non-zero size. if dir.Length == 0 && !isDir { switch { - case isSessionStoreFile(path): - if content, err := storeRead(s.sessionStore, base); err == nil { + case isSessionManagerFile(path): + if content, err := storeRead(s.sessionStore.Store(), base); err == nil { dir.Length = uint64(len(content)) } case path == "/backends": @@ -1296,7 +1296,7 @@ func (s *Server) makeStat(path string) plan9.Dir { } case strings.HasPrefix(path, "/s/"): name := strings.TrimPrefix(path, "/s/") - if info, err := s.sessionStore.Stat(name); err == nil { + if info, err := s.sessionStore.Store().Stat(name); err == nil { dir.Length = uint64(info.Size()) } } diff --git a/store/dirstore.go b/store/dirstore.go index 1b8b180..75eae8f 100644 --- a/store/dirstore.go +++ b/store/dirstore.go @@ -22,6 +22,7 @@ type DirStore struct { deleteFn func([]string, string) error renameFn func([]string, string, string) error openFn func([]string, string) (StoreEntry, error) + statFn func([]string, string) (os.FileInfo, error) children map[string]*DirStore } @@ -67,6 +68,11 @@ func WithOpener(fn func(dirs []string, name string) (StoreEntry, error)) DirOpti return func(d *DirStore) { d.openFn = fn } } +// WithStat overrides how entries are stat'd. +func WithStat(fn func(dirs []string, name string) (os.FileInfo, error)) DirOption { + return func(d *DirStore) { d.statFn = fn } +} + // NewDirStore creates a DirStore backed by one or more directories. // The first directory is the write target for unions. func NewDirStore(dirs []string, perm os.FileMode, opts ...DirOption) *DirStore { @@ -155,6 +161,10 @@ func (d *DirStore) Stat(name string) (os.FileInfo, error) { if name == "idx" && d.indexFn != nil { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil } + // Custom stat hook. + if d.statFn != nil { + return d.statFn(d.dirs, name) + } // Synthetic files: use opener for stat. if d.openFn != nil { e, err := d.openFn(d.dirs, name) diff --git a/store/session.go b/store/session.go index e0e3463..1c103f3 100644 --- a/store/session.go +++ b/store/session.go @@ -92,15 +92,15 @@ var sessionStoreFiles = map[string]os.FileMode{ var sessionStoreOrder = []string{"new", "idx", "ls", "kill", "sh", "b", "bfg", "bbg", "cleanup"} -// SessionStoreFileMode returns the mode for a fixed session store file, +// SessionManagerFileMode returns the mode for a fixed session store file, // or 0 and false if the name is not a fixed file. -func SessionStoreFileMode(name string) (os.FileMode, bool) { +func SessionManagerFileMode(name string) (os.FileMode, bool) { m, ok := sessionStoreFiles[name] return m, ok } -// SessionStoreConfig holds the dependencies for a SessionStore. -type SessionStoreConfig struct { +// SessionManagerConfig holds the dependencies for a SessionManager. +type SessionManagerConfig struct { AgentsDir string SessionsDir string Log *olog.Logger @@ -122,33 +122,37 @@ type SessionStoreConfig struct { Yolo bool } -// SessionStore implements Store for session management. -type SessionStore struct { - *DirStore - cfg SessionStoreConfig +// SessionManager manages session lifecycle and exposes sessions as a DirStore. +type SessionManager struct { + store *DirStore + cfg SessionManagerConfig mu sync.RWMutex sessions map[string]*Session nextUID atomic.Uint32 // incrementing principal counter } +// Store returns the DirStore view of the session namespace. +func (s *SessionManager) Store() *DirStore { return s.store } + // nextUname generates the next uname atomically. -func (s *SessionStore) nextUname() string { +func (s *SessionManager) nextUname() string { return fmt.Sprintf("%d", s.nextUID.Add(1)) } -func NewSessionStore(cfg SessionStoreConfig) *SessionStore { +func NewSessionManager(cfg SessionManagerConfig) *SessionManager { if cfg.ReadFile == nil { cfg.ReadFile = os.ReadFile } if cfg.MkdirAll == nil { cfg.MkdirAll = os.MkdirAll } - ss := &SessionStore{ + ss := &SessionManager{ cfg: cfg, sessions: make(map[string]*Session), } ss.nextUID.Store(9999) - ss.DirStore = NewDirStore(nil, 0, + ss.store = NewDirStore(nil, 0, + WithStat(func(_ []string, name string) (os.FileInfo, error) { return ss.stat(name) }), WithOpener(func(_ []string, name string) (StoreEntry, error) { return ss.openEntry(name) }), WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }), WithDeleter(func(_ []string, name string) error { return ss.del(name) }), @@ -159,13 +163,13 @@ func NewSessionStore(cfg SessionStoreConfig) *SessionStore { } // AddSession inserts a pre-built session into the store. -func (s *SessionStore) AddSession(sess *Session) { +func (s *SessionManager) AddSession(sess *Session) { s.mu.Lock() s.sessions[sess.RunnableID()] = sess s.mu.Unlock() } -func (s *SessionStore) list() ([]os.DirEntry, error) { +func (s *SessionManager) list() ([]os.DirEntry, error) { entries := make([]os.DirEntry, 0, len(sessionStoreOrder)) for _, name := range sessionStoreOrder { entries = append(entries, FileEntry(name, sessionStoreFiles[name])) @@ -179,7 +183,7 @@ func (s *SessionStore) list() ([]os.DirEntry, error) { } // ListDir lists entries in a subdirectory (e.g. "{id}", "{id}/t", "{id}/t/{subdir}"). -func (s *SessionStore) ListDir(name string) ([]os.DirEntry, error) { +func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) { parts := strings.SplitN(name, "/", 3) sessID := parts[0] sess := s.Session(sessID) @@ -215,11 +219,7 @@ func (s *SessionStore) ListDir(name string) ([]os.DirEntry, error) { return nil, fmt.Errorf("%s: not a directory", name) } -func (s *SessionStore) Stat(name string) (os.FileInfo, error) { - return s.stat(name) -} - -func (s *SessionStore) stat(name string) (os.FileInfo, error) { +func (s *SessionManager) stat(name string) (os.FileInfo, error) { // Top-level fixed files (new, idx, sh, etc.) if mode, ok := sessionStoreFiles[name]; ok { return &SyntheticFileInfo{Name_: name, Mode_: mode}, nil @@ -255,7 +255,7 @@ func (s *SessionStore) stat(name string) (os.FileInfo, error) { return sfs.Stat(parts[1]) } -func (s *SessionStore) openEntry(name string) (StoreEntry, error) { +func (s *SessionManager) openEntry(name string) (StoreEntry, error) { notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } @@ -330,7 +330,7 @@ func (s *SessionStore) openEntry(name string) (StoreEntry, error) { } -func (s *SessionStore) openFilteredToolIdx(allowed map[string]bool) (StoreEntry, error) { +func (s *SessionManager) openFilteredToolIdx(allowed map[string]bool) (StoreEntry, error) { base, err := s.cfg.ToolStore.Open("idx") if err != nil { return nil, err @@ -364,7 +364,7 @@ func filterToolIdx(data []byte, allowed map[string]bool) []byte { return out } -func (s *SessionStore) create(name string) error { +func (s *SessionManager) create(name string) error { // Tool create: {id}/t/{rel} parts := strings.SplitN(name, "/", 3) if len(parts) == 3 && parts[1] == "t" && s.cfg.ToolStore != nil { @@ -374,7 +374,7 @@ func (s *SessionStore) create(name string) error { } // MkdirAll creates a directory within the session's tool store. -func (s *SessionStore) MkdirAll(name string) error { +func (s *SessionManager) MkdirAll(name string) error { parts := strings.SplitN(name, "/", 3) if len(parts) == 3 && parts[1] == "t" { type mkdirAller interface { @@ -387,7 +387,7 @@ func (s *SessionStore) MkdirAll(name string) error { return fmt.Errorf("mkdir not supported: %s", name) } -func (s *SessionStore) del(name string) error { +func (s *SessionManager) del(name string) error { parts := strings.SplitN(name, "/", 3) sessID := parts[0] // Delete session directory itself. @@ -413,14 +413,14 @@ func (s *SessionStore) del(name string) error { } // Session returns the session for the given ID, or nil. -func (s *SessionStore) Session(id string) *Session { +func (s *SessionManager) Session(id string) *Session { s.mu.RLock() defer s.mu.RUnlock() return s.sessions[id] } // SessionByUname returns the session with the given uname (principal), or nil. -func (s *SessionStore) SessionByUname(uname string) *Session { +func (s *SessionManager) SessionByUname(uname string) *Session { s.mu.RLock() defer s.mu.RUnlock() for _, sess := range s.sessions { @@ -432,26 +432,26 @@ func (s *SessionStore) SessionByUname(uname string) *Session { } // OpenStore returns a DirStore for the given session ID. -func (s *SessionStore) OpenStore(id string) (*DirStore, error) { +func (s *SessionManager) OpenStore(id string) (*DirStore, error) { if sess := s.Session(id); sess != nil { return s.openStore(sess) } return nil, fmt.Errorf("session not found: %s", id) } -func (s *SessionStore) openStore(sess *Session) (*DirStore, error) { +func (s *SessionManager) openStore(sess *Session) (*DirStore, error) { id := sess.RunnableID() return NewSessionFileStore( sess, s.cfg.Log, func() { s.KillSession(id) }, - func(newID string) error { return s.Rename(id, newID) }, + func(newID string) error { return s.renameSession(id, newID) }, s.cfg.SaveTranscript, ), nil } // InterruptAll interrupts every active session. -func (s *SessionStore) InterruptAll() { +func (s *SessionManager) InterruptAll() { s.mu.RLock() defer s.mu.RUnlock() for _, sess := range s.sessions { @@ -460,7 +460,7 @@ func (s *SessionStore) InterruptAll() { } // Shutdown kills all active sessions. -func (s *SessionStore) Shutdown() { +func (s *SessionManager) Shutdown() { s.mu.Lock() ids := make([]string, 0, len(s.sessions)) for id := range s.sessions { @@ -472,7 +472,7 @@ func (s *SessionStore) Shutdown() { } } -func (s *SessionStore) KillSession(id string) { +func (s *SessionManager) KillSession(id string) { s.mu.Lock() sess := s.sessions[id] delete(s.sessions, id) @@ -484,7 +484,7 @@ func (s *SessionStore) KillSession(id string) { } } -func (s *SessionStore) index() []byte { +func (s *SessionManager) index() []byte { var sb strings.Builder s.mu.RLock() ids := make([]string, 0, len(s.sessions)) @@ -506,7 +506,7 @@ func (s *SessionStore) index() []byte { return []byte(sb.String()) } -func (s *SessionStore) createSession(args []string) error { +func (s *SessionManager) createSession(args []string) error { name := "" backendOverride := "" modelOverride := "" @@ -641,7 +641,7 @@ func (s *SessionStore) createSession(args []string) error { return nil } -func (s *SessionStore) renameSession(old, new string) error { +func (s *SessionManager) renameSession(old, new string) error { // Tool rename: {id}/t/{oldRel} -> newName if strings.Contains(old, "/t/") { parts := strings.SplitN(old, "/", 3) @@ -689,7 +689,7 @@ func (s *SessionStore) renameSession(old, new string) error { // OnRename must be called without holding s.mu to avoid deadlock // with connection handlers that hold connState.mu and call into - // SessionStore (e.g. SessionByUname during Tattach). + // SessionManager (e.g. SessionByUname during Tattach). if s.cfg.OnRename != nil { s.cfg.OnRename(oldID, newID) } diff --git a/store/store_test.go b/store/store_test.go index 830736e..7643de5 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -142,10 +142,10 @@ func seedSkill(t *testing.T, base, name string) { os.WriteFile(filepath.Join(d, "SKILL.md"), []byte("---\ndescription: test\n---\n"), 0644) } -func newSessionStore(t *testing.T) *store.SessionStore { +func newSessionManager(t *testing.T) *store.SessionManager { t.Helper() sink := testSink() - return store.NewSessionStore(store.SessionStoreConfig{ + return store.NewSessionManager(store.SessionManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -153,10 +153,10 @@ func newSessionStore(t *testing.T) *store.SessionStore { }) } -func newSessionStoreWithCore(t *testing.T) *store.SessionStore { +func newSessionManagerWithCore(t *testing.T) *store.SessionManager { t.Helper() sink := testSink() - return store.NewSessionStore(store.SessionStoreConfig{ + return store.NewSessionManager(store.SessionManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -449,48 +449,48 @@ func TestSessionAppendLog(t *testing.T) { } } -func TestSessionStoreFileMode(t *testing.T) { - if m, ok := store.SessionStoreFileMode("new"); !ok || m != 0666 { - t.Errorf("SessionStoreFileMode(new) = %o, %v", m, ok) +func TestSessionManagerFileMode(t *testing.T) { + if m, ok := store.SessionManagerFileMode("new"); !ok || m != 0666 { + t.Errorf("SessionManagerFileMode(new) = %o, %v", m, ok) } - if _, ok := store.SessionStoreFileMode("bogus"); ok { - t.Error("SessionStoreFileMode(bogus) should be false") + if _, ok := store.SessionManagerFileMode("bogus"); ok { + t.Error("SessionManagerFileMode(bogus) should be false") } } -// ===== SessionStore ===== +// ===== SessionManager ===== -func TestSessionStoreReadableContract(t *testing.T) { - checkReadableContract(t, newSessionStore(t), "new") +func TestSessionManagerReadableContract(t *testing.T) { + checkReadableContract(t, newSessionManager(t).Store(), "new") } -func TestSessionStoreGetIdx(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerGetIdx(t *testing.T) { + s := newSessionManager(t) sess := testSession("abc") defer sess.Cancel() s.AddSession(sess) - data := storeRead(t, s, "idx") + data := storeRead(t, s.Store(), "idx") if !strings.Contains(string(data), "abc") { t.Errorf("idx = %q; want to contain abc", data) } } -func TestSessionStoreGetScript(t *testing.T) { - s := newSessionStore(t) - data := storeRead(t, s, "ls") +func TestSessionManagerGetScript(t *testing.T) { + s := newSessionManager(t) + data := storeRead(t, s.Store(), "ls") if string(data) != "#!/bin/sh\n" { t.Errorf("Read(ls) = %q", data) } } -func TestSessionStoreStatSession(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerStatSession(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) - fi, err := s.Stat("s1") + fi, err := s.Store().Stat("s1") if err != nil { t.Fatalf("Stat(s1): %v", err) } @@ -499,13 +499,13 @@ func TestSessionStoreStatSession(t *testing.T) { } } -func TestSessionStoreListIncludesSessions(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerListIncludesSessions(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) - entries, _ := s.List() + entries, _ := s.Store().List() found := false for _, e := range entries { if e.Name() == "s1" { @@ -517,9 +517,9 @@ func TestSessionStoreListIncludesSessions(t *testing.T) { } } -func TestSessionStoreWriteNotWritable(t *testing.T) { - s := newSessionStore(t) - e, err := s.Open("idx") +func TestSessionManagerWriteNotWritable(t *testing.T) { + s := newSessionManager(t) + e, err := s.Store().Open("idx") if err != nil { t.Fatalf("Open: %v", err) } @@ -528,19 +528,19 @@ func TestSessionStoreWriteNotWritable(t *testing.T) { } } -func TestSessionStoreCreateErrors(t *testing.T) { - s := newSessionStore(t) - if err := s.Create("x"); err == nil { +func TestSessionManagerCreateErrors(t *testing.T) { + s := newSessionManager(t) + if err := s.Store().Create("x"); err == nil { t.Error("Create should always error") } } -func TestSessionStoreDeleteAndKill(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerDeleteAndKill(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") s.AddSession(sess) - if err := s.Delete("s1"); err != nil { + if err := s.Store().Delete("s1"); err != nil { t.Fatalf("Delete: %v", err) } if s.Session("s1") != nil { @@ -550,13 +550,13 @@ func TestSessionStoreDeleteAndKill(t *testing.T) { if !core.closed { t.Error("core should be closed after Delete") } - if err := s.Delete("nope"); err == nil { + if err := s.Store().Delete("nope"); err == nil { t.Error("Delete(nonexistent) should error") } } -func TestSessionStoreSession(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerSession(t *testing.T) { + s := newSessionManager(t) if s.Session("nope") != nil { t.Error("Session(nonexistent) should be nil") } @@ -568,8 +568,8 @@ func TestSessionStoreSession(t *testing.T) { } } -func TestSessionStoreInterruptAll(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerInterruptAll(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") defer sess.Cancel() sess.Core.(*stubCore).running = true @@ -578,8 +578,8 @@ func TestSessionStoreInterruptAll(t *testing.T) { s.InterruptAll() // should not panic } -func TestSessionStoreShutdown(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerShutdown(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") s.AddSession(sess) @@ -589,10 +589,10 @@ func TestSessionStoreShutdown(t *testing.T) { } } -func TestSessionStoreRename(t *testing.T) { +func TestSessionManagerRename(t *testing.T) { sink := testSink() var renamed [2]string - s := store.NewSessionStore(store.SessionStoreConfig{ + s := store.NewSessionManager(store.SessionManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return nil, nil }, @@ -603,7 +603,7 @@ func TestSessionStoreRename(t *testing.T) { defer sess.Cancel() s.AddSession(sess) - if err := s.Rename("old", "new"); err != nil { + if err := s.Store().Rename("old", "new"); err != nil { t.Fatalf("Rename: %v", err) } if s.Session("old") != nil { @@ -617,30 +617,30 @@ func TestSessionStoreRename(t *testing.T) { } } -func TestSessionStoreRenameErrors(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerRenameErrors(t *testing.T) { + s := newSessionManager(t) // nonexistent - if err := s.Rename("nope", "x"); err == nil { + if err := s.Store().Rename("nope", "x"); err == nil { t.Error("Rename(nonexistent) should error") } // duplicate s.AddSession(testSession("a")) s.AddSession(testSession("b")) - if err := s.Rename("a", "b"); err == nil { + if err := s.Store().Rename("a", "b"); err == nil { t.Error("Rename to existing should error") } // running sess := testSession("r") sess.Core.(*stubCore).running = true s.AddSession(sess) - if err := s.Rename("r", "r2"); err == nil { + if err := s.Store().Rename("r", "r2"); err == nil { t.Error("Rename while running should error") } // SetSessionID error sess2 := testSession("sid") sess2.Core.(*stubCore).setSessionIDErr = fmt.Errorf("id error") s.AddSession(sess2) - if err := s.Rename("sid", "sid2"); err == nil { + if err := s.Store().Rename("sid", "sid2"); err == nil { t.Error("Rename with SetSessionID error should error") } } @@ -1179,8 +1179,8 @@ func TestSessionFileStoreEntryStat(t *testing.T) { } } -func TestSessionStoreOpenStore(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerOpenStore(t *testing.T) { + s := newSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) @@ -1352,11 +1352,11 @@ func TestSessionInterrupt(t *testing.T) { // stubCore.Interrupt is a no-op; just ensure it doesn't panic } -// ===== SessionStore.createSession ===== +// ===== SessionManager.createSession ===== -func TestSessionStoreCreateSessionViaWrite(t *testing.T) { - s := newSessionStoreWithCore(t) - e, err := s.Open("new") +func TestSessionManagerCreateSessionViaWrite(t *testing.T) { + s := newSessionManagerWithCore(t) + e, err := s.Store().Open("new") if err != nil { t.Fatalf("Open(new): %v", err) } @@ -1369,17 +1369,17 @@ func TestSessionStoreCreateSessionViaWrite(t *testing.T) { s.KillSession("testsess") } -func TestSessionStoreCreateSessionNoCwd(t *testing.T) { - s := newSessionStoreWithCore(t) - e, _ := s.Open("new") +func TestSessionManagerCreateSessionNoCwd(t *testing.T) { + s := newSessionManagerWithCore(t) + e, _ := s.Store().Open("new") if err := e.Write([]byte("name=nocwd")); err == nil { t.Error("expected error for missing cwd") } } -func TestSessionStoreCreateSessionDuplicate(t *testing.T) { - s := newSessionStoreWithCore(t) - e, _ := s.Open("new") +func TestSessionManagerCreateSessionDuplicate(t *testing.T) { + s := newSessionManagerWithCore(t) + e, _ := s.Store().Open("new") e.Write([]byte("name=dup cwd=/tmp")) //nolint:errcheck if err := e.Write([]byte("name=dup cwd=/tmp")); err == nil { t.Error("expected error for duplicate session name") @@ -1387,23 +1387,23 @@ func TestSessionStoreCreateSessionDuplicate(t *testing.T) { s.KillSession("dup") } -func TestSessionStoreCreateSessionBadOption(t *testing.T) { - s := newSessionStoreWithCore(t) - e, _ := s.Open("new") +func TestSessionManagerCreateSessionBadOption(t *testing.T) { + s := newSessionManagerWithCore(t) + e, _ := s.Store().Open("new") if err := e.Write([]byte("bogus cwd=/tmp")); err == nil { t.Error("expected error for invalid option") } } -func TestSessionStoreCreateSessionUnknownKey(t *testing.T) { - s := newSessionStoreWithCore(t) - e, _ := s.Open("new") +func TestSessionManagerCreateSessionUnknownKey(t *testing.T) { + s := newSessionManagerWithCore(t) + e, _ := s.Store().Open("new") if err := e.Write([]byte("unknown=x cwd=/tmp")); err == nil { t.Error("expected error for unknown key") } } -func TestSessionStoreCreateSessionEnvExpansion(t *testing.T) { +func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("no home dir") @@ -1411,7 +1411,7 @@ func TestSessionStoreCreateSessionEnvExpansion(t *testing.T) { t.Setenv("HOME", home) var gotCwd string sink := testSink() - s := store.NewSessionStore(store.SessionStoreConfig{ + s := store.NewSessionManager(store.SessionManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -1421,7 +1421,7 @@ func TestSessionStoreCreateSessionEnvExpansion(t *testing.T) { return &stubCore{state: "idle", backend: "stub", model: "m", agentName: agentName, cwd: cwd}, nil }, }) - e, _ := s.Open("new") + e, _ := s.Store().Open("new") if err := e.Write([]byte("name=envtest cwd=$HOME/")); err != nil { t.Fatalf("Write(new): %v", err) } @@ -1431,14 +1431,14 @@ func TestSessionStoreCreateSessionEnvExpansion(t *testing.T) { s.KillSession("envtest") } -func TestSessionStoreCreateSessionTildeExpansion(t *testing.T) { +func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("no home dir") } var gotCwd string sink := testSink() - s := store.NewSessionStore(store.SessionStoreConfig{ + s := store.NewSessionManager(store.SessionManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -1448,7 +1448,7 @@ func TestSessionStoreCreateSessionTildeExpansion(t *testing.T) { return &stubCore{state: "idle", backend: "stub", model: "m", agentName: agentName, cwd: cwd}, nil }, }) - e, _ := s.Open("new") + e, _ := s.Store().Open("new") if err := e.Write([]byte("name=tildetest cwd=~/")); err != nil { t.Fatalf("Write(new): %v", err) } @@ -1460,17 +1460,17 @@ func TestSessionStoreCreateSessionTildeExpansion(t *testing.T) { // ===== openEntry not-found ===== -func TestSessionStoreOpenEntryNotFound(t *testing.T) { - s := newSessionStore(t) - if _, err := s.Open("__nonexistent__"); err == nil { +func TestSessionManagerOpenEntryNotFound(t *testing.T) { + s := newSessionManager(t) + if _, err := s.Store().Open("__nonexistent__"); err == nil { t.Error("Open(nonexistent) should error") } } // ===== OpenStore not-found ===== -func TestSessionStoreOpenStoreNotFound(t *testing.T) { - s := newSessionStore(t) +func TestSessionManagerOpenStoreNotFound(t *testing.T) { + s := newSessionManager(t) if _, err := s.OpenStore("__missing__"); err == nil { t.Error("OpenStore(missing) should error") }