Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
176 lines
9.1 KiB
Go
176 lines
9.1 KiB
Go
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)
|
||
}
|
||
}
|