test: 添加各模块单元测试(config/llm/service/handler)
This commit is contained in:
267
backend/internal/config/loader_test.go
Normal file
267
backend/internal/config/loader_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"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"},
|
||||
ChatModel: model.ChatModelConfig{Model: "gpt-4"},
|
||||
Agents: []model.AgentConfig{{Name: "bot", Instruction: "hello"}},
|
||||
Runner: model.RunnerConfig{AgentName: "bot"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTestYAML() []byte {
|
||||
return []byte(`
|
||||
ai:
|
||||
agent:
|
||||
config:
|
||||
tables:
|
||||
test-app:
|
||||
app-name: test-app
|
||||
agent:
|
||||
agent-id: "10001"
|
||||
agent-name: test
|
||||
agent-desc: test agent
|
||||
module:
|
||||
ai-api:
|
||||
base-url: "http://localhost:8080"
|
||||
api-key: "test-key"
|
||||
chat-model:
|
||||
model: "gpt-4"
|
||||
agents:
|
||||
- name: bot
|
||||
instruction: hello
|
||||
runner:
|
||||
agent-name: bot
|
||||
`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// expandEnvPlaceholders 测试
|
||||
// ============================================================
|
||||
|
||||
func TestExpandEnvPlaceholders_SetVar_ReturnsValue(t *testing.T) {
|
||||
t.Setenv("TEST_URL", "http://example.com")
|
||||
result := expandEnvPlaceholders("url=${TEST_URL}")
|
||||
assert.Equal(t, "url=http://example.com", result)
|
||||
}
|
||||
|
||||
func TestExpandEnvPlaceholders_UnsetVar_NoDefault_ReturnsEmpty(t *testing.T) {
|
||||
os.Unsetenv("TEST_MISSING_VAR")
|
||||
result := expandEnvPlaceholders("url=${TEST_MISSING_VAR}")
|
||||
assert.Equal(t, "url=", result)
|
||||
}
|
||||
|
||||
func TestExpandEnvPlaceholders_UnsetVar_WithDefault_ReturnsDefault(t *testing.T) {
|
||||
os.Unsetenv("TEST_DEFAULT_VAR")
|
||||
result := expandEnvPlaceholders("url=${TEST_DEFAULT_VAR:-http://localhost}")
|
||||
assert.Equal(t, "url=http://localhost", result)
|
||||
}
|
||||
|
||||
func TestExpandEnvPlaceholders_SetVar_IgnoresDefault(t *testing.T) {
|
||||
t.Setenv("TEST_OVERRIDE", "http://real.com")
|
||||
result := expandEnvPlaceholders("url=${TEST_OVERRIDE:-http://default.com}")
|
||||
assert.Equal(t, "url=http://real.com", result)
|
||||
}
|
||||
|
||||
func TestExpandEnvPlaceholders_MultipleVars(t *testing.T) {
|
||||
t.Setenv("VAR_A", "aaa")
|
||||
t.Setenv("VAR_B", "bbb")
|
||||
result := expandEnvPlaceholders("${VAR_A}-${VAR_B}")
|
||||
assert.Equal(t, "aaa-bbb", result)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// validateTable 测试 — table-driven
|
||||
// ============================================================
|
||||
|
||||
func TestValidateTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
modify func(*model.AiAgentConfigTable)
|
||||
wantErr string
|
||||
}{
|
||||
{"missing app-name", func(t *model.AiAgentConfigTable) { t.AppName = "" }, "app-name"},
|
||||
{"missing agent-id", func(t *model.AiAgentConfigTable) { t.Agent.AgentID = "" }, "agent.agent-id"},
|
||||
{"missing base-url", func(t *model.AiAgentConfigTable) { t.Module.AiAPI.BaseURL = "" }, "module.ai-api.base-url"},
|
||||
{"missing api-key", func(t *model.AiAgentConfigTable) { t.Module.AiAPI.APIKey = "" }, "module.ai-api.api-key"},
|
||||
{"missing model", func(t *model.AiAgentConfigTable) { t.Module.ChatModel.Model = "" }, "module.chat-model.model"},
|
||||
{"missing runner agent-name", func(t *model.AiAgentConfigTable) { t.Module.Runner.AgentName = "" }, "module.runner.agent-name"},
|
||||
{"missing agents", func(t *model.AiAgentConfigTable) { t.Module.Agents = nil }, "module.agents"},
|
||||
{"empty agent name", func(t *model.AiAgentConfigTable) { t.Module.Agents[0].Name = "" }, "module.agents[0].name"},
|
||||
{"empty agent instruction", func(t *model.AiAgentConfigTable) { t.Module.Agents[0].Instruction = "" }, "module.agents[0].instruction"},
|
||||
{"invalid workflow type", func(t *model.AiAgentConfigTable) {
|
||||
t.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||||
{Type: "bad", Name: "wf"},
|
||||
}
|
||||
}, "module.agent-workflows[0].type is invalid"},
|
||||
{"missing workflow name", func(t *model.AiAgentConfigTable) {
|
||||
t.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||||
{Type: model.WorkflowTypeSequential, Name: ""},
|
||||
}
|
||||
}, "module.agent-workflows[0].name is required"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
table := newTestTable()
|
||||
tt.modify(&table)
|
||||
err := validateTable("test", table)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTable_ValidTable_NoError(t *testing.T) {
|
||||
table := newTestTable()
|
||||
err := validateTable("test", table)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// normalizeDefaults 测试
|
||||
// ============================================================
|
||||
|
||||
func TestNormalizeDefaults_SetsCompletionsPath(t *testing.T) {
|
||||
table := newTestTable()
|
||||
table.Module.AiAPI.CompletionsPath = ""
|
||||
normalizeDefaults(&table)
|
||||
assert.Equal(t, "v1/chat/completions", table.Module.AiAPI.CompletionsPath)
|
||||
}
|
||||
|
||||
func TestNormalizeDefaults_SetsMaxIterations(t *testing.T) {
|
||||
table := newTestTable()
|
||||
table.Module.AgentWorkflows = []model.AgentWorkflowConfig{
|
||||
{Type: model.WorkflowTypeLoop, Name: "loop", MaxIterations: 0},
|
||||
}
|
||||
normalizeDefaults(&table)
|
||||
assert.Equal(t, 3, table.Module.AgentWorkflows[0].MaxIterations)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LoadAgentTables 测试
|
||||
// ============================================================
|
||||
|
||||
func TestLoadAgentTables_ValidYAML_ReturnsTable(t *testing.T) {
|
||||
tables, err := LoadAgentTables(newTestYAML())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tables, 1)
|
||||
|
||||
table, ok := tables["test-app"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test-app", table.AppName)
|
||||
assert.Equal(t, "10001", table.Agent.AgentID)
|
||||
assert.Equal(t, "gpt-4", table.Module.ChatModel.Model)
|
||||
}
|
||||
|
||||
func TestLoadAgentTables_WithEnvVar_Substitutes(t *testing.T) {
|
||||
t.Setenv("TEST_LLM_URL", "http://llm.example.com")
|
||||
yaml := []byte(`
|
||||
ai:
|
||||
agent:
|
||||
config:
|
||||
tables:
|
||||
t:
|
||||
app-name: app
|
||||
agent:
|
||||
agent-id: "1"
|
||||
agent-name: n
|
||||
agent-desc: d
|
||||
module:
|
||||
ai-api:
|
||||
base-url: "${TEST_LLM_URL}"
|
||||
api-key: k
|
||||
chat-model:
|
||||
model: m
|
||||
agents:
|
||||
- name: bot
|
||||
instruction: hi
|
||||
runner:
|
||||
agent-name: bot
|
||||
`)
|
||||
tables, err := LoadAgentTables(yaml)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://llm.example.com", tables["t"].Module.AiAPI.BaseURL)
|
||||
}
|
||||
|
||||
func TestLoadAgentTables_EmptyTables_ReturnsError(t *testing.T) {
|
||||
yaml := []byte(`
|
||||
ai:
|
||||
agent:
|
||||
config:
|
||||
tables: {}
|
||||
`)
|
||||
_, err := LoadAgentTables(yaml)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "agent config tables are required")
|
||||
}
|
||||
|
||||
func TestLoadAgentTables_InvalidYAML_ReturnsError(t *testing.T) {
|
||||
_, err := LoadAgentTables([]byte("not: [valid: yaml"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadAgentTables_MissingRequiredField_ReturnsError(t *testing.T) {
|
||||
yaml := []byte(`
|
||||
ai:
|
||||
agent:
|
||||
config:
|
||||
tables:
|
||||
t:
|
||||
app-name: ""
|
||||
agent:
|
||||
agent-id: "1"
|
||||
module:
|
||||
ai-api:
|
||||
base-url: u
|
||||
api-key: k
|
||||
chat-model:
|
||||
model: m
|
||||
agents:
|
||||
- name: bot
|
||||
instruction: hi
|
||||
runner:
|
||||
agent-name: bot
|
||||
`)
|
||||
_, err := LoadAgentTables(yaml)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "app-name")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LoadAgentTablesFile 测试
|
||||
// ============================================================
|
||||
|
||||
func TestLoadAgentTablesFile_ValidFile_ReturnsTable(t *testing.T) {
|
||||
tmp, err := os.CreateTemp("", "agent-*.yaml")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
_, err = tmp.Write(newTestYAML())
|
||||
assert.NoError(t, err)
|
||||
tmp.Close()
|
||||
|
||||
tables, err := LoadAgentTablesFile(tmp.Name())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tables, 1)
|
||||
}
|
||||
|
||||
func TestLoadAgentTablesFile_FileNotFound_ReturnsError(t *testing.T) {
|
||||
_, err := LoadAgentTablesFile("/nonexistent/path.yaml")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user