Files

188 lines
8.5 KiB
Go
Raw Permalink Normal View History

package workbench
import (
"context"
"strings"
"testing"
"github.com/Tencent/WeKnora/internal/agent/token"
"github.com/Tencent/WeKnora/internal/types"
)
type readingStub struct {
budgetSearchStub
overviewCalls int
}
func (s *readingStub) KnowledgeReadingOverview(context.Context, []string, []string, []types.TagScope) (*types.KnowledgeReadingOverview, error) {
s.overviewCalls++
return &types.KnowledgeReadingOverview{DocumentCount: 3, SummaryCount: 2, Fingerprint: "same", Entries: []types.KnowledgeReadingEntry{
{Title: "第一份", Summary: "已有摘要"},
{Title: "第二份", Summary: strings.Repeat("无法完整容纳的摘要", 2000)},
{Title: "第三份"},
}}, nil
}
func TestLayeredReadingCountsActualIncludedSummaries(t *testing.T) {
s := NewService(nil)
stub := &readingStub{}
s.sessions = stub
ctx, report := withReading(context.Background(), "full", &ReadingReport{Fingerprint: "same"})
text, err := s.readingOverview(ctx, []string{"kb"}, nil, nil, 900)
if err != nil {
t.Fatal(err)
}
estimator, _ := token.NewEstimator()
if report.Documents != 3 || report.AvailableSummaries != 2 || report.IncludedSummaries != 1 || report.IncludedDocuments != 3 || !report.ScopeUnchanged || estimator.EstimateString(text) > 300 {
t.Fatalf("incorrect coverage: %+v", report)
}
quick, _ := withReading(context.Background(), "quick", report)
if text, err = s.readingOverview(quick, []string{"kb"}, nil, nil, 900); err != nil || text != "" || stub.overviewCalls != 1 {
t.Fatal("quick mode unexpectedly reads summaries")
}
}
func TestAnchorMustMatchExactAcceptedSourceAndFingerprint(t *testing.T) {
seg := Segment{ID: "a", Title: "基础", Content: "前文定义是准确引用,不可改写成另一种结论。"}
anchor := ContinuityAnchor{SourceHash: segmentHash(seg), Quotes: []string{"前文定义是准确引用"}}
if err := validateAnchor(anchor, seg); err != nil {
t.Fatal(err)
}
anchor.Quotes = []string{"前文定义不准确"}
if validateAnchor(anchor, seg) == nil {
t.Fatal("invented anchor accepted")
}
anchor.Quotes = []string{"前文定义是准确引用"}
seg.Content += "正文已变更。"
if validateAnchor(anchor, seg) == nil {
t.Fatal("stale anchor accepted")
}
}
func TestLongContinuityReusesAnchorsAndOmitsSkippedText(t *testing.T) {
s, ctx, _, model := generationHarness(t)
d := TaskData{ContinuityAnchors: map[string]ContinuityAnchor{}, Segments: []Segment{
{ID: "a", Title: "开头", Status: "accepted", Content: strings.Repeat("已讲授定义。", 2500)},
{ID: "skip", Title: "废弃", Status: "skipped", Content: "禁止把此内容带入后文"},
{ID: "b", Title: "中间", Status: "accepted", Content: "最近完整正文甲"},
{ID: "c", Title: "承接", Status: "accepted", Content: "最近完整正文乙"},
{ID: "d", Title: "本段", Status: "pending"},
}}
d.ContinuityAnchors["a"] = ContinuityAnchor{SourceHash: segmentHash(d.Segments[0]), Quotes: []string{"已讲授定义。"}}
text, err := s.continuityContext(ctx, &d, 4, "agent", false)
if err != nil {
t.Fatal(err)
}
if len(model.messages) != 0 || strings.Contains(text, "禁止把此内容") || !strings.Contains(text, "最近完整正文甲") || !strings.Contains(text, "最近完整正文乙") || !strings.Contains(text, "已讲授定义") || !strings.Contains(text, "最后一个未跳过") {
t.Fatal("anchor reuse or accepted context violated")
}
}
func TestSegmentPositionExcludesSkippedOutlinesAndClosesFinalSegment(t *testing.T) {
d := TaskData{Segments: []Segment{{Title: "当前"}, {Title: "废弃主题", Status: "skipped"}, {Title: "预算落地", Status: "pending"}}}
first := segmentPosition(d, 0)
if strings.Contains(first, "废弃主题") || !strings.Contains(first, "预算落地") || strings.Contains(first, "最后一个未跳过") {
t.Fatal("position included a skipped topic or closed too early")
}
d.Segments[2].Status = "skipped"
if !strings.Contains(segmentPosition(d, 0), "最后一个未跳过") {
t.Fatal("last retained segment lacks closure instructions")
}
}
func TestManualCategorySelectionUsesAssignedAgentScope(t *testing.T) {
options := []SegmentCategory{{ID: "a"}, {ID: "b"}}
d := TaskData{CategoryOptions: options, AgentCategoryOptions: map[string][]SegmentCategory{"writer": {options[0]}, "editor": {options[1]}}, Segments: []Segment{{ID: "s1", AgentID: "writer", CategoryID: "a", Status: "accepted", Content: "正文"}}}
r := &Resource{Kind: "content-tasks", Data: asJSON(d)}
d.Segments[0].AgentID = "editor"
if applyManualUpdate(r, resourceRequest{Name: "任务", Data: asJSON(d)}) == nil {
t.Fatal("agent change retained a category outside its scope")
}
d.Segments[0].CategoryID = "b"
if err := applyManualUpdate(r, resourceRequest{Name: "任务", Data: asJSON(d)}); err != nil {
t.Fatal(err)
}
updated, _ := decode[TaskData](r.Data)
if updated.Segments[0].Status != "needs_review" {
t.Fatal("agent and category change did not invalidate approval")
}
updated.AgentCategoryOptions["editor"] = options
if applyManualUpdate(r, resourceRequest{Name: "任务", Data: asJSON(updated)}) == nil {
t.Fatal("client widened category scope")
}
}
func TestCategoryRecommendationAndManualUpdateProtection(t *testing.T) {
options := []SegmentCategory{{ID: "cat", KnowledgeBaseID: "kb", Name: "课程/基础"}}
segments := []Segment{{ID: "a", AgentID: "agent", Status: "accepted", Content: "正文", RecommendedCategoryID: "cat", CategoryReason: "适合本段"}}
if err := normalizeCategoryRecommendations(segments, options); err != nil || segments[0].CategoryID != "cat" {
t.Fatal(err)
}
d := TaskData{CategoryOptions: options, Segments: segments}
r := &Resource{Kind: "content-tasks", Data: asJSON(d), Status: "completed"}
d.Segments[0].CategoryID = ""
if err := applyManualUpdate(r, resourceRequest{Name: "任务", Data: asJSON(d)}); err != nil {
t.Fatal(err)
}
got, _ := decode[TaskData](r.Data)
if got.Segments[0].Status != "needs_review" {
t.Fatal("category change kept stale approval")
}
got.CategoryOptions = append(got.CategoryOptions, SegmentCategory{ID: "foreign"})
if applyManualUpdate(r, resourceRequest{Name: "任务", Data: asJSON(got)}) == nil {
t.Fatal("client injected category")
}
segments[0].RecommendedCategoryID = "foreign"
if normalizeCategoryRecommendations(segments, options) == nil {
t.Fatal("unknown AI category accepted")
}
}
func TestRecommendationsCannotBorrowAnotherAgentsCategory(t *testing.T) {
options := map[string][]SegmentCategory{
"writer": {{ID: "writing", KnowledgeBaseID: "kb-writing", Name: "写作"}},
"editor": {{ID: "editing", KnowledgeBaseID: "kb-editing", Name: "编辑"}},
}
segments := []Segment{{AgentID: "writer", RecommendedCategoryID: "editing"}}
if validateAgentCategoryRecommendations(segments, options) == nil {
t.Fatal("recommendation expanded the assigned Agent's knowledge scope")
}
segments[0].RecommendedCategoryID = "writing"
if err := validateAgentCategoryRecommendations(segments, options); err != nil {
t.Fatal(err)
}
if normalizeCategoryRecommendations(segments, options["writer"]) == nil {
t.Fatal("category recommendation accepted without a reason")
}
segments[0].CategoryReason = "本段讲解写作结构,与该分类匹配。"
if err := normalizeCategoryRecommendations(segments, options["writer"]); err != nil {
t.Fatal(err)
}
}
func TestDecompositionDiscardsOnlyUnusableRecommendations(t *testing.T) {
options := []SegmentCategory{{ID: "writing"}, {ID: "editing"}}
byAgent := map[string][]SegmentCategory{"writer": {options[0]}, "editor": {options[1]}}
segments := []Segment{
{ID: "valid", AgentID: "writer", RecommendedCategoryID: "writing", CategoryReason: "Matches writing"},
{ID: "other-agent", AgentID: "writer", RecommendedCategoryID: "editing", CategoryReason: "Wrong scope", CategoryID: "editing"},
{ID: "invented", AgentID: "editor", RecommendedCategoryID: "unknown", CategoryReason: "Invented"},
{ID: "missing-reason", AgentID: "writer", RecommendedCategoryID: "writing"},
{ID: "none", AgentID: "writer"},
}
if err := reconcileCategoryRecommendations(segments, options, byAgent); err != nil {
t.Fatal(err)
}
if segments[0].CategoryID != "writing" || segments[0].CategoryReason != "Matches writing" {
t.Fatal("valid recommendation was changed")
}
for _, segment := range segments[1:] {
if segment.RecommendedCategoryID != "" || segment.CategoryID != "" || !strings.Contains(segment.CategoryReason, "Agent") {
t.Fatalf("invalid recommendation retained or fallback not explained: %s", segment.ID)
}
}
if err := validateAgentCategoryRecommendations(segments, byAgent); err != nil {
t.Fatal(err)
}
}