package main import ( "context" "fmt" "io" "os" "path/filepath" "strings" "sync" "testing" "time" agent "ollie/session" "ollie/backend" olog "ollie/log" "olliesrv/fs" "olliesrv/session" ) // --- test helpers --- func testSink() *olog.Sink { return olog.NewSink(io.Discard, io.Discard, olog.LevelError) } // newNoopCore creates a real *agent.Session with a noop backend. func newNoopCore(id string) *agent.Session { be := backend.NewNoop("stub", "m") return agent.New(agent.Config{ Backend: be, AgentName: "default", CWD: "/tmp", SessionID: id, NewBackend: func(name string) (backend.Backend, error) { return backend.NewNoop(name, "default"), nil }, }) } // newBlockingCore creates a real *agent.Session whose backend blocks until ctx is cancelled. func newBlockingCore(id string) *agent.Session { be := backend.NewNoop("stub", "m") be.ChatStreamFunc = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { ch := make(chan backend.StreamEvent, 1) go func() { <-ctx.Done() ch <- backend.StreamEvent{Done: true, StopReason: "interrupted"} close(ch) }() return ch, nil } return agent.New(agent.Config{ Backend: be, AgentName: "default", CWD: "/tmp", SessionID: id, NewBackend: func(name string) (backend.Backend, error) { return backend.NewNoop(name, "default"), nil }, }) } // newContentCore creates a real *agent.Session whose backend emits specific content. func newContentCore(id, content string) *agent.Session { be := backend.NewNoop("stub", "m") be.ChatStreamFunc = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { ch := make(chan backend.StreamEvent, 2) ch <- backend.StreamEvent{Content: content} ch <- backend.StreamEvent{Done: true, StopReason: "end_turn"} close(ch) return ch, nil } return agent.New(agent.Config{ Backend: be, AgentName: "default", CWD: "/tmp", SessionID: id, NewBackend: func(name string) (backend.Backend, error) { return backend.NewNoop(name, "default"), nil }, }) } func testSession(id string) *session.Session { ctx, cancel := context.WithCancel(context.Background()) core := newNoopCore(id) return session.NewSession(id, core, ctx, cancel) } func testBlockingSession(id string) *session.Session { ctx, cancel := context.WithCancel(context.Background()) core := newBlockingCore(id) return session.NewSession(id, core, ctx, cancel) } func newTestSessionManager(t *testing.T) *session.Manager { t.Helper() sink := testSink() return session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, MkdirAll: func(string, os.FileMode) error { return nil }, }) } func newTestSessionManagerWithCore(t *testing.T) *session.Manager { t.Helper() sink := testSink() return session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, MkdirAll: func(string, os.FileMode) error { return nil }, NewCore: func(sessionID, agentName, cwd string) (*agent.Session, error) { be := backend.NewNoop("stub", "m") return agent.New(agent.Config{ Backend: be, AgentName: agentName, CWD: cwd, SessionID: sessionID, NewBackend: func(name string) (backend.Backend, error) { return backend.NewNoop(name, "default"), nil }, }), nil }, }) } func testStoreRead(t *testing.T, s fs.FileTree, name string) []byte { t.Helper() e, err := s.Open(name) if err != nil { t.Fatalf("Open(%s): %v", name, err) } data, err := e.Read() if err != nil { t.Fatalf("Read(%s): %v", name, err) } return data } func testStoreWrite(t *testing.T, s fs.FileTree, name string, data []byte) { t.Helper() e, err := s.Open(name) if err != nil { t.Fatalf("Open(%s): %v", name, err) } if err := e.Write(data); err != nil { t.Fatalf("Write(%s): %v", name, err) } } var _ = time.Second // suppress unused import var _ = strings.Contains // suppress unused import var _ = fmt.Sprintf // suppress unused import var _ = os.Remove // suppress unused import var _ = filepath.Join // suppress unused import 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 }, func(data []byte) error { return nil }, func() {}, nil, nil, ) return sf } func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *fs.Tree { t.Helper() sink := testSink() return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {}, nil, nil) } // ===== session.Session ===== func TestSessionAppendLog(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sess.AppendLog([]byte("hello")) sess.AppendLog(nil) // no-op sess.AppendLog([]byte(" world")) l, v := sess.LogInfo() if l != 11 { t.Errorf("ChatInfo length = %d; want 11", l) } if v != 2 { t.Errorf("ChatInfo vers = %d; want 2", v) } } func TestSessionManagerFileMode(t *testing.T) { if m, ok := session.FileMode("new"); !ok || m != 0666 { t.Errorf("session.FileMode(new) = %o, %v", m, ok) } if _, ok := session.FileMode("bogus"); ok { t.Error("session.FileMode(bogus) should be false") } } // ===== session.Manager ===== func TestSessionManagerReadableContract(t *testing.T) { } func TestSessionManagerGetIdx(t *testing.T) { s := newTestSessionManager(t) sess := testSession("abc") defer sess.Cancel() s.AddSession(sess) data := testStoreRead(t, s.Tree(), "idx") if !strings.Contains(string(data), "abc") { t.Errorf("idx = %q; want to contain abc", data) } } func TestSessionManagerGetScript(t *testing.T) { s := newTestSessionManager(t) data := testStoreRead(t, s.Tree(), "ls") if string(data) != "#!/bin/sh\n" { t.Errorf("Read(ls) = %q", data) } } func TestSessionManagerStatSession(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) fi, err := s.Tree().Stat("s1") if err != nil { t.Fatalf("Stat(s1): %v", err) } if !fi.IsDir() { t.Error("session stat should be dir") } } func TestSessionManagerListIncludesSessions(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) entries, _ := s.Tree().List() found := false for _, e := range entries { if e.Name() == "s1" { found = true } } if !found { t.Error("List() missing session s1") } } func TestSessionManagerWriteNotWritable(t *testing.T) { s := newTestSessionManager(t) e, err := s.Tree().Open("idx") if err != nil { t.Fatalf("Open: %v", err) } if err := e.Write(nil); err == nil { t.Error("Write(idx) should error") } } func TestSessionManagerCreateErrors(t *testing.T) { s := newTestSessionManager(t) if err := s.Tree().Create("x"); err == nil { t.Error("Create should always error") } } func TestSessionManagerDeleteAndKill(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") s.AddSession(sess) if err := s.Tree().Delete("s1"); err != nil { t.Fatalf("Delete: %v", err) } if s.Session("s1") != nil { t.Error("session should be gone after Delete") } // Verify core was closed by checking ctx is cancelled if sess.Ctx.Err() == nil { t.Error("session ctx should be cancelled after Delete") } if err := s.Tree().Delete("nope"); err == nil { t.Error("Delete(nonexistent) should error") } } func TestSessionManagerSession(t *testing.T) { s := newTestSessionManager(t) if s.Session("nope") != nil { t.Error("session.Session(nonexistent) should be nil") } sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) if s.Session("s1") == nil { t.Error("session.Session(s1) should not be nil") } } func TestSessionManagerInterruptAll(t *testing.T) { s := newTestSessionManager(t) sess := testBlockingSession("s1") defer sess.Cancel() // Submit in background to make it "running" go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) // let it enter thinking state s.AddSession(sess) s.InterruptAll() // should not panic } func TestSessionManagerShutdown(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") s.AddSession(sess) s.Shutdown() if s.Session("s1") != nil { t.Error("session should be gone after Shutdown") } } func TestSessionManagerRename(t *testing.T) { sink := testSink() s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return nil, nil }, MkdirAll: func(string, os.FileMode) error { return nil }, }) sess := testSession("old") defer sess.Cancel() s.AddSession(sess) if err := s.Tree().Rename("old", "new"); err != nil { t.Fatalf("Rename: %v", err) } if s.Session("old") != nil { t.Error("old session should be gone") } if s.Session("new") == nil { t.Error("new session should exist") } } func TestSessionManagerRenameErrors(t *testing.T) { s := newTestSessionManager(t) // nonexistent if err := s.Tree().Rename("nope", "x"); err == nil { t.Error("Rename(nonexistent) should error") } // duplicate s.AddSession(testSession("a")) s.AddSession(testSession("b")) if err := s.Tree().Rename("a", "b"); err == nil { t.Error("Rename to existing should error") } // running sess := testBlockingSession("r") go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) s.AddSession(sess) if err := s.Tree().Rename("r", "r2"); err == nil { t.Error("Rename while running should error") } sess.Cancel() // cleanup } // ===== fs.Tree ===== 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) } 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) 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)) } } func TestSessionFileStoreStatChat(t *testing.T) { sess := testSession("s1") 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) fi, err := sf.Stat("chat") if err != nil { t.Fatalf("Stat(chat): %v", err) } if fi.Size() != 5 { t.Errorf("Stat(chat).Size() = %d; want 5", fi.Size()) } } func TestSessionFileStoreGetChat(t *testing.T) { sess := testSession("s1") 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) data := testStoreRead(t, sf, "chat") if string(data) != "hello" { t.Errorf("Read(chat) = %q; want hello", data) } } 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) for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} { if _, err := sf.Open(name); err != nil { t.Errorf("Get(%q): %v", name, err) } } } 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) // Create the directory first so SetCWD validates it os.MkdirAll("/tmp/newpath", 0755) defer os.Remove("/tmp/newpath") testStoreWrite(t, sf, "cfg", []byte("cwd=/tmp/newpath")) if sess.Core.CWD() != "/tmp/newpath" { t.Errorf("cwd = %q; want /tmp/newpath", sess.Core.CWD()) } } 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) // Empty write is a no-op e, err := sf.Open("cfg") if err != nil { t.Fatalf("Open: %v", err) } if err := e.Write([]byte("")); err != nil { t.Fatalf("Write(spec, empty): %v", err) } } // ===== fs.Tree: content, writeFile, handleCtl, blockingRead, makePublish ===== func TestSessionFileStoreContentAllFields(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sess.ChatOffset = 5 sf := newTestSessionFileStore(t, sess) // individual metric files for _, tc := range []struct { name string }{ {"usage"}, {"ctxsz"}, {"models"}, {"systemprompt"}, {"offset"}, } { data := testStoreRead(t, sf, tc.name) if len(data) == 0 && tc.name == "offset" { t.Errorf("Read(%q) should not be empty", tc.name) } } // cfg contains all config in KV form spec := string(testStoreRead(t, sf, "cfg")) for _, want := range []string{ "name=s1\n", "backend=stub\n", "model=m\n", "agent=default\n", "cwd=/tmp\n", } { if !strings.Contains(spec, want) { t.Errorf("spec missing %q; got:\n%s", want, spec) } } } func TestSessionFileStoreReadFifoOut(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sess.Core.Queue("queued-item") sf := newTestSessionFileStore(t, sess) data := testStoreRead(t, sf, "fifo.out") if string(data) != "queued-item" { t.Errorf("Read(fifo.out) = %q; want queued-item", data) } // Empty queue returns nil data2 := testStoreRead(t, sf, "fifo.out") if len(data2) != 0 { t.Errorf("Read(fifo.out) empty queue = %q; want empty", data2) } } func TestSessionFileStoreReadNotFound(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) if _, err := sf.Open("__bogus__"); err == nil { t.Error("Open(bogus) should error") } } func TestSessionFileStoreWritePrompt(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // Writing to prompt triggers Submit. With noop backend it completes immediately. testStoreWrite(t, sf, "prompt", []byte("hello agent")) // Verify state is back to idle (Submit completed) time.Sleep(50 * time.Millisecond) if sess.Core.State() != "idle" { t.Errorf("state = %q; want idle after submit", sess.Core.State()) } } func TestSessionFileStoreWriteFifoIn(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "fifo.in", []byte("inject this")) item, ok := sess.Core.PopQueue() if !ok || item != "inject this" { t.Errorf("PopQueue() = %q, %v; want inject this, true", item, ok) } } 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 }) testStoreWrite(t, sf, "chat", []byte("transcript data")) if string(saved) != "transcript data" { t.Errorf("saved = %q; want transcript data", saved) } } func TestSessionFileStoreWriteBackendModelAgent(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // Writing backend= to cfg triggers Submit with /backend command // With real session + NewBackend func, it switches backend testStoreWrite(t, sf, "cfg", []byte("backend=openai")) time.Sleep(50 * time.Millisecond) if sess.Core.BackendName() != "openai" { t.Errorf("BackendName() = %q; want openai", sess.Core.BackendName()) } testStoreWrite(t, sf, "cfg", []byte("model=gpt-4")) time.Sleep(50 * time.Millisecond) if sess.Core.ModelName() != "gpt-4" { t.Errorf("ModelName() = %q; want gpt-4", sess.Core.ModelName()) } } func TestSessionFileStoreWriteBackendWhileRunning(t *testing.T) { sess := testBlockingSession("s1") defer sess.Cancel() // Make session running go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) sf := newTestSessionFileStore(t, sess) e, _ := sf.Open("cfg") if err := e.Write([]byte("backend=openai")); err == nil { t.Error("Write spec backend= while running should error") } e2, _ := sf.Open("cfg") if err := e2.Write([]byte("model=gpt-4")); err == nil { t.Error("Write spec model= while running should error") } e3, _ := sf.Open("cfg") if err := e3.Write([]byte("agent=coder")); err == nil { t.Error("Write spec agent= while running should error") } } func TestSessionFileStoreWriteParams(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "cfg", []byte("maxTokens=2048")) p := sess.Core.GenerationParams() if p.MaxTokens != 2048 { t.Errorf("MaxTokens = %d; want 2048", p.MaxTokens) } } func TestSessionFileStoreWriteParamsWhileRunning(t *testing.T) { sess := testBlockingSession("s1") defer sess.Cancel() go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) sf := newTestSessionFileStore(t, sess) e, _ := sf.Open("cfg") if err := e.Write([]byte("maxTokens=2048")); err == nil { t.Error("Write spec maxTokens= while running should error") } } func TestSessionFileStoreHandleCtl(t *testing.T) { sess := testBlockingSession("s1") defer sess.Cancel() // Make session running for interrupt test go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) sf := newTestSessionFileStore(t, sess) // stop — interrupts the running session testStoreWrite(t, sf, "ctl", []byte("stop")) time.Sleep(50 * time.Millisecond) if sess.Core.IsRunning() { 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() sf5 := newTestSessionFileStore(t, sess5) for _, cmd := range []string{"compact", "clear", "help", "history", "tools", "skills"} { testStoreWrite(t, sf5, "ctl", []byte(cmd)) } // These are slash commands that run via Submit. With noop backend, they complete. // Just verify no panic/error occurred (already verified by testStoreWrite not failing). } func TestSessionFileStoreHandleCtlErrors(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // unknown command e, _ := sf.Open("ctl") if err := e.Write([]byte("boguscmd")); err == nil { t.Error("unknown ctl command should error") } } func TestSessionFileStoreBlockingRead(t *testing.T) { sess := testBlockingSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // statewait uses BlockingRead, not Read. It blocks until state changes. done := make(chan string, 1) go func() { e, err := sf.Open("statewait") if err != nil { done <- "" return } // Use BlockingRead which invokes the Wait function data, _, _ := e.BlockingRead(sess.Ctx, "") done <- string(data) }() // Give the goroutine time to enter the blocking read time.Sleep(100 * time.Millisecond) // Submit triggers state change: idle → thinking go sess.Core.Submit(sess.Ctx, "trigger") select { case v := <-done: if v == "" { t.Error("statewait returned empty") } case <-time.After(3 * time.Second): t.Error("statewait timed out") } } func TestSessionFileStoreBlockingReadCancel(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // Reading statewait with short timeout should return when ctx expires done := make(chan struct{}) go func() { e, err := sf.Open("statewait") if err != nil { close(done) return } e.Read() // blocks until session state changes or ctx done close(done) }() // Cancel session to unblock time.Sleep(50 * time.Millisecond) sess.Cancel() select { case <-done: // good case <-time.After(2 * time.Second): t.Error("statewait did not unblock after cancel") } } func TestSessionFileStoreBlockingReadNotWaitFile(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // Non-wait files should return immediately data := testStoreRead(t, sf, "state") if string(data) != "idle\n" { t.Errorf("Read(state) = %q; want idle", data) } } func TestSessionFileStoreBlockingReadAllWaitFiles(t *testing.T) { // Verify that statewait is the only blocking file; state returns immediately sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) for _, name := range []string{"state", "cfg", "usage", "ctxsz"} { done := make(chan struct{}) go func() { testStoreRead(t, sf, name) close(done) }() select { case <-done: // good, returned immediately case <-time.After(500 * time.Millisecond): t.Errorf("Read(%q) blocked unexpectedly", name) } } } func TestSessionFileStoreBlockingReadIdleTimeout(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // statewait on an idle session should block until something changes done := make(chan struct{}) go func() { e, _ := sf.Open("statewait") e.Read() close(done) }() select { case <-done: // This is fine if something triggered it case <-time.After(100 * time.Millisecond): // Expected — still blocking. Cancel to cleanup. sess.Cancel() } } func TestSessionFileStoreMakePublish(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() core := newContentCore("s1", "hello world") sess := session.NewSession("s1", core, ctx, cancel) sf := newTestSessionFileStore(t, sess) // Submit triggers the content backend testStoreWrite(t, sf, "prompt", []byte("say hello")) time.Sleep(100 * time.Millisecond) // Chat log should contain the assistant response sess.Mu().RLock() log := string(sess.Log()) sess.Mu().RUnlock() if !strings.Contains(log, "hello world") { t.Errorf("log = %q; want to contain 'hello world'", log) } } func TestSessionFileStoreMakePublishMultipleEvents(t *testing.T) { be := backend.NewNoop("stub", "m") be.ChatStreamFunc = func(ctx context.Context, _ []backend.Message, _ []backend.Tool, _ backend.GenerationParams) (<-chan backend.StreamEvent, error) { ch := make(chan backend.StreamEvent, 3) ch <- backend.StreamEvent{Content: "thinking..."} ch <- backend.StreamEvent{Content: " done"} ch <- backend.StreamEvent{Done: true, StopReason: "end_turn"} close(ch) return ch, nil } core := agent.New(agent.Config{ Backend: be, AgentName: "default", CWD: "/tmp", SessionID: "s1", }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() sess := session.NewSession("s1", core, ctx, cancel) sf := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "prompt", []byte("multi")) time.Sleep(100 * time.Millisecond) sess.Mu().RLock() log := string(sess.Log()) sess.Mu().RUnlock() if !strings.Contains(log, "thinking...") || !strings.Contains(log, " done") { t.Errorf("log = %q; want to contain streamed content", log) } } func TestSessionFileStoreEntryStat(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // All known files should be stat-able entries, err := sf.List() if err != nil { t.Fatalf("List: %v", err) } for _, ent := range entries { fi, err := sf.Stat(ent.Name()) if err != nil { t.Errorf("Stat(%q): %v", ent.Name(), err) continue } if fi.Name() != ent.Name() { t.Errorf("Stat(%q).Name() = %q", ent.Name(), fi.Name()) } } } func TestSessionReactFile(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf := newTestSessionFileStore(t, sess) // React requires a prior response. Just verify the file is openable. e, err := sf.Open("react") if err != nil { t.Fatalf("Open(react): %v", err) } // Writing a reaction without a prior response may error — that's fine. // We're testing the file layer exists, not the business logic. _ = e.Write([]byte("thumbsup")) } func TestSessionManagerOpenStore(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) // Access a session file through the manager e, err := s.Tree().Open("s1/cfg") if err != nil { t.Fatalf("Open(s1/cfg): %v", err) } data, 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) } } func TestLoadAgentConfig(t *testing.T) { dir := t.TempDir() data := []byte(`{"prompt":"test prompt","maxTokens":1024}`) os.WriteFile(filepath.Join(dir, "test.json"), data, 0644) cfg := session.LoadAgentConfig(dir, "test", nil) if cfg == nil { t.Fatal("LoadAgentConfig returned nil") } if cfg.MaxTokens != 1024 { t.Errorf("MaxTokens = %d; want 1024", cfg.MaxTokens) } } func TestFormatEvent(t *testing.T) { tests := []struct { ev agent.Event want string }{ {agent.Event{Role: "user", Content: "hello"}, "[user]\nhello\n"}, {agent.Event{Role: "assistant", Content: "hi"}, "hi"}, {agent.Event{Role: "call", Name: "fn", Content: "args"}, "[call:fn]\nargs\n"}, {agent.Event{Role: "tool", Name: "fn", Content: "result"}, "[tool:fn]\nresult\n"}, {agent.Event{Role: "info", Content: "msg\n"}, "[info]\nmsg\n"}, } for _, tc := range tests { got := string(session.FormatEvent(tc.ev)) if got != tc.want { t.Errorf("FormatEvent(%v) = %q; want %q", tc.ev.Role, got, tc.want) } } } func TestSyntheticFileInfo(t *testing.T) { fi := &fs.SyntheticFileInfo{Name_: "test", Mode_: 0644, Size_: 42} if fi.Name() != "test" { t.Errorf("Name() = %q", fi.Name()) } if fi.Size() != 42 { t.Errorf("Size() = %d", fi.Size()) } if fi.Mode() != 0644 { t.Errorf("Mode() = %o", fi.Mode()) } } func TestFileEntryDirEntry(t *testing.T) { e := fs.FileEntry("test", 0644) if e.Name() != "test" { t.Errorf("Name() = %q", e.Name()) } if e.IsDir() { t.Error("FileEntry should not be dir") } d := fs.DirEntry("dir", 0755) if d.Name() != "dir" { t.Errorf("Name() = %q", d.Name()) } if !d.IsDir() { t.Error("DirEntry should be dir") } } func TestFormatParamsRoundTrip(t *testing.T) { temp := 0.7 topP := 0.9 p := backend.GenerationParams{ MaxTokens: 4096, Temperature: &temp, TopP: &topP, } text := session.FormatParams(p) if !strings.Contains(text, "maxTokens=4096") { t.Errorf("FormatParams missing maxTokens; got: %s", text) } if !strings.Contains(text, "temperature=0.7") { t.Errorf("FormatParams missing temperature; got: %s", text) } parsed, err := session.ParseParams(text, backend.GenerationParams{}) if err != nil { t.Fatalf("ParseParams: %v", err) } if parsed.MaxTokens != 4096 { t.Errorf("parsed.MaxTokens = %d; want 4096", parsed.MaxTokens) } if parsed.Temperature == nil || *parsed.Temperature != 0.7 { t.Errorf("parsed.Temperature = %v; want 0.7", parsed.Temperature) } } func TestFormatParamsNilOptionals(t *testing.T) { p := backend.GenerationParams{MaxTokens: 100} text := session.FormatParams(p) // Real FormatParams lists all fields; nil optionals appear as empty values if !strings.Contains(text, "maxTokens=100") { t.Errorf("FormatParams missing maxTokens=100; got: %s", text) } // Temperature is nil, so it should appear as "temperature=" (empty value) if !strings.Contains(text, "temperature=\n") { t.Errorf("nil temperature should appear as empty; got: %s", text) } } func TestParseParamsClearWithEmpty(t *testing.T) { temp := 0.7 base := backend.GenerationParams{Temperature: &temp, MaxTokens: 100} parsed, err := session.ParseParams("temperature=\nmaxTokens=200", base) if err != nil { t.Fatalf("ParseParams: %v", err) } if parsed.Temperature != nil { t.Error("empty temperature= should clear it") } if parsed.MaxTokens != 200 { t.Errorf("MaxTokens = %d; want 200", parsed.MaxTokens) } } func TestParseParamsErrors(t *testing.T) { _, err := session.ParseParams("maxTokens=notanumber", backend.GenerationParams{}) if err == nil { t.Error("non-numeric maxTokens should error") } _, err = session.ParseParams("temperature=notafloat", backend.GenerationParams{}) if err == nil { t.Error("non-numeric temperature should error") } } func TestSessionInterrupt(t *testing.T) { sess := testBlockingSession("s1") defer sess.Cancel() go sess.Core.Submit(sess.Ctx, "hello") time.Sleep(50 * time.Millisecond) sess.Interrupt() time.Sleep(50 * time.Millisecond) if sess.Core.IsRunning() { t.Error("session should not be running after interrupt") } } func TestSessionManagerCreateSessionViaWrite(t *testing.T) { s := newTestSessionManagerWithCore(t) e, err := s.Tree().Open("new") if err != nil { t.Fatalf("Open(new): %v", err) } if err := e.Write([]byte("cwd=/home/test")); err != nil { t.Fatalf("Write(new): %v", err) } // Should have created a session entries, _ := s.Tree().List() found := false for _, ent := range entries { if ent.IsDir() { found = true break } } if !found { t.Error("no session directory found after Write(new)") } } func TestSessionManagerCreateSessionNoCwd(t *testing.T) { s := newTestSessionManagerWithCore(t) e, err := s.Tree().Open("new") if err != nil { t.Fatalf("Open(new): %v", err) } // Empty write (no cwd) should error if err := e.Write([]byte("")); err == nil { t.Error("Write(new) with no cwd should error") } } func TestSessionManagerCreateSessionDuplicate(t *testing.T) { s := newTestSessionManagerWithCore(t) sess := testSession("dup") defer sess.Cancel() s.AddSession(sess) e, _ := s.Tree().Open("new") if err := e.Write([]byte("name=dup\ncwd=/tmp")); err == nil { t.Error("creating duplicate session should error") } } func TestSessionManagerCreateSessionBadOption(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().Open("new") // Malformed line (no =) if err := e.Write([]byte("badline\ncwd=/tmp")); err == nil { t.Error("malformed option should error") } } func TestSessionManagerCreateSessionUnknownKey(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().Open("new") if err := e.Write([]byte("unknownkey=val\ncwd=/tmp")); err == nil { t.Error("unknown key should error") } } func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { t.Setenv("TEST_CWD", "/expanded/path") sink := testSink() s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, MkdirAll: func(string, os.FileMode) error { return nil }, NewCore: func(sessionID, agentName, cwd string) (*agent.Session, error) { be := backend.NewNoop("stub", "m") return agent.New(agent.Config{ Backend: be, AgentName: agentName, CWD: cwd, SessionID: sessionID, }), nil }, }) e, _ := s.Tree().Open("new") if err := e.Write([]byte("cwd=$TEST_CWD")); err != nil { t.Fatalf("Write: %v", err) } } func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) { sink := testSink() s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, MkdirAll: func(string, os.FileMode) error { return nil }, NewCore: func(sessionID, agentName, cwd string) (*agent.Session, error) { be := backend.NewNoop("stub", "m") return agent.New(agent.Config{ Backend: be, AgentName: agentName, CWD: cwd, SessionID: sessionID, }), nil }, }) e, _ := s.Tree().Open("new") if err := e.Write([]byte("cwd=~/projects")); err != nil { t.Fatalf("Write: %v", err) } } func TestSessionManagerOpenEntryNotFound(t *testing.T) { s := newTestSessionManager(t) if _, err := s.Tree().Open("nonexistent/cfg"); err == nil { t.Error("Open(nonexistent/cfg) should error") } } func TestSessionManagerOpenStoreNotFound(t *testing.T) { s := newTestSessionManager(t) if _, err := s.Tree().Open("nosess/chat"); err == nil { t.Error("Open for nonexistent session should error") } }