1283 lines
38 KiB
Go
1283 lines
38 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/simonfxr/pubsub"
|
|
agent "ollie/session"
|
|
"ollie/backend"
|
|
olog "ollie/log"
|
|
"olliesrv/fs"
|
|
"olliesrv/session"
|
|
)
|
|
|
|
// --- stub agent.Session ---
|
|
|
|
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.Session, 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,
|
|
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, 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, 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)
|
|
|
|
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, 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, 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.Session, 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.Session, 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")
|
|
}
|
|
}
|