Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions cmd/entire/cli/agent/agent_homes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,55 @@ func TestResolveTrustedHome_ComparesTheActiveHomeAsCleaned(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, base, got)
}

// repoStubAgent is a HomeLayoutProvider whose active home is home, with the
// given stores beneath it, and whose session directory for every repository
// is sessionDir.
type repoStubAgent struct {
mockBaseAgent

home, sessionDir string
stores []string
}

func (r *repoStubAgent) SessionHome() (string, error) { return r.home, nil }
func (r *repoStubAgent) HomeLayout() HomeLayout { return HomeLayout{Stores: r.stores} }
func (r *repoStubAgent) GetSessionDir(string) (string, error) { return r.sessionDir, nil }

func TestRepoHomeLayout(t *testing.T) {
t.Parallel()

home := filepath.Join(t.TempDir(), "home")
tests := []struct {
name string
stores []string
sessionDir string
want []string // nil when the session directory lies outside every store
}{
{name: "project directory in the first store", stores: []string{"projects"},
sessionDir: filepath.Join(home, "projects", "-repo"), want: []string{"projects/-repo"}},
{name: "store itself applies to every store", stores: []string{"sessions", "archived_sessions"},
sessionDir: filepath.Join(home, "sessions"), want: []string{"sessions", "archived_sessions"}},
{name: "project directory in a later store", stores: []string{"a", "b/c"},
sessionDir: filepath.Join(home, "b", "c", "-repo"), want: []string{"a/-repo", "b/c/-repo"}},
{name: "unclean session directory", stores: []string{"projects"},
sessionDir: filepath.Join(home, "projects", "x") + string(filepath.Separator) + filepath.Join("..", "-repo"), want: []string{"projects/-repo"}},
{name: "sibling of a store", stores: []string{"projects"},
sessionDir: filepath.Join(home, "projects-other", "-repo")},
{name: "outside the home", stores: []string{"sessions"},
sessionDir: filepath.Join(t.TempDir(), "sessions")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

provider := &repoStubAgent{home: home, sessionDir: tt.sessionDir, stores: tt.stores}

got, ok, err := RepoHomeLayout(provider, "/repo")

require.NoError(t, err)
assert.Equal(t, tt.want != nil, ok)
assert.Equal(t, tt.want, got.Stores)
})
}
}
43 changes: 42 additions & 1 deletion cmd/entire/cli/agent/home_layout.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package agent

import "path/filepath"
import (
"fmt"
"path"
"path/filepath"
)

// HomeLayout describes where an agent keeps session transcripts beneath its
// per-user home directory.
Expand Down Expand Up @@ -32,6 +36,43 @@ func (l HomeLayout) Holds(home, path string) bool {
return false
}

// RepoHomeLayout returns provider's home layout narrowed to the sessions of
// repoPath. The session directory GetSessionDir resolves for repoPath must lie
// in one of the active home's stores; its path within that store is appended
// to every store. For an agent with a directory per project, such as Claude
// Code, the result names that project's directory in each store; for one
// without, such as Codex, it is the full layout. It reports false when the
// session directory lies outside every store of the active home, as for a Pi
// store relocated with PI_CODING_AGENT_SESSION_DIR or under a test override of
// the session directory.
func RepoHomeLayout(provider HomeLayoutProvider, repoPath string) (HomeLayout, bool, error) {
active, err := provider.SessionHome()
if err != nil {
return HomeLayout{}, false, fmt.Errorf("resolve %s home: %w", provider.Type(), err)
}
dir, err := provider.GetSessionDir(repoPath)
if err != nil {
return HomeLayout{}, false, fmt.Errorf("resolve %s session directory: %w", provider.Type(), err)
}
dir = filepath.Clean(dir)
layout := provider.HomeLayout()
for _, store := range layout.StoresUnder(filepath.Clean(active)) {
if !pathHasDirPrefix(dir, store) {
continue
}
rel, err := filepath.Rel(store, dir)
if err != nil || !filepath.IsLocal(rel) {
return HomeLayout{}, false, nil //nolint:nilerr // a directory Rel cannot place within the store lies outside it
}
narrowed := HomeLayout{Stores: make([]string, len(layout.Stores))}
for i, s := range layout.Stores {
narrowed.Stores[i] = path.Join(s, filepath.ToSlash(rel))
}
return narrowed, true, nil
}
return HomeLayout{}, false, nil
}

// HomeLayoutProvider is implemented by built-in agents that keep session
// transcripts in one or more stores beneath a per-user home directory, such as
// Codex's live and archived session stores beneath CODEX_HOME.
Expand Down
Loading
Loading