package main import ( "context" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" "github.com/simonfxr/pubsub" "ollie/pkg/agent" "ollie/pkg/backend" olog "ollie/pkg/log" "olliesrv/fs" "olliesrv/session" ) // --- stub agent.Core --- type stubCore struct { state string running bool backend_ string model string agentName string cwd string usage string ctxsz string models string sysprompt string reply string params backend.GenerationParams closed bool waitCh chan string submitted []string queued []string interrupted bool setSessionIDErr error reactResponseID string reactEmoji string submitCh chan struct{} bus_ *pubsub.Bus } func (c *stubCore) Submit(_ context.Context, input string) { c.submitted = append(c.submitted, input) if c.reply != "" { c.Bus().Publish("event", agent.Event{Role: "assistant", Content: c.reply}) } if c.submitCh != nil { close(c.submitCh) } } func (c *stubCore) Interrupt(error) bool { c.interrupted = true; return c.running } func (c *stubCore) Inject(string) {} func (c *stubCore) Queue(s string) { c.queued = append(c.queued, s) } func (c *stubCore) PopQueue() (string, bool) { if len(c.queued) == 0 { return "", false } s := c.queued[0] c.queued = c.queued[1:] return s, true } func (c *stubCore) IsRunning() bool { return c.running } func (c *stubCore) State() string { return c.state } func (c *stubCore) Reply() string { return c.reply } func (c *stubCore) AgentName() string { return c.agentName } func (c *stubCore) BackendName() string { return c.backend_ } func (c *stubCore) ModelName() string { return c.model } func (c *stubCore) CtxSz() string { return c.ctxsz } func (c *stubCore) Usage() string { return c.usage } func (c *stubCore) Cost() string { return "" } func (c *stubCore) ListModels() string { return c.models } func (c *stubCore) CWD() string { return c.cwd } func (c *stubCore) SetCWD(dir string) error { c.cwd = dir; return nil } func (c *stubCore) SetSessionID(string) error { return c.setSessionIDErr } func (c *stubCore) Context() []backend.Message { return nil } func (c *stubCore) SystemPrompt() string { return c.sysprompt } func (c *stubCore) GenerationParams() backend.GenerationParams { return c.params } func (c *stubCore) SetGenerationParams(p backend.GenerationParams) error { c.params = p; return nil } func (c *stubCore) SetEnv(string, string) {} func (c *stubCore) WaitChange(ctx context.Context, _, _ string) (string, bool) { if c.waitCh != nil { select { case v := <-c.waitCh: return v, true case <-ctx.Done(): return "", false } } <-ctx.Done() return "", false } func (c *stubCore) Close() { c.closed = true } func (c *stubCore) Detach() bool { return false } func (c *stubCore) ListDetached() []agent.DetachedInfo { return nil } func (c *stubCore) SignalDetached(int, int) error { return nil } func (c *stubCore) GetDetachedOutput(int) (string, error) { return "", nil } func (c *stubCore) DismissDetached(int) bool { return false } func (c *stubCore) InjectSystemEvent(string) {} func (c *stubCore) Reactions() map[string]string { return nil } func (c *stubCore) React(string) {} func (c *stubCore) ReactTo(responseID, emoji string) error { c.reactResponseID = responseID c.reactEmoji = emoji return nil } func (c *stubCore) SaveSession(string) error { return nil } func (c *stubCore) ToolCallCount() int64 { return 0 } func (c *stubCore) CompactionModel() string { return "" } func (c *stubCore) SetCompactionModel(string) {} func (c *stubCore) Bus() *pubsub.Bus { if c.bus_ == nil { c.bus_ = pubsub.NewBus() } return c.bus_ } type publishCore struct{ *stubCore } func (c *publishCore) Submit(_ context.Context, input string) { c.submitted = append(c.submitted, input) bus := c.Bus() bus.Publish("event", agent.Event{Role: "user", Content: input}) bus.Publish("event", agent.Event{Role: "assistant", Content: "thinking..."}) bus.Publish("event", agent.Event{Role: "call", Name: "fn", Content: "arg1"}) bus.Publish("event", agent.Event{Role: "tool", Content: "result"}) bus.Publish("event", agent.Event{Role: "assistant", Content: "done"}) } type blockingCore struct{ *stubCore } func (c *blockingCore) Submit(ctx context.Context, input string) { c.submitted = append(c.submitted, input) <-ctx.Done() } func testSink() *olog.Sink { return olog.NewSink(io.Discard, io.Discard, olog.LevelError) } func testSession(id string) *session.Session { ctx, cancel := context.WithCancel(context.Background()) return session.NewSession(id, &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}, 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.Core, error) { return &stubCore{state: "idle", backend_: "stub", model: "m", agentName: agentName, cwd: cwd}, 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 func newTestSessionFileStore(t *testing.T, sess *session.Session) (*fs.Tree, *stubCore) { t.Helper() sink := testSink() core := sess.Core.(*stubCore) sf := session.NewSessionTree(sess, sink.NewLogger("test"), func() {}, func(id string) error { return nil }, func(data []byte) error { return nil }, func() {}, nil, ) return sf, core } 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) } // ===== 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") } core := sess.Core.(*stubCore) if !core.closed { t.Error("core should be closed 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 := testSession("s1") defer sess.Cancel() sess.Core.(*stubCore).running = true 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 := testSession("r") sess.Core.(*stubCore).running = true s.AddSession(sess) if err := s.Tree().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.Tree().Rename("sid", "sid2"); err == nil { t.Error("Rename with SetSessionID error should error") } } // ===== 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) } 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) 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) 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) 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) 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) testStoreWrite(t, sf, "cfg", []byte("cwd=/new/path")) core := sess.Core.(*stubCore) if core.cwd != "/new/path" { t.Errorf("cwd = %q; want /new/path", 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) // 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() core := sess.Core.(*stubCore) core.usage = "100" core.ctxsz = "4096" core.models = "m1\nm2" core.sysprompt = "you are helpful" sess.ChatOffset = 5 sf, _ := newTestSessionFileStore(t, sess) // individual metric files for _, tc := range []struct { name, want string }{ {"usage", "100\n"}, {"ctxsz", "4096\n"}, {"models", "m1\nm2\n"}, {"systemprompt", "you are helpful"}, {"offset", "5\n"}, } { data := testStoreRead(t, sf, tc.name) if string(data) != tc.want { t.Errorf("Read(%q) = %q; want %q", tc.name, data, tc.want) } } // spec contains all config and current state 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() core := sess.Core.(*stubCore) core.queued = []string{"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, core := newTestSessionFileStore(t, sess) core.submitCh = make(chan struct{}) testStoreWrite(t, sf, "prompt", []byte("hello agent")) <-core.submitCh if len(core.submitted) != 1 || core.submitted[0] != "hello agent" { t.Errorf("submitted = %v; want [hello agent]", core.submitted) } } func TestSessionFileStoreWriteFifoIn(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf, core := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "fifo.in", []byte("inject this")) if len(core.queued) != 1 || core.queued[0] != "inject this" { t.Errorf("queued = %v; want [inject this]", core.queued) } } 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, core := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "cfg", []byte("backend=openai")) if len(core.submitted) != 1 || core.submitted[0] != "/backend openai" { t.Errorf("submitted = %v; want [/backend openai]", core.submitted) } testStoreWrite(t, sf, "cfg", []byte("model=gpt-4")) if core.submitted[1] != "/model gpt-4" { t.Errorf("submitted[1] = %q; want /model gpt-4", core.submitted[1]) } testStoreWrite(t, sf, "cfg", []byte("agent=coder")) if core.submitted[2] != "/agent coder" { t.Errorf("submitted[2] = %q; want /agent coder", core.submitted[2]) } } func TestSessionFileStoreWriteBackendWhileRunning(t *testing.T) { sess := testSession("s1") defer sess.Cancel() core := sess.Core.(*stubCore) core.running = true 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, core := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "cfg", []byte("maxTokens=2048")) if core.params.MaxTokens != 2048 { t.Errorf("MaxTokens = %d; want 2048", core.params.MaxTokens) } } func TestSessionFileStoreWriteParamsWhileRunning(t *testing.T) { sess := testSession("s1") defer sess.Cancel() core := sess.Core.(*stubCore) core.running = true 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 := testSession("s1") defer sess.Cancel() core := sess.Core.(*stubCore) core.running = true sf, _ := newTestSessionFileStore(t, sess) // stop testStoreWrite(t, sf, "ctl", []byte("stop")) if !core.interrupted { t.Error("ctl stop should interrupt") } // 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 core5 := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"} ctx5, cancel5 := context.WithCancel(context.Background()) defer cancel5() sess5 := session.NewSession("s5", core5, ctx5, cancel5) sf5, _ := newTestSessionFileStore(t, sess5) for _, cmd := range []string{"compact", "clear", "help", "history", "tools", "skills"} { testStoreWrite(t, sf5, "ctl", []byte(cmd)) } for i, cmd := range []string{"compact", "clear", "help", "history", "tools", "skills"} { want := "/" + cmd if i >= len(core5.submitted) || core5.submitted[i] != want { t.Errorf("submitted[%d] = %q; want %q", i, core5.submitted[i], want) } } } 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) { core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", waitCh: make(chan string, 1)} ctx, cancel := context.WithCancel(context.Background()) sess := session.NewSession("s1", core, ctx, cancel) defer cancel() sf, _ := newTestSessionFileStore(t, sess) core.waitCh <- "running" e, err := sf.Open("statewait") if err != nil { t.Fatalf("Open(statewait): %v", err) } data, _, err := e.BlockingRead(context.Background(), "idle") if err != nil { t.Fatalf("BlockingRead: %v", err) } if string(data) != "running\n" { t.Errorf("BlockingRead = %q; want running\\n", data) } } func TestSessionFileStoreBlockingReadCancel(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf, _ := newTestSessionFileStore(t, sess) ctx, cancel := context.WithCancel(context.Background()) cancel() e, _ := sf.Open("statewait") data, _, err := e.BlockingRead(ctx, "idle") if err != nil { t.Fatalf("BlockingRead error: %v", err) } // On cancel/timeout, returns current state instead of nil if string(data) != "idle\n" { t.Errorf("BlockingRead cancelled = %q; want idle\\n", data) } } func TestSessionFileStoreBlockingReadNotWaitFile(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sf, _ := newTestSessionFileStore(t, sess) e, _ := sf.Open("chat") if _, _, err := e.BlockingRead(context.Background(), ""); err == nil { t.Error("BlockingRead(chat) should error") } } func TestSessionFileStoreBlockingReadAllWaitFiles(t *testing.T) { // Start with state="thinking" so statewait blocks until state changes. // When base="" and state is already "idle", statewait returns immediately // to avoid blocking forever in wait loops. core := &stubCore{state: "thinking", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", usage: "0", ctxsz: "0", waitCh: make(chan string, 1)} ctx, cancel := context.WithCancel(context.Background()) sess := session.NewSession("s1", core, ctx, cancel) defer cancel() sf, _ := newTestSessionFileStore(t, sess) for _, name := range []string{"statewait"} { core.waitCh <- "newval" e, err := sf.Open(name) if err != nil { t.Fatalf("Open(%q): %v", name, err) } data, _, err := e.BlockingRead(context.Background(), "") if err != nil { t.Fatalf("BlockingRead(%q): %v", name, err) } if string(data) != "newval\n" { t.Errorf("BlockingRead(%q) = %q; want newval\\n", name, data) } } } func TestSessionFileStoreBlockingReadIdleTimeout(t *testing.T) { // When state doesn't change, timeout returns current state core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() sess := session.NewSession("s1", core, ctx, cancel) sf, _ := newTestSessionFileStore(t, sess) e, err := sf.Open("statewait") if err != nil { t.Fatalf("Open(statewait): %v", err) } data, _, err := e.BlockingRead(ctx, "") if err != nil { t.Fatalf("BlockingRead: %v", err) } if string(data) != "idle\n" { t.Errorf("BlockingRead = %q; want idle\\n", data) } } func TestSessionFileStoreMakePublish(t *testing.T) { sess := testSession("s1") defer sess.Cancel() core := sess.Core.(*stubCore) core.reply = "hello back" sf, _ := newTestSessionFileStore(t, sess) testStoreWrite(t, sf, "prompt", []byte("hi")) time.Sleep(10 * time.Millisecond) l, _ := sess.LogInfo() if l == 0 { t.Error("session log should be non-empty after prompt+reply") } // Read the log to verify format data := testStoreRead(t, sf, "chat") if !strings.Contains(string(data), "[assistant]\nhello back") { t.Errorf("chat log = %q; want to contain '[assistant]\\nhello back'", data) } } func TestSessionFileStoreMakePublishMultipleEvents(t *testing.T) { // Manually exercise makePublish with varied event sequences core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"} // Override Submit to emit a sequence of events core2 := &publishCore{stubCore: core} ctx, cancel := context.WithCancel(context.Background()) defer cancel() sess := session.NewSession("s1", core2, ctx, cancel) sf := newTestSessionFileStoreWith(t, sess, func() {}, func(string) error { return nil }, func([]byte) error { return nil }) testStoreWrite(t, sf, "prompt", []byte("test")) time.Sleep(10 * time.Millisecond) data := testStoreRead(t, sf, "chat") s := string(data) // Should contain user prefix, assistant prefix, tool call if !strings.Contains(s, "[user]\n") { t.Errorf("missing user header in %q", s) } if !strings.Contains(s, "[assistant]\n") { t.Errorf("missing assistant header in %q", s) } if !strings.Contains(s, "[call:fn]\n") { t.Errorf("missing call header in %q", s) } } func TestSessionFileStoreEntryStat(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sess.AppendLog([]byte("hello")) sf, _ := newTestSessionFileStore(t, sess) e, _ := sf.Open("chat") fi, err := e.Stat() if err != nil { t.Fatalf("Stat: %v", err) } if fi.Size() != 5 { t.Errorf("entry Stat(chat).Size() = %d; want 5", fi.Size()) } e2, _ := sf.Open("statewait") fi2, err := e2.Stat() if err != nil { t.Fatalf("Stat: %v", err) } // Wait files report non-zero size so FUSE clients attempt to read if fi2.Size() == 0 { t.Error("entry Stat(statewait).Size() = 0; want non-zero for wait files") } e3, _ := sf.Open("cfg") fi3, err := e3.Stat() if err != nil { t.Fatalf("Stat: %v", err) } if fi3.Size() == 0 { t.Error("entry Stat(spec).Size() = 0; want non-zero") } } func TestSessionReactFile(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() core := &stubCore{state: "idle"} sess := session.NewSession("s1", core, ctx, cancel) store := session.NewSessionTree(sess, testSink().NewLogger("test"), func() {}, func(string) error { return nil }, nil, nil, nil) testStoreWrite(t, store, "react", []byte("👍")) if core.reactResponseID != "" || core.reactEmoji != "👍" { t.Fatalf("plain reaction = (%q, %q)", core.reactResponseID, core.reactEmoji) } testStoreWrite(t, store, "react", []byte(`{"responseId":"r1","emoji":"👎"}`)) if core.reactResponseID != "r1" || core.reactEmoji != "👎" { t.Fatalf("JSON reaction = (%q, %q)", core.reactResponseID, core.reactEmoji) } if _, err := store.Stat("react"); err != nil { t.Fatalf("Stat(react): %v", err) } } func TestSessionManagerOpenStore(t *testing.T) { s := newTestSessionManager(t) sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) rs, err := s.OpenStore("s1") if err != nil { t.Fatalf("OpenStore: %v", err) } data := testStoreRead(t, rs, "cfg") if !strings.Contains(string(data), "name=s1\n") { t.Errorf("Read(cfg) = %q; want name=s1\\n", data) } if _, err := s.OpenStore("nope"); err == nil { t.Error("OpenStore(nonexistent) should error") } } // ===== session.LoadAgentConfig ===== func TestLoadAgentConfig(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "test.json"), []byte(`{}`), 0644) cfg := session.LoadAgentConfig(dir, "test", nil) if cfg == nil { t.Error("session.LoadAgentConfig should return non-nil for existing file") } cfg = session.LoadAgentConfig(dir, "nonexistent", nil) if cfg != nil { t.Error("session.LoadAgentConfig should return nil for missing file") } } // ===== session.FormatEvent ===== func TestFormatEvent(t *testing.T) { for _, tc := range []struct { ev agent.Event want string }{ {agent.Event{Role: "user", Content: "hi"}, "[user]\nhi\n"}, {agent.Event{Role: "assistant", Content: "hello"}, "hello"}, {agent.Event{Role: "reasoning", Content: "think"}, "think"}, {agent.Event{Role: "error", Content: "oops"}, "[error]\noops\n"}, {agent.Event{Role: "call", Name: "fn", Content: "args"}, "[call:fn]\nargs\n"}, {agent.Event{Role: "tool", Name: "fn", Content: "result\n"}, "[tool:fn]\nresult\n"}, {agent.Event{Role: "retry", Content: "5"}, "[retry]\n5s\n"}, {agent.Event{Role: "stalled"}, "[stalled]\n"}, {agent.Event{Role: "info", Content: "note"}, "[info]\nnote"}, {agent.Event{Role: "unknown"}, ""}, } { got := string(session.FormatEvent(tc.ev)) if got != tc.want { t.Errorf("session.FormatEvent(%q) = %q; want %q", tc.ev.Role, got, tc.want) } } } // ===== SyntheticFileInfo / SyntheticEntry / FileEntry / DirEntry ===== func TestSyntheticFileInfo(t *testing.T) { fi := &fs.SyntheticFileInfo{Name_: "f", Mode_: 0644, Size_: 42, IsDir_: false} if fi.Name() != "f" || fi.Size() != 42 || fi.Mode() != 0644 || fi.IsDir() || fi.Sys() != nil { t.Error("SyntheticFileInfo field mismatch") } if !fi.ModTime().IsZero() { t.Error("ModTime should be zero") } } func TestFileEntryDirEntry(t *testing.T) { fe := fs.FileEntry("f", 0644) if fe.Name() != "f" || fe.IsDir() || fe.Type() != 0 { t.Error("FileEntry mismatch") } de := fs.DirEntry("d", 0755) if de.Name() != "d" || !de.IsDir() || de.Type() != os.ModeDir { t.Error("DirEntry mismatch") } // Info() info, err := de.Info() if err != nil || info.Name() != "d" || !info.IsDir() { t.Error("DirEntry.Info() mismatch") } } // ===== session.FormatParams / session.ParseParams ===== func TestFormatParamsRoundTrip(t *testing.T) { temp := 0.7 freq := 0.1 pres := 0.2 p := backend.GenerationParams{ MaxTokens: 1024, Temperature: &temp, FrequencyPenalty: &freq, PresencePenalty: &pres, } out := session.FormatParams(p) got, err := session.ParseParams(out, backend.GenerationParams{}) if err != nil { t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 1024 { t.Errorf("MaxTokens = %d; want 1024", got.MaxTokens) } if got.Temperature == nil || *got.Temperature != 0.7 { t.Errorf("Temperature = %v; want 0.7", got.Temperature) } if got.FrequencyPenalty == nil || *got.FrequencyPenalty != 0.1 { t.Errorf("FrequencyPenalty = %v; want 0.1", got.FrequencyPenalty) } if got.PresencePenalty == nil || *got.PresencePenalty != 0.2 { t.Errorf("PresencePenalty = %v; want 0.2", got.PresencePenalty) } } func TestFormatParamsNilOptionals(t *testing.T) { p := backend.GenerationParams{MaxTokens: 512} out := session.FormatParams(p) got, err := session.ParseParams(out, backend.GenerationParams{}) if err != nil { t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 512 { t.Errorf("MaxTokens = %d; want 512", got.MaxTokens) } if got.Temperature != nil { t.Errorf("Temperature should be nil, got %v", got.Temperature) } } func TestParseParamsClearWithEmpty(t *testing.T) { temp := 1.0 p := backend.GenerationParams{MaxTokens: 100, Temperature: &temp} got, err := session.ParseParams("maxTokens=\ntemperature=\nfrequencyPenalty=\npresencePenalty=\n", p) if err != nil { t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 0 { t.Errorf("MaxTokens = %d; want 0", got.MaxTokens) } if got.Temperature != nil { t.Errorf("Temperature should be nil") } } func TestParseParamsErrors(t *testing.T) { if _, err := session.ParseParams("maxTokens=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid maxTokens") } if _, err := session.ParseParams("temperature=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid temperature") } if _, err := session.ParseParams("frequencyPenalty=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid frequencyPenalty") } if _, err := session.ParseParams("presencePenalty=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid presencePenalty") } } // ===== session.Session.Interrupt ===== func TestSessionInterrupt(t *testing.T) { sess := testSession("i1") defer sess.Cancel() sess.Interrupt() // stubCore.Interrupt is a no-op; just ensure it doesn't panic } // ===== session.Manager.createSession ===== 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("name=testsess cwd=/tmp agent=default")); err != nil { t.Fatalf("Write(new): %v", err) } if s.Session("testsess") == nil { t.Error("session 'testsess' not found after create") } s.KillSession("testsess") } func TestSessionManagerCreateSessionNoCwd(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().Open("new") if err := e.Write([]byte("name=nocwd")); err == nil { t.Error("expected error for missing cwd") } } func TestSessionManagerCreateSessionDuplicate(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().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") } s.KillSession("dup") } func TestSessionManagerCreateSessionBadOption(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().Open("new") if err := e.Write([]byte("bogus cwd=/tmp")); err == nil { t.Error("expected error for invalid option") } } func TestSessionManagerCreateSessionUnknownKey(t *testing.T) { s := newTestSessionManagerWithCore(t) e, _ := s.Tree().Open("new") if err := e.Write([]byte("unknown=x cwd=/tmp")); err == nil { t.Error("expected error for unknown key") } } func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("no home dir") } t.Setenv("HOME", home) var gotCwd string 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.Core, error) { gotCwd = cwd return &stubCore{state: "idle", backend_: "stub", model: "m", agentName: agentName, cwd: cwd}, nil }, }) e, _ := s.Tree().Open("new") if err := e.Write([]byte("name=envtest cwd=$HOME/")); err != nil { t.Fatalf("Write(new): %v", err) } if gotCwd != home+"/" { t.Errorf("cwd env not expanded: got %q, want %q", gotCwd, home+"/") } s.KillSession("envtest") } func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skip("no home dir") } var gotCwd string 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.Core, error) { gotCwd = cwd return &stubCore{state: "idle", backend_: "stub", model: "m", agentName: agentName, cwd: cwd}, nil }, }) e, _ := s.Tree().Open("new") if err := e.Write([]byte("name=tildetest cwd=~/")); err != nil { t.Fatalf("Write(new): %v", err) } if gotCwd != home+"/" { t.Errorf("cwd not expanded: got %q, want %q", gotCwd, home+"/") } s.KillSession("tildetest") } // ===== openEntry not-found ===== func TestSessionManagerOpenEntryNotFound(t *testing.T) { s := newTestSessionManager(t) if _, err := s.Tree().Open("__nonexistent__"); err == nil { t.Error("Open(nonexistent) should error") } } // ===== OpenStore not-found ===== func TestSessionManagerOpenStoreNotFound(t *testing.T) { s := newTestSessionManager(t) if _, err := s.OpenStore("__missing__"); err == nil { t.Error("OpenStore(missing) should error") } }