feat: harden support, localize knowledge and add operator availability [skip ci]

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.
This commit is contained in:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -0,0 +1,175 @@
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)
}
}