test: Phase 6 全功能测试 — 19个测试全部通过 + 开发路线文档

- host: 沙箱执行/命令拦截/超时/文件读写/系统信息/路径验证 (6 tests)
- rag: 文本分块/余弦相似度/关键词匹配/文档索引+搜索 (4 tests)
- tools: host_exec/host_file/host_system/knowledge_search/knowledge_ingest (5 tests)
- vision: 图片编码/错误处理/定义验证/执行流程 (4 tests)
- Embedder 重构为接口,支持 API 和 Simple 两种实现
- 添加 ROADMAP.md 未来开发路线

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 07:51:07 +08:00
parent edc20170b9
commit 63a8f95de1
7 changed files with 640 additions and 9 deletions
@@ -0,0 +1,142 @@
package tools
import (
"context"
"os"
"testing"
"github.com/yourname/cyrene-ai/ai-core/internal/host"
"github.com/yourname/cyrene-ai/ai-core/internal/rag"
)
func TestHostExecToolDefinition(t *testing.T) {
cfg := host.DefaultSandboxConfig()
cfg.AllowedDirs = []string{os.TempDir()}
sandbox := host.NewSandbox(cfg)
mgr := host.NewManager(sandbox)
tool := NewHostExecTool(mgr)
def := tool.Definition()
if def.Name != "host_exec" {
t.Fatalf("unexpected name: %s", def.Name)
}
t.Logf("host_exec definition OK")
// Test execute with echo
result, err := tool.Execute(context.Background(), map[string]interface{}{
"command": "echo test-ok",
})
if err != nil {
t.Fatalf("execute error: %v", err)
}
if !result.Success {
t.Fatalf("execute failed: %s", result.Error)
}
t.Logf("host_exec execute OK: data=%s", result.Data[:50])
}
func TestHostFileToolDefinition(t *testing.T) {
cfg := host.DefaultSandboxConfig()
tmpDir := os.TempDir()
cfg.AllowedDirs = []string{tmpDir}
sandbox := host.NewSandbox(cfg)
mgr := host.NewManager(sandbox)
mgr.SetAllowedDirs([]string{tmpDir})
tool := NewHostFileTool(mgr)
def := tool.Definition()
if def.Name != "host_file" {
t.Fatalf("unexpected name: %s", def.Name)
}
t.Logf("host_file definition OK")
// Test list
result, err := tool.Execute(context.Background(), map[string]interface{}{
"action": "list",
"path": tmpDir,
})
if err != nil {
t.Fatalf("list execute error: %v", err)
}
if !result.Success {
t.Fatalf("list failed: %s", result.Error)
}
t.Logf("host_file list OK: data len=%d", len(result.Data))
}
func TestHostSystemToolDefinition(t *testing.T) {
cfg := host.DefaultSandboxConfig()
sandbox := host.NewSandbox(cfg)
mgr := host.NewManager(sandbox)
tool := NewHostSystemTool(mgr)
def := tool.Definition()
if def.Name != "host_system" {
t.Fatalf("unexpected name: %s", def.Name)
}
t.Logf("host_system definition OK")
result, err := tool.Execute(context.Background(), map[string]interface{}{})
if err != nil {
t.Fatalf("execute error: %v", err)
}
if !result.Success {
t.Fatalf("execute failed: %s", result.Error)
}
t.Logf("host_system execute OK: data len=%d", len(result.Data))
}
type testEmbedder struct{}
func (e *testEmbedder) Embed(ctx context.Context, text string) ([]float64, error) {
n := float64(len([]rune(text)))
v := make([]float64, 128)
for _, r := range text {
v[int(r)%128] += 1.0 / n
}
return v, nil
}
func (e *testEmbedder) EmbedBatch(ctx context.Context, texts []string) ([]float64, error) {
combined := ""
for _, t := range texts {
combined += t
}
return e.Embed(ctx, combined)
}
func (e *testEmbedder) IsAvailable() bool { return true }
func TestKnowledgeSearchToolDefinition(t *testing.T) {
store := rag.NewKnowledgeStore(&testEmbedder{}, os.TempDir())
retriever := rag.NewRetriever(store)
tool := NewKnowledgeSearchTool(retriever)
def := tool.Definition()
if def.Name != "knowledge_search" {
t.Fatalf("unexpected name: %s", def.Name)
}
t.Logf("knowledge_search definition OK")
result, err := tool.Execute(context.Background(), map[string]interface{}{
"query": "test query",
})
if err != nil {
t.Fatalf("execute error: %v", err)
}
if !result.Success {
t.Fatalf("execute failed: %s", result.Error)
}
t.Logf("knowledge_search execute OK: data=%s", result.Data[:80])
}
func TestKnowledgeIngestToolDefinition(t *testing.T) {
store := rag.NewKnowledgeStore(&testEmbedder{}, os.TempDir())
tool := NewKnowledgeIngestTool(store)
def := tool.Definition()
if def.Name != "knowledge_ingest" {
t.Fatalf("unexpected name: %s", def.Name)
}
t.Logf("knowledge_ingest definition OK")
}
@@ -0,0 +1,90 @@
package tools
import (
"context"
"encoding/base64"
"os"
"path/filepath"
"strings"
"testing"
)
func TestEncodeImageToDataURL(t *testing.T) {
// Create a minimal 1x1 PNG
pngBytes, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==")
tmpPath := filepath.Join(os.TempDir(), "cyrene-test-vision.png")
if err := os.WriteFile(tmpPath, pngBytes, 0644); err != nil {
t.Fatalf("write test image: %v", err)
}
defer os.Remove(tmpPath)
dataURL, mimeType, err := encodeImageToDataURL(tmpPath)
if err != nil {
t.Fatalf("encode: %v", err)
}
if !strings.HasPrefix(dataURL, "data:image/png;base64,") {
t.Fatalf("unexpected data URL: %s...", dataURL[:50])
}
if mimeType != "image/png" {
t.Fatalf("unexpected mime type: %s", mimeType)
}
t.Logf("encode OK: mime=%s, len=%d", mimeType, len(dataURL))
}
func TestEncodeImageToDataURL_InvalidPath(t *testing.T) {
_, _, err := encodeImageToDataURL("/nonexistent/image.png")
if err == nil {
t.Fatal("expected error for nonexistent file")
}
t.Logf("error handling OK: %v", err)
}
func TestVisionToolDefinition(t *testing.T) {
tool := NewVisionTool()
def := tool.Definition()
if def.Name != "vision_analyze" {
t.Fatalf("unexpected tool name: %s", def.Name)
}
params := def.Parameters
props, ok := params["properties"].(map[string]interface{})
if !ok {
t.Fatal("missing properties")
}
if props["image_path"] == nil {
t.Fatal("missing image_path parameter")
}
if props["task"] == nil {
t.Fatal("missing task parameter")
}
t.Logf("definition OK: name=%s, params=%v", def.Name, def.Parameters)
}
func TestVisionToolExecute(t *testing.T) {
// Create test image
pngBytes, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==")
tmpPath := filepath.Join(os.TempDir(), "cyrene-test-vision-exec.png")
if err := os.WriteFile(tmpPath, pngBytes, 0644); err != nil {
t.Fatalf("write test image: %v", err)
}
defer os.Remove(tmpPath)
tool := NewVisionTool()
ctx := context.Background()
result, err := tool.Execute(ctx, map[string]interface{}{
"image_path": tmpPath,
"task": "ocr",
})
if err != nil {
t.Fatalf("execute: %v", err)
}
if !result.Success {
t.Fatalf("execute failed: %s", result.Error)
}
if !strings.Contains(result.Data, "data:image/png;base64,") {
t.Fatal("result missing data URL")
}
if !strings.Contains(result.Data, "ocr") {
t.Fatal("result missing task info")
}
t.Logf("execute OK: data len=%d", len(result.Data))
}