package rag import ( "context" "errors" "strings" "testing" "agent-desk/internal/ai" "agent-desk/internal/models" "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/i18nx" ) func TestKnowledgeAnswerLocalizedFallbackPaths(t *testing.T) { tests := []struct{ locale, fallback, retry, noAnswerStatus string }{ {i18nx.LocaleEnUS, "The knowledge base currently has no clear information.", "The knowledge base did not contain enough clear information. Try asking a more specific question.", "No answer"}, {i18nx.LocaleRuRU, "В базе знаний пока нет однозначной информации.", "В базе знаний недостаточно однозначной информации. Попробуйте задать более конкретный вопрос.", "Нет ответа"}, {i18nx.LocaleZhCN, "当前知识库暂无明确信息。", "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。", "无答案"}, } for _, tt := range tests { t.Run(tt.locale, func(t *testing.T) { ctx := i18nx.WithLocale(context.Background(), tt.locale) called := false failedChat := func(context.Context, string, string) (*ai.ChatCompletionResult, error) { called = true return nil, errors.New("provider failed") } noHits, err := generateKnowledgeAnswer(ctx, "问题 не переводить", nil, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, failedChat) if err != nil || called || noHits.Status != enums.KnowledgeAnswerStatusNoAnswer || noHits.Content != tt.fallback { t.Fatalf("no-hit result = %#v, %v; model called = %v", noHits, err, called) } if got := getAnswerStatusName(ctx, noHits.Status); got != tt.noAnswerStatus { t.Fatalf("status = %q", got) } hits := []RetrieveResult{{DocumentID: 1, Content: "Reference content"}} failed, err := generateKnowledgeAnswer(ctx, "Q", hits, enums.KnowledgeAnswerModeAssist, enums.AIAgentFallbackModeNoAnswer, failedChat) if err != nil || !called || failed.Status != enums.KnowledgeAnswerStatusFallback || failed.Content != tt.fallback { t.Fatalf("failed model result = %#v, %v", failed, err) } for _, empty := range []*ai.ChatCompletionResult{nil, {}, {Content: " \n\t"}} { got, err := generateKnowledgeAnswer(ctx, "Q", hits, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeSuggestRetry, func(context.Context, string, string) (*ai.ChatCompletionResult, error) { return empty, nil }) if err != nil || got.Status != enums.KnowledgeAnswerStatusFallback || got.Content != tt.retry { t.Fatalf("empty result fallback = %#v, %v", got, err) } } }) } } func TestKnowledgeGenerationPreservesUserAndModelContent(t *testing.T) { for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} { ctx := i18nx.WithLocale(context.Background(), locale) question := " 问题:User's question\n回答:not a wrapper " body := " 回答:body\n相似问:literal user text\n " output := &ai.ChatCompletionResult{Content: " 模型回答\n", ModelName: "test-model", PromptTokens: 4, CompletionTokens: 2} got, err := generateKnowledgeAnswer(ctx, question, []RetrieveResult{{DocumentID: 1, DocumentTitle: "用户标题", Content: body}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(modelCtx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) { if i18nx.LocaleFromContext(modelCtx) != locale { t.Fatal("locale not forwarded to model") } if systemPrompt != i18nx.Getf(locale, "knowledge.rag.system.strict") || !strings.Contains(userPrompt, question) || !strings.Contains(userPrompt, body) { t.Fatalf("incorrect prompts: %q, %q", systemPrompt, userPrompt) } return output, nil }) if err != nil || got.Status != enums.KnowledgeAnswerStatusNormal || got.ChatCompletionResult != *output { t.Fatalf("model output was changed: %#v, %v", got, err) } } } func TestKnowledgeCancellationBeforePersistenceOrProviderCalls(t *testing.T) { ctx, cancel := context.WithCancel(i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU)) cancel() if _, err := Answer.DebugSearch(ctx, request.KnowledgeSearchRequest{Question: "Q"}); !errors.Is(err, context.Canceled) { t.Fatalf("debug search ignored cancellation: %v", err) } if _, err := Answer.DebugAnswer(ctx, request.KnowledgeAnswerRequest{Question: "Q"}, nil); !errors.Is(err, context.Canceled) { t.Fatalf("debug answer ignored cancellation: %v", err) } if err := Index.IndexDocumentByID(ctx, 1); !errors.Is(err, context.Canceled) { t.Fatalf("document build ignored cancellation: %v", err) } if err := Index.IndexFAQByID(ctx, 1); !errors.Is(err, context.Canceled) { t.Fatalf("FAQ build ignored cancellation: %v", err) } if _, err := Retrieve.Retrieve(ctx, RetrieveRequest{Query: "Q"}); !errors.Is(err, context.Canceled) { t.Fatalf("retrieval ignored cancellation: %v", err) } } func TestKnowledgeGenerationCancellationIsNotFallback(t *testing.T) { ctx, cancel := context.WithCancel(i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU)) defer cancel() got, err := generateKnowledgeAnswer(ctx, "Q", []RetrieveResult{{DocumentID: 1, Content: "source"}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(modelCtx context.Context, _, _ string) (*ai.ChatCompletionResult, error) { cancel() return nil, modelCtx.Err() }) if got != nil || !errors.Is(err, context.Canceled) { t.Fatalf("cancelled model should not become a saved fallback: %#v, %v", got, err) } } func TestFAQGeneratedWrappersPreserveSourceContent(t *testing.T) { faq := models.KnowledgeFAQ{ Question: " 问题:What?\n回答:part of question ", SimilarQuestions: `["相似问:literal; text", "", " another\n问题:line "]`, Answer: " 回答:user answer\n相似问:inside answer\n问题:last line ", } original := faq source := sourceFAQContent(faq) for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} { ctx := i18nx.WithLocale(context.Background(), locale) expected := source.render(locale) for _, stored := range []string{source.legacyChinese(), source.render(i18nx.LocaleEnUS), source.render(i18nx.LocaleRuRU), source.render(i18nx.LocaleZhCN)} { got := renderVerifiedFAQChunkContent(ctx, stored, faq) if got != expected || !strings.Contains(got, faq.Question) || !strings.Contains(got, faq.Answer) || !strings.Contains(got, " another\n问题:line ") { t.Fatalf("%s changed source text: %q", locale, got) } } } if faq != original { t.Fatal("source model mutated") } if got := buildFAQChunkContent(faq); got != source.render(i18nx.DefaultLocale) { t.Fatalf("index did not use configured locale: %q", got) } } func TestFAQUnknownAndStaleDerivedContentStaysRecorded(t *testing.T) { faq := models.KnowledgeFAQ{Question: "Current Q", Answer: "Current A"} ctx := i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU) for _, stored := range []string{ "问题:Old Q\n回答:Historical A", "问题:Current Q\n回答:Current A\n", "问题:Current Q\n回答:User-authored custom answer", "相似问:not a generated chunk", "{malformed:derived}", "", } { if got := renderVerifiedFAQChunkContent(ctx, stored, faq); got != stored { t.Fatalf("unverified stored content was rewritten: %q => %q", stored, got) } } } func TestContextWrappersAreLocalizedWithoutChangingSource(t *testing.T) { content := " 原始文档\n回答:literal\n " tests := []struct{ locale, label string }{ {i18nx.LocaleEnUS, "[Document: Document#7 | Section: 用户章节]"}, {i18nx.LocaleRuRU, "[Документ: Документ №7 | Раздел: 用户章节]"}, {i18nx.LocaleZhCN, "【文档:文档#7|章节:用户章节】"}, } for _, tt := range tests { ctx := i18nx.WithLocale(context.Background(), tt.locale) got := Retrieve.BuildContext(ctx, []RetrieveResult{{DocumentID: 7, SectionPath: "用户章节", Content: content}}, 4000) if got != tt.label+"\n"+content+"\n\n" { t.Fatalf("%s unexpected context: %q", tt.locale, got) } } } func TestKnowledgeGenerationDefaultLocaleAndEmptyResponseMetrics(t *testing.T) { ctx := context.Background() noHits, err := generateKnowledgeAnswer(ctx, "Q", nil, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, nil) if err != nil || noHits.Content != i18nx.Get("knowledge.rag.fallback.noAnswer") { t.Fatalf("default-locale caller changed behavior: %#v, %v", noHits, err) } empty := &ai.ChatCompletionResult{Content: " \n", ModelName: "local-test-model", PromptTokens: 12, CompletionTokens: 3} got, err := generateKnowledgeAnswer(ctx, "Q", []RetrieveResult{{DocumentID: 1, Content: "reference"}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(context.Context, string, string) (*ai.ChatCompletionResult, error) { return empty, nil }) if err != nil || got.Status != enums.KnowledgeAnswerStatusFallback || got.Content != noHits.Content || got.ModelName != empty.ModelName || got.PromptTokens != empty.PromptTokens || got.CompletionTokens != empty.CompletionTokens { t.Fatalf("empty completion must retain usage metrics: %#v, %v", got, err) } }