Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
111 lines
4.5 KiB
Go
111 lines
4.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
|
"github.com/Tencent/WeKnora/internal/models/chat"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type agentDraftModels struct {
|
|
interfaces.ModelService
|
|
model *types.Model
|
|
llm *agentDraftChat
|
|
lookups int
|
|
}
|
|
|
|
func (m *agentDraftModels) GetModelByID(context.Context, string) (*types.Model, error) {
|
|
m.lookups++
|
|
return m.model, nil
|
|
}
|
|
|
|
func (m *agentDraftModels) GetChatModel(context.Context, string) (chat.Chat, error) {
|
|
return m.llm, nil
|
|
}
|
|
|
|
type agentDraftChat struct {
|
|
response *types.ChatResponse
|
|
calls int
|
|
ctx context.Context
|
|
messages []chat.Message
|
|
}
|
|
|
|
func (m *agentDraftChat) ChatStream(context.Context, []chat.Message, *chat.ChatOptions) (<-chan types.StreamResponse, error) {
|
|
panic("draft generation must not use streaming")
|
|
}
|
|
|
|
func (m *agentDraftChat) GetModelName() string { return "selected" }
|
|
func (m *agentDraftChat) GetModelID() string { return "selected" }
|
|
|
|
func (m *agentDraftChat) Chat(ctx context.Context, messages []chat.Message, options *chat.ChatOptions) (*types.ChatResponse, error) {
|
|
m.calls++
|
|
m.ctx, m.messages = ctx, messages
|
|
return m.response, nil
|
|
}
|
|
|
|
func TestAgentDraftUsesSelectedModelWithoutPersistingBindings(t *testing.T) {
|
|
llm := &agentDraftChat{response: &types.ChatResponse{Content: `{"name":"Course Planner","description":"Creates connected daily frameworks","role":"training_map","content_tags":["planning"],"id":"injected","system_prompt":"injected","knowledge_bases":["private"],"config":{"allowed_tools":["shell"],"model_id":"other"}}`}}
|
|
models := &agentDraftModels{model: &types.Model{ID: "selected", Type: types.ModelTypeKnowledgeQA, Status: types.ModelStatusActive}, llm: llm}
|
|
service := &customAgentService{models: models}
|
|
draft, err := service.DraftAgent(instructionContext(1, "alice", types.TenantRoleContributor), "selected", "Plan a course")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, llm.calls)
|
|
purpose, correlation := types.LLMCallMetadataFromContext(llm.ctx)
|
|
require.Equal(t, "agent_draft_generation", purpose)
|
|
require.NotEmpty(t, correlation)
|
|
_, deadline := llm.ctx.Deadline()
|
|
require.True(t, deadline)
|
|
require.Equal(t, "Plan a course", llm.messages[1].Content)
|
|
require.Empty(t, draft.ID)
|
|
require.Equal(t, "private", draft.Visibility)
|
|
require.Equal(t, "selected", draft.Config.ModelID)
|
|
require.Equal(t, types.AgentRoleTrainingMap, draft.Config.Role)
|
|
require.Empty(t, draft.Config.SystemPrompt)
|
|
require.Empty(t, draft.Config.KnowledgeBases)
|
|
require.Empty(t, draft.Config.AllowedTools)
|
|
require.Equal(t, "none", draft.Config.KBSelectionMode)
|
|
}
|
|
|
|
func TestAgentDraftRejectsUnauthorizedOrInvalidRequestsBeforeCallingModel(t *testing.T) {
|
|
ctx := instructionContext(1, "alice", types.TenantRoleContributor)
|
|
for _, test := range []struct {
|
|
name string
|
|
ctx context.Context
|
|
requirement string
|
|
model *types.Model
|
|
}{
|
|
{"denied", accesspolicy.WithSnapshot(ctx, &accesspolicy.Snapshot{Restricted: true}), "plan", &types.Model{Type: types.ModelTypeKnowledgeQA, Status: types.ModelStatusActive}},
|
|
{"empty", ctx, " ", nil},
|
|
{"too-long", ctx, strings.Repeat("a", 4001), nil},
|
|
{"missing-model", ctx, "plan", nil},
|
|
{"embedding-model", ctx, "plan", &types.Model{Type: types.ModelTypeEmbedding, Status: types.ModelStatusActive}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
llm := &agentDraftChat{}
|
|
models := &agentDraftModels{model: test.model, llm: llm}
|
|
_, err := (&customAgentService{models: models}).DraftAgent(test.ctx, "selected", test.requirement)
|
|
require.Error(t, err)
|
|
require.Zero(t, llm.calls)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAgentDraftRejectsMalformedAndIncompleteResponses(t *testing.T) {
|
|
for _, content := range []string{"not-json", `{"name":"A","role":"admin"}`, `{"name":"A","content_tags":["same","same"]}`, `{"name":""}`, strings.Repeat("x", 16385)} {
|
|
_, err := parseAgentDraft(content, "selected")
|
|
require.Error(t, err)
|
|
}
|
|
draft, err := parseAgentDraft("```json\n{\"name\":\"Valid\"}\n```", "selected")
|
|
require.NoError(t, err)
|
|
require.Equal(t, types.AgentRoleGeneral, draft.Config.Role)
|
|
llm := &agentDraftChat{response: &types.ChatResponse{Content: `{"name":"A"}`, FinishReason: "length"}}
|
|
models := &agentDraftModels{model: &types.Model{Type: types.ModelTypeKnowledgeQA, Status: types.ModelStatusActive}, llm: llm}
|
|
_, err = (&customAgentService{models: models}).DraftAgent(instructionContext(1, "alice", types.TenantRoleContributor), "selected", "plan")
|
|
require.ErrorContains(t, err, "完整")
|
|
}
|