session: enforce agent/session split in file trees

- NewSessionTree now only serves plan + env (session-level)
- NewAgentTree serves all 21 agent-level files
- Tests updated to use agent tree for agent files
- Removed kill/rename/save from ctl (session ops via rm/mv)
- TestSessionManagerOpenStore tests session-level file (plan)
This commit is contained in:
Levi Neely 2026-07-29 20:56:09 +02:00
parent f7e084106c
commit c07aef6bb9
2 changed files with 29 additions and 66 deletions

View File

@ -160,9 +160,8 @@ var _ sync.Mutex // suppress unused import
func newTestSessionFileStore(t *testing.T, sess *session.Session) *fs.Tree {
t.Helper()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {},
func(id string) error { return nil },
// Use agent tree for agent-level file tests
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func(data []byte) error { return nil },
func() {},
nil,
@ -174,6 +173,7 @@ func newTestSessionFileStore(t *testing.T, sess *session.Session) *fs.Tree {
func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *fs.Tree {
t.Helper()
sink := testSink()
// Session tree for kill/rename/save operations
return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {}, nil, nil)
}
@ -390,23 +390,23 @@ func TestSessionFileStoreReadableContract(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
sink := testSink()
_ = session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
_ = session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
}
func TestSessionFileStoreList(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
entries, err := sf.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != len(session.FileList) {
t.Errorf("List() returned %d entries; want %d", len(entries), len(session.FileList))
if len(entries) != len(session.AgentFileList) {
t.Errorf("List() returned %d entries; want %d", len(entries), len(session.AgentFileList))
}
}
@ -415,8 +415,8 @@ func TestSessionFileStoreStatChat(t *testing.T) {
defer sess.Cancel()
sess.AppendLog([]byte("hello"))
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
fi, err := sf.Stat("chat")
if err != nil {
@ -432,8 +432,8 @@ func TestSessionFileStoreGetChat(t *testing.T) {
defer sess.Cancel()
sess.AppendLog([]byte("hello"))
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
data := testStoreRead(t, sf, "chat")
if string(data) != "hello" {
@ -445,8 +445,8 @@ func TestSessionFileStoreGetContent(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} {
if _, err := sf.Open(name); err != nil {
@ -459,8 +459,8 @@ func TestSessionFileStorePutCwd(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
// Create the directory first so SetCWD validates it
os.MkdirAll("/tmp/newpath", 0755)
@ -475,8 +475,8 @@ func TestSessionFileStorePutEmpty(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil)
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func([]byte) error { return nil }, func() {}, nil, nil)
// Empty write is a no-op
e, err := sf.Open("cfg")
@ -581,9 +581,9 @@ func TestSessionFileStoreWriteChat(t *testing.T) {
sess := testSession("s1")
defer sess.Cancel()
var saved []byte
sf := newTestSessionFileStoreWith(t, sess,
func() {}, func(string) error { return nil },
func(data []byte) error { saved = data; return nil })
sink := testSink()
sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func(data []byte) error { saved = data; return nil }, func() {}, nil, nil)
testStoreWrite(t, sf, "chat", []byte("transcript data"))
if string(saved) != "transcript data" {
@ -679,40 +679,6 @@ func TestSessionFileStoreHandleCtl(t *testing.T) {
t.Error("ctl stop should interrupt (session should not be running)")
}
// kill
var killed bool
sf2 := newTestSessionFileStoreWith(t, sess,
func() { killed = true },
func(string) error { return nil },
func([]byte) error { return nil })
testStoreWrite(t, sf2, "ctl", []byte("kill"))
if !killed {
t.Error("ctl kill should call kill callback")
}
// rn (rename)
var renamed string
sf3 := newTestSessionFileStoreWith(t, sess,
func() {},
func(id string) error { renamed = id; return nil },
func([]byte) error { return nil })
testStoreWrite(t, sf3, "ctl", []byte("rn newname"))
if renamed != "newname" {
t.Errorf("renamed = %q; want newname", renamed)
}
// save
sess.AppendLog([]byte("log data"))
var saved []byte
sf4 := newTestSessionFileStoreWith(t, sess,
func() {},
func(string) error { return nil },
func(data []byte) error { saved = data; return nil })
testStoreWrite(t, sf4, "ctl", []byte("save"))
if !strings.Contains(string(saved), "log data") {
t.Errorf("saved = %q; want to contain log data", saved)
}
// slash commands forwarded to Submit
sess5 := testSession("s5")
defer sess5.Cancel()
@ -949,17 +915,14 @@ func TestSessionManagerOpenStore(t *testing.T) {
defer sess.Cancel()
s.AddSession(sess)
// Access a session file through the manager
e, err := s.Tree().Open("s1/cfg")
// Access a session-level file through the manager
e, err := s.Tree().Open("s1/plan")
if err != nil {
t.Fatalf("Open(s1/cfg): %v", err)
t.Fatalf("Open(s1/plan): %v", err)
}
data, err := e.Read()
_, err = e.Read()
if err != nil {
t.Fatalf("Read(s1/cfg): %v", err)
}
if !strings.Contains(string(data), "name=s1") {
t.Errorf("cfg = %q; want to contain name=s1", data)
t.Fatalf("Read(s1/plan): %v", err)
}
}

View File

@ -66,8 +66,8 @@ func sessionFilePerms() map[string]os.FileMode {
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func(), resetElevation func(), toolRegistry *tools.Registry) *fs.Tree {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels, resetElevation: resetElevation, toolRegistry: toolRegistry}
perms := sessionFilePerms()
specs := make([]fs.FileSpec, len(FileList))
for i, f := range FileList {
specs := make([]fs.FileSpec, len(SessionFileList))
for i, f := range SessionFileList {
specs[i] = h.fileSpec(f.Name, perms[f.Name])
specs[i].OneShot = f.OneShot
specs[i].Async = f.Async