Files
xinmi/internal/workbench/generation_budget_test.go
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

174 lines
7.0 KiB
Go

package workbench
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/Tencent/WeKnora/internal/agent/token"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
func TestCompletionRespectsAgentAndModelOutputLimits(t *testing.T) {
for _, tc := range []struct {
name string
agent, model, want int
}{
{"agent lower", 512, 8192, 512},
{"model lower", 8192, 1024, 1024},
{"default", 0, 0, types.DefaultQuickAnswerMaxCompletionTokens},
} {
t.Run(tc.name, func(t *testing.T) {
s, ctx, _, model := generationHarness(t)
s.agents = generationAgentStub{config: types.CustomAgentConfig{MaxCompletionTokens: tc.agent}}
s.models = generationModelStub{model: model, parameters: types.ModelParameters{ContextWindow: 16000, MaxOutputTokens: tc.model}}
if _, err := s.complete(ctx, "agent", "system", "prompt"); err != nil {
t.Fatal(err)
}
if model.options.MaxCompletionTokens != tc.want {
t.Fatalf("output limit = %d, want %d", model.options.MaxCompletionTokens, tc.want)
}
})
}
}
func TestOversizedAcceptedContinuityFailsBeforeChatAndPreservesResource(t *testing.T) {
for _, action := range []string{"generate", "review"} {
t.Run(action, func(t *testing.T) {
s, ctx, job, model := generationHarness(t)
s.models = generationModelStub{model: model, parameters: types.ModelParameters{ContextWindow: 4096, MaxOutputTokens: 512}}
job.Action = action
d := TaskData{AgentID: "agent", Segments: []Segment{
{ID: "a", Title: "基础", Status: "accepted", Content: strings.Repeat("已确认的中文正文需要完整保留。", 1500)},
{ID: "b", Title: "进阶", AgentID: "agent", Status: "accepted", Content: "后文"},
}}
accepted, _ := acceptedManuscript(d)
d.Quality = &QualityReport{Passed: true, Summary: "通过", ManuscriptHash: manuscriptHash(accepted)}
r := &Resource{Data: asJSON(d)}
original := compact(r.Data)
err := s.executeTask(ctx, job, r, ActionRequest{SegmentID: "b"})
if !errors.Is(err, ErrInvalid) || !strings.Contains(err.Error(), "上下文容量不足") {
t.Fatalf("expected actionable context error, got %v", err)
}
if len(model.messages) != 0 || compact(r.Data) != original {
t.Fatal("oversized prompt reached model or changed accepted content")
}
})
}
}
type budgetSearchStub struct {
interfaces.SessionService
rows []*types.SearchResult
calls int
}
func (s *budgetSearchStub) SearchKnowledge(context.Context, []string, []string, []types.TagScope, string) ([]*types.SearchResult, error) {
s.calls++
return s.rows, nil
}
func TestEvidenceBudgetIncludesSourceLabelsAndKeepsRelevanceOrder(t *testing.T) {
s := NewService(nil)
s.sessions = &budgetSearchStub{rows: []*types.SearchResult{
nil,
{ID: "empty", Content: " "},
{ID: "first", KnowledgeFilename: "中文文件名", Content: "最高相关的事实。"},
{ID: "first", Content: "重复内容不可引用"},
{ID: "second", KnowledgeFilename: "另一份资料", Content: strings.Repeat("第二相关片段包含很多汉字。", 1000)},
{ID: "third", Content: "不应挤掉更相关片段"},
}}
const budget = 250
evidence, ids, err := s.evidence(context.Background(), []string{"kb"}, nil, "query", budget)
if err != nil {
t.Fatal(err)
}
estimator, _ := token.NewEstimator()
if n := estimator.EstimateString(evidence); n > budget {
t.Fatalf("evidence uses %d tokens, limit %d", n, budget)
}
if !reflect.DeepEqual(ids, []string{"first", "second"}) || !strings.Contains(evidence, "仅引用部分内容") || strings.Contains(evidence, "重复内容不可引用") {
t.Fatalf("source selection = %v, text = %s", ids, evidence)
}
}
func TestEvidenceFitsRemainingContextWithoutTruncatingAcceptedText(t *testing.T) {
s, ctx, _, model := generationHarness(t)
s.agents = generationAgentStub{config: types.CustomAgentConfig{MaxCompletionTokens: 512, SystemPrompt: strings.Repeat("表达风格", 30)}}
s.models = generationModelStub{model: model, parameters: types.ModelParameters{ContextWindow: 4096}}
s.sessions = &budgetSearchStub{rows: []*types.SearchResult{{ID: "chunk", Content: strings.Repeat("资料的正文内容。", 5000)}}}
prompt := "已采用正文:" + strings.Repeat("前序完整内容。", 160)
if _, _, err := s.completeWithEvidence(ctx, "agent", "system", prompt, []string{"kb"}, nil, "query"); err != nil {
t.Fatal(err)
}
if !strings.Contains(model.messages[1].Content, prompt) {
t.Fatal("accepted continuity was truncated")
}
estimator, _ := token.NewEstimator()
if n := estimator.EstimateMessages(model.messages); n+512 > 4096-4096*15/100 {
t.Fatalf("full request exceeded context safety budget: %d", n)
}
}
func TestEvidenceRefusesFullContextBeforeRetrieval(t *testing.T) {
s := NewService(nil)
search := &budgetSearchStub{}
s.sessions = search
if _, _, err := s.evidence(context.Background(), []string{"kb"}, nil, "query", 0); !errors.Is(err, ErrInvalid) || search.calls != 0 {
t.Fatalf("full context should fail before retrieval: %v, calls=%d", err, search.calls)
}
}
func TestCompletionRecordsProviderUsageOnOwnedJob(t *testing.T) {
s, ctx, job, model := generationHarness(t)
model.usage = types.TokenUsage{PromptTokens: 80, CompletionTokens: 20, TotalTokens: 100}
if _, err := s.complete(ctx, "agent", "system", "prompt"); err != nil {
t.Fatal(err)
}
var saved Job
if err := s.db.First(&saved, "id = ?", job.ID).Error; err != nil {
t.Fatal(err)
}
metadata, err := decode[struct {
AgentID string `json:"agent_id"`
ModelID string `json:"model_id"`
Estimated int `json:"estimated_input_tokens"`
Reported bool `json:"provider_usage_reported"`
Usage types.TokenUsage `json:"usage"`
}](saved.Generation)
if err != nil || metadata.AgentID != "agent" || metadata.ModelID != "model" || metadata.Estimated <= 0 || !metadata.Reported || metadata.Usage.TotalTokens != 100 {
t.Fatalf("usage metadata missing: %+v, %v", saved.Generation, err)
}
}
func TestCompletionRequiresValidOwnedLeaseBeforeCallingModel(t *testing.T) {
for _, column := range []string{"owner_id", "lease_until"} {
t.Run(column, func(t *testing.T) {
s, ctx, job, model := generationHarness(t)
var value any = "someone-else"
if column == "lease_until" {
value = time.Now().Add(-time.Minute)
}
if err := s.db.Model(&Job{}).Where("id = ?", job.ID).Update(column, value).Error; err != nil {
t.Fatal(err)
}
if _, err := s.complete(ctx, "agent", "system", "prompt"); !errors.Is(err, ErrConflict) || len(model.messages) > 0 {
t.Fatalf("invalid lease reached provider: %v", err)
}
})
}
}
func TestProviderContextOverflowProducesActionableErrorWithoutRawDetails(t *testing.T) {
s, ctx, _, model := generationHarness(t)
model.err = errors.New("context_length_exceeded provider-secret-marker")
_, err := s.complete(ctx, "agent", "system", "prompt")
if !errors.Is(err, ErrInvalid) || !strings.Contains(publicJobError(err), "上下文") || strings.Contains(publicJobError(err), "provider-secret-marker") {
t.Fatalf("unexpected public error: %v", err)
}
}