mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-04 08:19:34 +00:00
Session tools (sessions_list, session_status, sessions_history, sessions_send) were using resolveAgentIDString(ctx) which returns the agent UUID, but session keys are built using agent_key. This caused all session tool operations to fail silently or return "access denied" for every channel. Replace resolveAgentIDString() with ToolAgentKeyFromCtx() in all four session tools. Add fail-closed guard for empty agent key.
712 lines
20 KiB
Go
712 lines
20 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// ============================================================
|
|
// mockSessionStore — minimal mock implementing store.SessionStore
|
|
// ============================================================
|
|
|
|
type mockSessionStore struct {
|
|
mu sync.RWMutex
|
|
sessions map[string]*store.SessionData
|
|
}
|
|
|
|
func newMockSessionStore() *mockSessionStore {
|
|
return &mockSessionStore{sessions: make(map[string]*store.SessionData)}
|
|
}
|
|
|
|
func (m *mockSessionStore) seed(key string, msgs []providers.Message, label string) {
|
|
now := time.Now()
|
|
m.sessions[key] = &store.SessionData{
|
|
Key: key,
|
|
Messages: msgs,
|
|
Label: label,
|
|
Created: now,
|
|
Updated: now,
|
|
}
|
|
}
|
|
|
|
func (m *mockSessionStore) GetOrCreate(_ context.Context, key string) *store.SessionData {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
return d
|
|
}
|
|
now := time.Now()
|
|
d := &store.SessionData{Key: key, Messages: []providers.Message{}, Created: now, Updated: now}
|
|
m.sessions[key] = d
|
|
return d
|
|
}
|
|
|
|
func (m *mockSessionStore) Get(_ context.Context, key string) *store.SessionData {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.sessions[key]
|
|
}
|
|
|
|
func (m *mockSessionStore) AddMessage(_ context.Context, key string, msg providers.Message) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
d.Messages = append(d.Messages, msg)
|
|
}
|
|
}
|
|
|
|
func (m *mockSessionStore) GetHistory(_ context.Context, key string) []providers.Message {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
cp := make([]providers.Message, len(d.Messages))
|
|
copy(cp, d.Messages)
|
|
return cp
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockSessionStore) GetSummary(_ context.Context, key string) string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
return d.Summary
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *mockSessionStore) SetSummary(_ context.Context, key, summary string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
d.Summary = summary
|
|
}
|
|
}
|
|
|
|
func (m *mockSessionStore) GetLabel(_ context.Context, key string) string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
return d.Label
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *mockSessionStore) SetLabel(_ context.Context, key, label string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if d, ok := m.sessions[key]; ok {
|
|
d.Label = label
|
|
}
|
|
}
|
|
|
|
func (m *mockSessionStore) SetAgentInfo(context.Context, string, uuid.UUID, string) {}
|
|
func (m *mockSessionStore) TruncateHistory(context.Context, string, int) {}
|
|
func (m *mockSessionStore) SetHistory(context.Context, string, []providers.Message) {}
|
|
func (m *mockSessionStore) Reset(context.Context, string) {}
|
|
func (m *mockSessionStore) Delete(context.Context, string) error { return nil }
|
|
func (m *mockSessionStore) Save(context.Context, string) error { return nil }
|
|
|
|
func (m *mockSessionStore) UpdateMetadata(context.Context, string, string, string, string) {}
|
|
func (m *mockSessionStore) AccumulateTokens(context.Context, string, int64, int64) {}
|
|
func (m *mockSessionStore) IncrementCompaction(context.Context, string) {}
|
|
func (m *mockSessionStore) GetCompactionCount(context.Context, string) int { return 0 }
|
|
func (m *mockSessionStore) GetMemoryFlushCompactionCount(context.Context, string) int { return 0 }
|
|
func (m *mockSessionStore) SetMemoryFlushDone(context.Context, string) {}
|
|
func (m *mockSessionStore) GetSessionMetadata(context.Context, string) map[string]string { return nil }
|
|
func (m *mockSessionStore) SetSessionMetadata(context.Context, string, map[string]string) {}
|
|
func (m *mockSessionStore) SetSpawnInfo(context.Context, string, string, int) {}
|
|
func (m *mockSessionStore) SetContextWindow(context.Context, string, int) {}
|
|
func (m *mockSessionStore) GetContextWindow(context.Context, string) int { return 0 }
|
|
func (m *mockSessionStore) SetLastPromptTokens(context.Context, string, int, int) {}
|
|
func (m *mockSessionStore) GetLastPromptTokens(context.Context, string) (int, int) { return 0, 0 }
|
|
|
|
func (m *mockSessionStore) List(_ context.Context, agentID string) []store.SessionInfo {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
var result []store.SessionInfo
|
|
prefix := "agent:" + agentID + ":"
|
|
for _, d := range m.sessions {
|
|
if agentID == "" || strings.HasPrefix(d.Key, prefix) {
|
|
result = append(result, store.SessionInfo{
|
|
Key: d.Key,
|
|
MessageCount: len(d.Messages),
|
|
Created: d.Created,
|
|
Updated: d.Updated,
|
|
Label: d.Label,
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m *mockSessionStore) ListPaged(context.Context, store.SessionListOpts) store.SessionListResult {
|
|
return store.SessionListResult{}
|
|
}
|
|
func (m *mockSessionStore) ListPagedRich(context.Context, store.SessionListOpts) store.SessionListRichResult {
|
|
return store.SessionListRichResult{}
|
|
}
|
|
func (m *mockSessionStore) LastUsedChannel(context.Context, string) (string, string) {
|
|
return "", ""
|
|
}
|
|
|
|
// ============================================================
|
|
// test helpers
|
|
// ============================================================
|
|
|
|
func agentCtx(agentID string) context.Context {
|
|
ctx := context.Background()
|
|
uid, err := uuid.Parse(agentID)
|
|
if err != nil {
|
|
return ctx
|
|
}
|
|
ctx = store.WithAgentID(ctx, uid)
|
|
// Session keys use agent_key, not UUID. In production WithToolAgentKey
|
|
// is set by the agent loop; tests must mirror this.
|
|
return WithToolAgentKey(ctx, agentID)
|
|
}
|
|
|
|
func agentCtxWithSandbox(agentID, sandboxKey string) context.Context {
|
|
ctx := agentCtx(agentID)
|
|
return WithToolSandboxKey(ctx, sandboxKey)
|
|
}
|
|
|
|
const sessTestAgentID = "11111111-1111-1111-1111-111111111111"
|
|
const sessTestAgentID2 = "22222222-2222-2222-2222-222222222222"
|
|
|
|
// ============================================================
|
|
// sessions_list tests
|
|
// ============================================================
|
|
|
|
func TestSessionsList_ReturnsAgentSessions(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
ms.seed("agent:"+sessTestAgentID+":ws:direct:1", nil, "")
|
|
ms.seed("agent:"+sessTestAgentID+":ws:direct:2", nil, "")
|
|
ms.seed("agent:"+sessTestAgentID2+":ws:direct:1", nil, "")
|
|
|
|
tool := NewSessionsListTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{})
|
|
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
|
|
count := int(out["count"].(float64))
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 sessions for agent, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSessionsList_ActiveMinutesFilter(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
ms.seed("agent:"+sessTestAgentID+":ws:direct:1", nil, "")
|
|
// Make one session old
|
|
ms.sessions["agent:"+sessTestAgentID+":ws:direct:1"].Updated = time.Now().Add(-2 * time.Hour)
|
|
ms.seed("agent:"+sessTestAgentID+":ws:direct:2", nil, "")
|
|
|
|
tool := NewSessionsListTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"active_minutes": float64(30),
|
|
})
|
|
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
|
|
count := int(out["count"].(float64))
|
|
if count != 1 {
|
|
t.Fatalf("expected 1 active session, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSessionsList_LimitCapsResults(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
for i := range 5 {
|
|
ms.seed("agent:"+sessTestAgentID+":ws:direct:"+string(rune('a'+i)), nil, "")
|
|
}
|
|
|
|
tool := NewSessionsListTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"limit": float64(2),
|
|
})
|
|
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
|
|
count := int(out["count"].(float64))
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 sessions (limited), got %d", count)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// session_status tests
|
|
// ============================================================
|
|
|
|
func TestSessionStatus_ValidSession(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:1"
|
|
ms.seed(key, []providers.Message{{Role: "user", Content: "hello"}}, "my-label")
|
|
ms.sessions[key].Model = "claude-3"
|
|
ms.sessions[key].InputTokens = 100
|
|
ms.sessions[key].OutputTokens = 50
|
|
|
|
tool := NewSessionStatusTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("unexpected error: %s", res.ForLLM)
|
|
}
|
|
if !strings.Contains(res.ForLLM, "Messages: 1") {
|
|
t.Fatalf("expected message count, got: %s", res.ForLLM)
|
|
}
|
|
if !strings.Contains(res.ForLLM, "Model: claude-3") {
|
|
t.Fatalf("expected model info, got: %s", res.ForLLM)
|
|
}
|
|
if !strings.Contains(res.ForLLM, "Label: my-label") {
|
|
t.Fatalf("expected label, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionStatus_NonExistent_NoPhantom(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
tool := NewSessionStatusTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:999"
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
})
|
|
|
|
if !res.IsError {
|
|
t.Fatal("expected error for non-existent session")
|
|
}
|
|
if !strings.Contains(res.ForLLM, "session not found") {
|
|
t.Fatalf("expected 'session not found', got: %s", res.ForLLM)
|
|
}
|
|
|
|
// Verify no phantom session was created
|
|
if ms.Get(context.Background(), key) != nil {
|
|
t.Fatal("phantom session was created — Get should have been used, not GetOrCreate")
|
|
}
|
|
}
|
|
|
|
func TestSessionStatus_CrossAgent_Denied(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
otherKey := "agent:" + sessTestAgentID2 + ":ws:direct:1"
|
|
ms.seed(otherKey, nil, "")
|
|
|
|
tool := NewSessionStatusTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": otherKey,
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "access denied") {
|
|
t.Fatalf("expected access denied, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionStatus_EmptyAgentID_Error(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
tool := NewSessionStatusTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
// Context with no agent ID
|
|
res := tool.Execute(context.Background(), map[string]any{
|
|
"session_key": "agent:x:ws:direct:1",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "agent context required") {
|
|
t.Fatalf("expected 'agent context required', got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// sessions_history tests
|
|
// ============================================================
|
|
|
|
func TestSessionsHistory_ReturnsMessages(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:1"
|
|
ms.seed(key, []providers.Message{
|
|
{Role: "user", Content: "hello"},
|
|
{Role: "assistant", Content: "hi there"},
|
|
}, "")
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("unexpected error: %s", res.ForLLM)
|
|
}
|
|
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
count := int(out["count"].(float64))
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 messages, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSessionsHistory_FilterToolMessages(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:1"
|
|
ms.seed(key, []providers.Message{
|
|
{Role: "user", Content: "hello"},
|
|
{Role: "tool", Content: "tool result"},
|
|
{Role: "assistant", Content: "response"},
|
|
}, "")
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
// Without include_tools
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
})
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
if int(out["count"].(float64)) != 2 {
|
|
t.Fatalf("expected 2 messages (tool filtered), got %v", out["count"])
|
|
}
|
|
|
|
// With include_tools
|
|
res = tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
"include_tools": true,
|
|
})
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
if int(out["count"].(float64)) != 3 {
|
|
t.Fatalf("expected 3 messages (with tools), got %v", out["count"])
|
|
}
|
|
}
|
|
|
|
func TestSessionsHistory_LimitFromEnd(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:1"
|
|
var msgs []providers.Message
|
|
for range 10 {
|
|
msgs = append(msgs, providers.Message{Role: "user", Content: "msg"})
|
|
}
|
|
ms.seed(key, msgs, "")
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
"limit": float64(3),
|
|
})
|
|
|
|
var out map[string]any
|
|
json.Unmarshal([]byte(res.ForLLM), &out)
|
|
if int(out["count"].(float64)) != 3 {
|
|
t.Fatalf("expected 3 messages (limited), got %v", out["count"])
|
|
}
|
|
}
|
|
|
|
func TestSessionsHistory_NonExistent_SafeJSON(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
// Use a key with special chars that would break naive string concat
|
|
key := "agent:" + sessTestAgentID + `:ws:direct:"inject`
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": key,
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("non-existent session should return empty, not error: %s", res.ForLLM)
|
|
}
|
|
|
|
// Verify it's valid JSON (no injection)
|
|
var out map[string]any
|
|
if err := json.Unmarshal([]byte(res.ForLLM), &out); err != nil {
|
|
t.Fatalf("invalid JSON (possible injection): %v\nContent: %s", err, res.ForLLM)
|
|
}
|
|
|
|
if int(out["count"].(float64)) != 0 {
|
|
t.Fatalf("expected 0 messages for non-existent session")
|
|
}
|
|
}
|
|
|
|
func TestSessionsHistory_CrossAgent_Denied(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
otherKey := "agent:" + sessTestAgentID2 + ":ws:direct:1"
|
|
ms.seed(otherKey, nil, "")
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": otherKey,
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "access denied") {
|
|
t.Fatalf("expected access denied, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsHistory_EmptyAgentID_Error(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
tool := NewSessionsHistoryTool()
|
|
tool.SetSessionStore(ms)
|
|
|
|
res := tool.Execute(context.Background(), map[string]any{
|
|
"session_key": "agent:x:ws:direct:1",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "agent context required") {
|
|
t.Fatalf("expected 'agent context required', got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// sessions_send tests
|
|
// ============================================================
|
|
|
|
func TestSessionsSend_ValidSend(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
targetKey := "agent:" + sessTestAgentID + ":ws:direct:target"
|
|
ms.seed(targetKey, nil, "")
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
currentSession := "agent:" + sessTestAgentID + ":ws:direct:current"
|
|
ctx := agentCtxWithSandbox(sessTestAgentID, currentSession)
|
|
|
|
res := tool.Execute(ctx, map[string]any{
|
|
"session_key": targetKey,
|
|
"message": "hello target",
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("unexpected error: %s", res.ForLLM)
|
|
}
|
|
|
|
// Verify valid JSON response
|
|
var out map[string]any
|
|
if err := json.Unmarshal([]byte(res.ForLLM), &out); err != nil {
|
|
t.Fatalf("invalid JSON response: %v", err)
|
|
}
|
|
if out["status"] != "accepted" {
|
|
t.Fatalf("expected accepted status, got: %v", out["status"])
|
|
}
|
|
|
|
// Verify message was published to bus
|
|
ctx2, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
msg, ok := msgBus.ConsumeInbound(ctx2)
|
|
if !ok {
|
|
t.Fatal("expected inbound message on bus")
|
|
}
|
|
if msg.Content != "hello target" {
|
|
t.Fatalf("bus message content mismatch: %q", msg.Content)
|
|
}
|
|
if msg.ChatID != targetKey {
|
|
t.Fatalf("bus message chatID mismatch: %q", msg.ChatID)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_LabelResolution(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:labeled"
|
|
ms.seed(key, nil, "my-session")
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
ctx := agentCtxWithSandbox(sessTestAgentID, "agent:"+sessTestAgentID+":ws:direct:other")
|
|
|
|
res := tool.Execute(ctx, map[string]any{
|
|
"label": "my-session",
|
|
"message": "hello",
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("unexpected error: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_LabelNotFound(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"label": "nonexistent",
|
|
"message": "hello",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "no session found with label") {
|
|
t.Fatalf("expected label not found error, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_LabelResolution_NoPhantom(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:1"
|
|
ms.seed(key, nil, "other-label")
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
initialCount := len(ms.sessions)
|
|
|
|
// Search for non-existent label — should NOT create phantom sessions
|
|
tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"label": "nonexistent",
|
|
"message": "hello",
|
|
})
|
|
|
|
if len(ms.sessions) != initialCount {
|
|
t.Fatalf("phantom sessions created: had %d, now %d", initialCount, len(ms.sessions))
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_CrossAgent_Denied(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
otherKey := "agent:" + sessTestAgentID2 + ":ws:direct:1"
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": otherKey,
|
|
"message": "hello",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "access denied") {
|
|
t.Fatalf("expected access denied, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_SelfSend_Blocked(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
key := "agent:" + sessTestAgentID + ":ws:direct:current"
|
|
ms.seed(key, nil, "")
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
// Set sandbox key to same session as target
|
|
ctx := agentCtxWithSandbox(sessTestAgentID, key)
|
|
|
|
res := tool.Execute(ctx, map[string]any{
|
|
"session_key": key,
|
|
"message": "hello myself",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "cannot send to your own current session") {
|
|
t.Fatalf("expected self-send block, got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_EmptyAgentID_Error(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
res := tool.Execute(context.Background(), map[string]any{
|
|
"session_key": "agent:x:ws:direct:1",
|
|
"message": "hello",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "agent context required") {
|
|
t.Fatalf("expected 'agent context required', got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_MissingMessage_Error(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
res := tool.Execute(agentCtx(sessTestAgentID), map[string]any{
|
|
"session_key": "agent:" + sessTestAgentID + ":ws:direct:1",
|
|
})
|
|
|
|
if !res.IsError || !strings.Contains(res.ForLLM, "message is required") {
|
|
t.Fatalf("expected 'message is required', got: %s", res.ForLLM)
|
|
}
|
|
}
|
|
|
|
func TestSessionsSend_ResponseJSON_NoInjection(t *testing.T) {
|
|
ms := newMockSessionStore()
|
|
// Key with quotes that could break naive Sprintf
|
|
key := `agent:` + sessTestAgentID + `:ws:direct:"quoted`
|
|
ms.seed(key, nil, "")
|
|
|
|
msgBus := bus.New()
|
|
tool := NewSessionsSendTool()
|
|
tool.SetSessionStore(ms)
|
|
tool.SetMessageBus(msgBus)
|
|
|
|
ctx := agentCtxWithSandbox(sessTestAgentID, "agent:"+sessTestAgentID+":ws:direct:other")
|
|
|
|
res := tool.Execute(ctx, map[string]any{
|
|
"session_key": key,
|
|
"message": "test",
|
|
})
|
|
|
|
if res.IsError {
|
|
t.Fatalf("unexpected error: %s", res.ForLLM)
|
|
}
|
|
|
|
// Verify valid JSON
|
|
var out map[string]any
|
|
if err := json.Unmarshal([]byte(res.ForLLM), &out); err != nil {
|
|
t.Fatalf("invalid JSON (possible injection): %v\nContent: %s", err, res.ForLLM)
|
|
}
|
|
}
|