From c540cf3fafa8cda30015ee601af3dcb005a1b852 Mon Sep 17 00:00:00 2001 From: kfjie Date: Wed, 28 Oct 2026 10:47:07 +0000 Subject: [PATCH 1/2] fix: omit failure messages for files without processing errors --- .../application/processing/service.go | 11 ++- .../processing/service_error_message_test.go | 69 +++++++++++++++++++ .../http/conversation/dto_response.go | 4 +- .../dto_response_file_error_test.go | 48 +++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 backend/internal/application/processing/service_error_message_test.go create mode 100644 backend/internal/transport/http/conversation/dto_response_file_error_test.go diff --git a/backend/internal/application/processing/service.go b/backend/internal/application/processing/service.go index ae957d587..19a37737c 100644 --- a/backend/internal/application/processing/service.go +++ b/backend/internal/application/processing/service.go @@ -681,7 +681,7 @@ func fileProcessingStatusFromFileObject(fileObj *domainconversation.FileObject) CompletedAt: fileObj.ProcessingCompletedAt, UpdatedAt: fileObj.UpdatedAt, } - dto.ErrorMessage = HumanizeFileProcessingError(dto.FileCategory, dto.ErrorCode, dto.ErrorMessage) + dto.ErrorMessage = ResolveFileProcessingErrorMessage(dto.FileCategory, dto.ProcessingStatus, dto.ErrorCode, dto.ErrorMessage) return dto } @@ -1438,6 +1438,15 @@ func processingErrorSummary(err error) string { return HumanizeFileProcessingError("", code, "") } +// ResolveFileProcessingErrorMessage formats response errors without inventing a failure +// for files that have no processing error. Legacy failed rows still get a safe fallback. +func ResolveFileProcessingErrorMessage(fileCategory, processingStatus, code, message string) string { + if processingStatus != "failed" && strings.TrimSpace(code) == "" && strings.TrimSpace(message) == "" { + return "" + } + return HumanizeFileProcessingError(fileCategory, code, message) +} + func HumanizeFileProcessingError(fileCategory string, code string, _ string) string { normalizedCode := strings.ToLower(strings.TrimSpace(code)) if normalizedCode == "" { diff --git a/backend/internal/application/processing/service_error_message_test.go b/backend/internal/application/processing/service_error_message_test.go new file mode 100644 index 000000000..a4fa9d863 --- /dev/null +++ b/backend/internal/application/processing/service_error_message_test.go @@ -0,0 +1,69 @@ +package processing + +import ( + "testing" + + model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation" +) + +func TestFileProcessingStatusDoesNotInventFailureMessages(t *testing.T) { + for _, category := range []string{"pdf", "document", "image"} { + for _, status := range []string{"uploaded", "queued", "extracting", "embedding", "ready"} { + t.Run(category+"/"+status, func(t *testing.T) { + file := &model.FileObject{ + FileCategory: category, ProcessingStatus: status, + ProcessingReady: status == "ready", ExtractStatus: "ready", EmbedStatus: "none", + ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1, + } + got := fileProcessingStatusFromFileObject(file) + if got.ErrorCode != "" || got.ErrorMessage != "" { + t.Fatalf("file without an error acquired a failure: code=%q message=%q", got.ErrorCode, got.ErrorMessage) + } + if file.ProcessingErrorMessage != "" { + t.Fatal("mapping must not modify the stored file") + } + }) + } + } +} + +func TestFileProcessingStatusEmptyAndEmbeddingFailureStaySeparate(t *testing.T) { + for _, file := range []model.FileObject{ + {FileCategory: "pdf", ProcessingStatus: "ready", ExtractStatus: "empty", EmbedStatus: "empty"}, + {FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ExtractStatus: "ready", EmbedStatus: "failed", EmbedError: "embedding unavailable"}, + {FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ProcessingErrorCode: " \t", ProcessingErrorMessage: " \n"}, + } { + got := fileProcessingStatusFromFileObject(&file) + if got.ErrorMessage != "" { + t.Fatalf("non-processing failure acquired a processing error: %q", got.ErrorMessage) + } + if got.EmbedError != file.EmbedError { + t.Fatalf("embedding error changed: %q", got.EmbedError) + } + } +} + +func TestFileProcessingStatusKeepsSafeFailureMessages(t *testing.T) { + for _, tc := range []struct { + name, status, code, message, want string + }{ + {"coded failure", "failed", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + {"legacy failure", "failed", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + {"message only", "ready", "", "private upstream diagnostic", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + {"unknown code", "failed", "unknown_error", "private upstream diagnostic", "文件处理失败,请稍后重试。"}, + {"code without failed status", "ready", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + } { + t.Run(tc.name, func(t *testing.T) { + got := fileProcessingStatusFromFileObject(&model.FileObject{ + FileCategory: "pdf", ProcessingStatus: tc.status, + ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message, + }) + if got.ErrorMessage != tc.want { + t.Fatalf("message = %q, want %q", got.ErrorMessage, tc.want) + } + if got.ErrorCode != tc.code { + t.Fatalf("error code changed: %q", got.ErrorCode) + } + }) + } +} diff --git a/backend/internal/transport/http/conversation/dto_response.go b/backend/internal/transport/http/conversation/dto_response.go index 2ebc3a5f4..f058b36a0 100644 --- a/backend/internal/transport/http/conversation/dto_response.go +++ b/backend/internal/transport/http/conversation/dto_response.go @@ -485,7 +485,7 @@ func toFileObjectResponse(item *model.FileObject, capability appembedding.FileVe ProcessingStatus: item.ProcessingStatus, ProcessingReady: item.ProcessingReady, ProcessingErrorCode: item.ProcessingErrorCode, - ProcessingErrorMessage: appprocessing.HumanizeFileProcessingError(item.FileCategory, item.ProcessingErrorCode, item.ProcessingErrorMessage), + ProcessingErrorMessage: appprocessing.ResolveFileProcessingErrorMessage(item.FileCategory, item.ProcessingStatus, item.ProcessingErrorCode, item.ProcessingErrorMessage), ExtractStatus: item.ExtractStatus, EmbedStatus: item.EmbedStatus, EmbedError: item.EmbedError, @@ -1478,7 +1478,7 @@ func toFileProcessingStatusResponse(d *appprocessing.FileProcessingStatusDTO) Fi RAGReady: d.RAGReady, RAGReason: d.RAGReason, ErrorCode: d.ErrorCode, - ErrorMessage: appprocessing.HumanizeFileProcessingError(d.FileCategory, d.ErrorCode, d.ErrorMessage), + ErrorMessage: appprocessing.ResolveFileProcessingErrorMessage(d.FileCategory, d.ProcessingStatus, d.ErrorCode, d.ErrorMessage), ExtractChars: d.ExtractChars, ExtractPages: d.ExtractPages, ChunkCount: d.ChunkCount, diff --git a/backend/internal/transport/http/conversation/dto_response_file_error_test.go b/backend/internal/transport/http/conversation/dto_response_file_error_test.go new file mode 100644 index 000000000..fa7deb1fa --- /dev/null +++ b/backend/internal/transport/http/conversation/dto_response_file_error_test.go @@ -0,0 +1,48 @@ +package conversation + +import ( + "testing" + + appembedding "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/embedding" + appprocessing "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/processing" + model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation" +) + +func TestFileResponsesDoNotInventFailureMessages(t *testing.T) { + for _, tc := range []struct { + name, status, extract, embed, code, message, want string + }{ + {"successful MinerU PDF", "ready", "ready", "none", "", "", ""}, + {"queued PDF", "queued", "none", "none", "", "", ""}, + {"extracting PDF", "extracting", "none", "none", "", "", ""}, + {"empty PDF", "ready", "empty", "empty", "", "", ""}, + {"embedding failed separately", "ready", "ready", "failed", "", "", ""}, + {"whitespace error fields", "ready", "ready", "none", " \t", " \n", ""}, + {"actual failure", "failed", "failed", "none", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, + {"legacy failure without fields", "failed", "failed", "none", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + } { + t.Run(tc.name, func(t *testing.T) { + file := &model.FileObject{ + FileID: "synthetic-pdf", FileCategory: "pdf", MimeType: "application/pdf", + ProcessingStatus: tc.status, ProcessingReady: tc.extract == "ready", + ExtractStatus: tc.extract, ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1, + EmbedStatus: tc.embed, ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message, + } + object := toFileObjectResponse(file, appembedding.FileVectorizationCapability{}) + if object.ProcessingErrorMessage != tc.want { + t.Errorf("file response message = %q, want %q", object.ProcessingErrorMessage, tc.want) + } + status := toFileProcessingStatusResponse(&appprocessing.FileProcessingStatusDTO{ + FileID: file.FileID, FileCategory: file.FileCategory, ProcessingStatus: tc.status, + ProcessingReady: file.ProcessingReady, ExtractStatus: tc.extract, EmbedStatus: tc.embed, + ErrorCode: tc.code, ErrorMessage: tc.message, ExtractChars: 638, ExtractPages: 1, + }) + if status.ErrorMessage != tc.want { + t.Errorf("processing response message = %q, want %q", status.ErrorMessage, tc.want) + } + if object.ProcessingStatus != tc.status || status.ProcessingStatus != tc.status { + t.Fatal("mapping changed processing status") + } + }) + } +} From de08c7ac246024d5fcbe208f737fdaf60aeb574e Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:10:12 +0800 Subject: [PATCH 2/2] refactor: improve handling of processing error messages and update related tests --- backend/internal/application/processing/service.go | 4 ++-- .../application/processing/service_error_message_test.go | 6 ++---- .../internal/transport/http/conversation/dto_response.go | 2 +- .../http/conversation/dto_response_file_error_test.go | 5 +++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/internal/application/processing/service.go b/backend/internal/application/processing/service.go index 19a37737c..26c0c1431 100644 --- a/backend/internal/application/processing/service.go +++ b/backend/internal/application/processing/service.go @@ -1439,9 +1439,9 @@ func processingErrorSummary(err error) string { } // ResolveFileProcessingErrorMessage formats response errors without inventing a failure -// for files that have no processing error. Legacy failed rows still get a safe fallback. +// for files that have no processing error code. Legacy failed rows still get a safe fallback. func ResolveFileProcessingErrorMessage(fileCategory, processingStatus, code, message string) string { - if processingStatus != "failed" && strings.TrimSpace(code) == "" && strings.TrimSpace(message) == "" { + if !strings.EqualFold(strings.TrimSpace(processingStatus), "failed") && strings.TrimSpace(code) == "" { return "" } return HumanizeFileProcessingError(fileCategory, code, message) diff --git a/backend/internal/application/processing/service_error_message_test.go b/backend/internal/application/processing/service_error_message_test.go index a4fa9d863..568933d95 100644 --- a/backend/internal/application/processing/service_error_message_test.go +++ b/backend/internal/application/processing/service_error_message_test.go @@ -19,9 +19,6 @@ func TestFileProcessingStatusDoesNotInventFailureMessages(t *testing.T) { if got.ErrorCode != "" || got.ErrorMessage != "" { t.Fatalf("file without an error acquired a failure: code=%q message=%q", got.ErrorCode, got.ErrorMessage) } - if file.ProcessingErrorMessage != "" { - t.Fatal("mapping must not modify the stored file") - } }) } } @@ -49,7 +46,8 @@ func TestFileProcessingStatusKeepsSafeFailureMessages(t *testing.T) { }{ {"coded failure", "failed", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, {"legacy failure", "failed", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, - {"message only", "ready", "", "private upstream diagnostic", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, + {"stale message without code", "ready", "", "private upstream diagnostic", ""}, + {"padded failed status", " FAILED ", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, {"unknown code", "failed", "unknown_error", "private upstream diagnostic", "文件处理失败,请稍后重试。"}, {"code without failed status", "ready", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, } { diff --git a/backend/internal/transport/http/conversation/dto_response.go b/backend/internal/transport/http/conversation/dto_response.go index f058b36a0..1f50d5087 100644 --- a/backend/internal/transport/http/conversation/dto_response.go +++ b/backend/internal/transport/http/conversation/dto_response.go @@ -1478,7 +1478,7 @@ func toFileProcessingStatusResponse(d *appprocessing.FileProcessingStatusDTO) Fi RAGReady: d.RAGReady, RAGReason: d.RAGReason, ErrorCode: d.ErrorCode, - ErrorMessage: appprocessing.ResolveFileProcessingErrorMessage(d.FileCategory, d.ProcessingStatus, d.ErrorCode, d.ErrorMessage), + ErrorMessage: d.ErrorMessage, ExtractChars: d.ExtractChars, ExtractPages: d.ExtractPages, ChunkCount: d.ChunkCount, diff --git a/backend/internal/transport/http/conversation/dto_response_file_error_test.go b/backend/internal/transport/http/conversation/dto_response_file_error_test.go index fa7deb1fa..cf7722c5a 100644 --- a/backend/internal/transport/http/conversation/dto_response_file_error_test.go +++ b/backend/internal/transport/http/conversation/dto_response_file_error_test.go @@ -16,8 +16,9 @@ func TestFileResponsesDoNotInventFailureMessages(t *testing.T) { {"queued PDF", "queued", "none", "none", "", "", ""}, {"extracting PDF", "extracting", "none", "none", "", "", ""}, {"empty PDF", "ready", "empty", "empty", "", "", ""}, - {"embedding failed separately", "ready", "ready", "failed", "", "", ""}, + {"stalled embedding marked failed", "ready", "ready", "failed", "embed_failed", "embedding stalled", "文件已完成文本提取,但向量化失败。"}, {"whitespace error fields", "ready", "ready", "none", " \t", " \n", ""}, + {"stale message without code", "ready", "ready", "none", "", "old diagnostic", ""}, {"actual failure", "failed", "failed", "none", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"}, {"legacy failure without fields", "failed", "failed", "none", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"}, } { @@ -35,7 +36,7 @@ func TestFileResponsesDoNotInventFailureMessages(t *testing.T) { status := toFileProcessingStatusResponse(&appprocessing.FileProcessingStatusDTO{ FileID: file.FileID, FileCategory: file.FileCategory, ProcessingStatus: tc.status, ProcessingReady: file.ProcessingReady, ExtractStatus: tc.extract, EmbedStatus: tc.embed, - ErrorCode: tc.code, ErrorMessage: tc.message, ExtractChars: 638, ExtractPages: 1, + ErrorCode: tc.code, ErrorMessage: object.ProcessingErrorMessage, ExtractChars: 638, ExtractPages: 1, }) if status.ErrorMessage != tc.want { t.Errorf("processing response message = %q, want %q", status.ErrorMessage, tc.want)