249 lines
7.1 KiB
Go
249 lines
7.1 KiB
Go
|
|
package service
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"ai-agent-scaffold-go/internal/model"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 辅助函数
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
func newTestTable() model.AiAgentConfigTable {
|
||
|
|
return model.AiAgentConfigTable{
|
||
|
|
AppName: "test-app",
|
||
|
|
Agent: model.AgentSummary{AgentID: "10001", AgentName: "test", AgentDesc: "test agent"},
|
||
|
|
Module: model.AgentModule{
|
||
|
|
AiAPI: model.AiAPIConfig{BaseURL: "http://localhost:8080", APIKey: "test-key", CompletionsPath: "v1/chat/completions"},
|
||
|
|
ChatModel: model.ChatModelConfig{Model: "gpt-4"},
|
||
|
|
Agents: []model.AgentConfig{{Name: "bot", Instruction: "hello"}},
|
||
|
|
Runner: model.RunnerConfig{AgentName: "bot"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// fakeLLMServer 返回固定回复的模拟 LLM 服务
|
||
|
|
func fakeLLMServer(response string) *httptest.Server {
|
||
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.Write([]byte(response))
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// assembleOne 测试
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
func TestAssembleOne_SingleAgent_Success(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
|
||
|
|
reg, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "10001", reg.AgentID)
|
||
|
|
assert.Equal(t, "test", reg.AgentName)
|
||
|
|
assert.Equal(t, "test-app", reg.AppName)
|
||
|
|
assert.NotNil(t, reg.Runner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_WithWorkflow_Success(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.Agents = []model.AgentConfig{
|
||
|
|
{Name: "agent1", Instruction: "inst1"},
|
||
|
|
{Name: "agent2", Instruction: "inst2"},
|
||
|
|
}
|
||
|
|
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||
|
|
{
|
||
|
|
Type: model.WorkflowTypeSequential,
|
||
|
|
Name: "seq",
|
||
|
|
SubAgents: []string{"agent1", "agent2"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
table.Module.Runner.AgentName = "seq"
|
||
|
|
|
||
|
|
reg, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, reg.Runner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_ParallelWorkflow_Success(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.Agents = []model.AgentConfig{
|
||
|
|
{Name: "a1", Instruction: "inst1"},
|
||
|
|
{Name: "a2", Instruction: "inst2"},
|
||
|
|
}
|
||
|
|
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||
|
|
{
|
||
|
|
Type: model.WorkflowTypeParallel,
|
||
|
|
Name: "par",
|
||
|
|
SubAgents: []string{"a1", "a2"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
table.Module.Runner.AgentName = "par"
|
||
|
|
|
||
|
|
reg, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, reg.Runner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_LoopWorkflow_Success(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.Agents = []model.AgentConfig{
|
||
|
|
{Name: "a1", Instruction: "inst"},
|
||
|
|
}
|
||
|
|
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||
|
|
{
|
||
|
|
Type: model.WorkflowTypeLoop,
|
||
|
|
Name: "loop",
|
||
|
|
SubAgents: []string{"a1"},
|
||
|
|
MaxIterations: 3,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
table.Module.Runner.AgentName = "loop"
|
||
|
|
|
||
|
|
reg, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, reg.Runner)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_UnknownSubAgent_ReturnsError(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||
|
|
{
|
||
|
|
Type: model.WorkflowTypeSequential,
|
||
|
|
Name: "seq",
|
||
|
|
SubAgents: []string{"nonexistent"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
table.Module.Runner.AgentName = "seq"
|
||
|
|
|
||
|
|
_, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Contains(t, err.Error(), "unknown agent")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_UnknownWorkflowType_ReturnsError(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||
|
|
{
|
||
|
|
Type: "bad-type",
|
||
|
|
Name: "wf",
|
||
|
|
SubAgents: []string{"bot"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
table.Module.Runner.AgentName = "wf"
|
||
|
|
|
||
|
|
_, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Contains(t, err.Error(), "unknown workflow type")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleOne_EntryAgentNotFound_ReturnsError(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
table := newTestTable()
|
||
|
|
table.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
table.Module.Runner.AgentName = "nonexistent"
|
||
|
|
|
||
|
|
_, err := assembleOne(context.Background(), table, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Contains(t, err.Error(), "not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// AssembleAll 测试
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
func TestAssembleAll_MultipleTables_Success(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
tables := map[string]model.AiAgentConfigTable{
|
||
|
|
"t1": func() model.AiAgentConfigTable {
|
||
|
|
t := newTestTable()
|
||
|
|
t.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
t.AppName = "app1"
|
||
|
|
t.Agent.AgentID = "1"
|
||
|
|
return t
|
||
|
|
}(),
|
||
|
|
"t2": func() model.AiAgentConfigTable {
|
||
|
|
t := newTestTable()
|
||
|
|
t.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
t.AppName = "app2"
|
||
|
|
t.Agent.AgentID = "2"
|
||
|
|
return t
|
||
|
|
}(),
|
||
|
|
}
|
||
|
|
|
||
|
|
agents, err := AssembleAll(context.Background(), tables, 5*time.Second)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Len(t, agents, 2)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssembleAll_OneFails_ReturnsError(t *testing.T) {
|
||
|
|
srv := fakeLLMServer(`{"choices":[{"message":{"content":"ok"}}]}`)
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
tables := map[string]model.AiAgentConfigTable{
|
||
|
|
"t1": func() model.AiAgentConfigTable {
|
||
|
|
t := newTestTable()
|
||
|
|
t.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
return t
|
||
|
|
}(),
|
||
|
|
"t2": func() model.AiAgentConfigTable {
|
||
|
|
t := newTestTable()
|
||
|
|
t.Module.AiAPI.BaseURL = srv.URL
|
||
|
|
t.Module.Runner.AgentName = "nonexistent"
|
||
|
|
return t
|
||
|
|
}(),
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := AssembleAll(context.Background(), tables, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// LoadAndAssemble 测试
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
func TestLoadAndAssemble_NoPaths_ReturnsError(t *testing.T) {
|
||
|
|
_, err := LoadAndAssemble(context.Background(), []string{}, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Contains(t, err.Error(), "no agent tables loaded")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoadAndAssemble_EmptyPaths_ReturnsError(t *testing.T) {
|
||
|
|
_, err := LoadAndAssemble(context.Background(), []string{"", " "}, 5*time.Second)
|
||
|
|
assert.Error(t, err)
|
||
|
|
}
|