package builtin import ( "context" "encoding/json" "strings" "sync" "testing" "time" ) // ========== 内存 ScratchpadStore stub(用于测试)========== // memScratchpad 是测试用的内存 ScratchpadStore 实现. // 不依赖 engine 包(避免循环导入),行为与 engine.Scratchpad 一致. type memScratchpad struct { mu sync.RWMutex entries map[string]memEntry } type memEntry struct { value string expiresAt time.Time } func (m *memScratchpad) isExpired(e memEntry) bool { if e.expiresAt.IsZero() { return false } return time.Now().After(e.expiresAt) } func newMemScratchpad() *memScratchpad { return &memScratchpad{entries: make(map[string]memEntry)} } func (m *memScratchpad) Set(key, value string, ttl time.Duration) { m.mu.Lock() defer m.mu.Unlock() e := memEntry{value: value} if ttl > 0 { e.expiresAt = time.Now().Add(ttl) } m.entries[key] = e } func (m *memScratchpad) Get(key string) (string, bool) { m.mu.RLock() e, ok := m.entries[key] m.mu.RUnlock() if !ok || m.isExpired(e) { return "", false } return e.value, true } func (m *memScratchpad) Delete(key string) { m.mu.Lock() defer m.mu.Unlock() delete(m.entries, key) } func (m *memScratchpad) Keys() []string { m.mu.RLock() defer m.mu.RUnlock() var keys []string for k, e := range m.entries { if !m.isExpired(e) { keys = append(keys, k) } } return keys } // ========== ScratchpadWriteTool 测试 ========== func TestScratchpadWriteTool_Name(t *testing.T) { tool := NewScratchpadWriteTool(newMemScratchpad()) if tool.Name() != "scratchpad_write" { t.Errorf("Name() 应为 'scratchpad_write',got %q", tool.Name()) } } func TestScratchpadWriteTool_Execute_Basic(t *testing.T) { store := newMemScratchpad() tool := NewScratchpadWriteTool(store) input := json.RawMessage(`{"key": "mykey", "value": "myvalue"}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } if result.IsError { t.Errorf("结果不应是错误,Output=%q", result.Output) } if !strings.Contains(result.Output, "mykey") { t.Errorf("输出应包含 key 名称,got %q", result.Output) } // 验证确实写入了 store val, found := store.Get("mykey") if !found || val != "myvalue" { t.Errorf("写入后 store 应包含该值,found=%v val=%q", found, val) } } func TestScratchpadWriteTool_Execute_WithTTL(t *testing.T) { store := newMemScratchpad() tool := NewScratchpadWriteTool(store) input := json.RawMessage(`{"key": "ttlkey", "value": "ttlval", "ttl_seconds": 60}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } if !strings.Contains(result.Output, "expires") { t.Errorf("带 TTL 的输出应包含 'expires',got %q", result.Output) } // 值应存在(还未过期) val, found := store.Get("ttlkey") if !found || val != "ttlval" { t.Errorf("TTL 未过期时应能读取,found=%v val=%q", found, val) } } func TestScratchpadWriteTool_Execute_ExpiredTTL(t *testing.T) { store := newMemScratchpad() // 注:此测试不使用 write tool(直接操作 store 更可靠地控制 TTL) _ = NewScratchpadWriteTool(store) // 更可靠的方式:直接调 store.Set,然后读工具验证过期行为 store.Set("expiredkey", "val", time.Nanosecond) time.Sleep(time.Microsecond) // scratchpad_read 验证 readTool := NewScratchpadReadTool(store) readInput := json.RawMessage(`{"key": "expiredkey"}`) result, _ := readTool.Execute(context.Background(), readInput, nil) var out map[string]any if err := json.Unmarshal([]byte(result.Output), &out); err != nil { t.Fatalf("输出应为 JSON,got %q: %v", result.Output, err) } if found, _ := out["found"].(bool); found { t.Error("过期的 key 不应 found=true") } } func TestScratchpadWriteTool_Execute_EmptyKey(t *testing.T) { tool := NewScratchpadWriteTool(newMemScratchpad()) input := json.RawMessage(`{"key": "", "value": "val"}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("空 key 不应返回 Go error,got %v", err) } if !result.IsError { t.Error("空 key 应返回 IsError=true") } } func TestScratchpadWriteTool_Execute_InvalidJSON(t *testing.T) { tool := NewScratchpadWriteTool(newMemScratchpad()) _, err := tool.Execute(context.Background(), json.RawMessage(`not-json`), nil) if err == nil { t.Error("非法 JSON 应返回 error") } } func TestScratchpadWriteTool_InputSchema_Valid(t *testing.T) { tool := NewScratchpadWriteTool(newMemScratchpad()) schema := tool.InputSchema() var v any if err := json.Unmarshal(schema, &v); err != nil { t.Errorf("InputSchema 应为合法 JSON,got %v", err) } } // ========== ScratchpadReadTool 测试 ========== func TestScratchpadReadTool_Name(t *testing.T) { tool := NewScratchpadReadTool(newMemScratchpad()) if tool.Name() != "scratchpad_read" { t.Errorf("Name() 应为 'scratchpad_read',got %q", tool.Name()) } } func TestScratchpadReadTool_Execute_Found(t *testing.T) { store := newMemScratchpad() store.Set("greeting", "hello world", 0) tool := NewScratchpadReadTool(store) input := json.RawMessage(`{"key": "greeting"}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } if result.IsError { t.Errorf("结果不应是错误,Output=%q", result.Output) } var out map[string]any if err := json.Unmarshal([]byte(result.Output), &out); err != nil { t.Fatalf("输出应为 JSON,got %q: %v", result.Output, err) } if out["value"] != "hello world" { t.Errorf("value 应为 'hello world',got %v", out["value"]) } if found, _ := out["found"].(bool); !found { t.Error("found 应为 true") } } func TestScratchpadReadTool_Execute_NotFound(t *testing.T) { store := newMemScratchpad() tool := NewScratchpadReadTool(store) input := json.RawMessage(`{"key": "missing"}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } var out map[string]any if err := json.Unmarshal([]byte(result.Output), &out); err != nil { t.Fatalf("输出应为 JSON,got %q: %v", result.Output, err) } if found, _ := out["found"].(bool); found { t.Error("不存在的 key found 应为 false") } if out["value"] != "" { t.Errorf("不存在的 key value 应为空字符串,got %v", out["value"]) } } func TestScratchpadReadTool_Execute_EmptyKey(t *testing.T) { tool := NewScratchpadReadTool(newMemScratchpad()) input := json.RawMessage(`{"key": ""}`) result, err := tool.Execute(context.Background(), input, nil) if err != nil { t.Fatalf("空 key 不应返回 Go error,got %v", err) } if !result.IsError { t.Error("空 key 应返回 IsError=true") } } func TestScratchpadReadTool_Metadata_ReadOnly(t *testing.T) { tool := NewScratchpadReadTool(newMemScratchpad()) meta := tool.Metadata() if !meta.ReadOnly { t.Error("scratchpad_read 应标记为 ReadOnly=true") } } // ========== ScratchpadListTool 测试 ========== func TestScratchpadListTool_Name(t *testing.T) { tool := NewScratchpadListTool(newMemScratchpad()) if tool.Name() != "scratchpad_list" { t.Errorf("Name() 应为 'scratchpad_list',got %q", tool.Name()) } } func TestScratchpadListTool_Execute_Empty(t *testing.T) { store := newMemScratchpad() tool := NewScratchpadListTool(store) result, err := tool.Execute(context.Background(), json.RawMessage(`{}`), nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } if !strings.Contains(result.Output, "empty") { t.Errorf("空 scratchpad 输出应包含 'empty',got %q", result.Output) } } func TestScratchpadListTool_Execute_WithKeys(t *testing.T) { store := newMemScratchpad() store.Set("beta", "b", 0) store.Set("alpha", "a", 0) store.Set("gamma", "g", 0) tool := NewScratchpadListTool(store) result, err := tool.Execute(context.Background(), json.RawMessage(`{}`), nil) if err != nil { t.Fatalf("Execute 不应返回错误,got %v", err) } if !strings.Contains(result.Output, "3") { t.Errorf("输出应包含条目数 3,got %q", result.Output) } // 验证三个 key 都出现在输出中 for _, key := range []string{"alpha", "beta", "gamma"} { if !strings.Contains(result.Output, key) { t.Errorf("输出应包含 key %q,got %q", key, result.Output) } } } func TestScratchpadListTool_Execute_NilInput(t *testing.T) { // scratchpad_list 不需要参数,nil input 也应正常工作 store := newMemScratchpad() tool := NewScratchpadListTool(store) result, err := tool.Execute(context.Background(), nil, nil) if err != nil { t.Fatalf("nil input 不应返回错误,got %v", err) } _ = result } func TestScratchpadListTool_Metadata_ReadOnly(t *testing.T) { tool := NewScratchpadListTool(newMemScratchpad()) meta := tool.Metadata() if !meta.ReadOnly { t.Error("scratchpad_list 应标记为 ReadOnly=true") } } // ========== 集成流程测试(Write → Read → List → Delete → List)========== func TestScratchpadTools_Integration(t *testing.T) { store := newMemScratchpad() writeTool := NewScratchpadWriteTool(store) readTool := NewScratchpadReadTool(store) listTool := NewScratchpadListTool(store) ctx := context.Background() // 1. 写入 _, err := writeTool.Execute(ctx, json.RawMessage(`{"key":"step","value":"processing"}`), nil) if err != nil { t.Fatalf("写入失败: %v", err) } // 2. 读取验证 readResult, err := readTool.Execute(ctx, json.RawMessage(`{"key":"step"}`), nil) if err != nil { t.Fatalf("读取失败: %v", err) } var readOut map[string]any json.Unmarshal([]byte(readResult.Output), &readOut) //nolint:errcheck if readOut["value"] != "processing" || readOut["found"] != true { t.Errorf("读取结果不正确: %v", readOut) } // 3. 列出 listResult, _ := listTool.Execute(ctx, json.RawMessage(`{}`), nil) if !strings.Contains(listResult.Output, "step") { t.Errorf("列出结果应包含 'step',got %q", listResult.Output) } // 4. 覆盖写入 writeTool.Execute(ctx, json.RawMessage(`{"key":"step","value":"done"}`), nil) //nolint:errcheck // 5. 再读取 readResult2, _ := readTool.Execute(ctx, json.RawMessage(`{"key":"step"}`), nil) var readOut2 map[string]any json.Unmarshal([]byte(readResult2.Output), &readOut2) //nolint:errcheck if readOut2["value"] != "done" { t.Errorf("覆盖后读取应为 'done',got %v", readOut2["value"]) } }