mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-25 18:03:29 +08:00
## Summary Adds page-range parsing support to the Go-native pipeline path and introduces strict `parse_type` validation for both dataset and document update endpoints. ## What changed ### Pages range parsing - **`internal/utility/pdf_pages.go`** — `NormalizePDFPages`: normalizes raw page ranges (list of `[from,to]` 1-indexed inclusive ranges) into sorted, merged, deduplicated `[][]int`. Invalid ranges are dropped. - **`internal/ingestion/pipeline/pdf_pages.go`** — `NormalizeParserConfigPages`: walks any parser_config map and normalizes `"pages"` values under every component → filetype setup, so the persisted config always carries clean, merged ranges. - **`internal/deepdoc/parser/pdf/parser.go`** — integrates `resolvePagesToProcess` to filter parsed PDF pages by the configured ranges. - Pipeline integration (parser pages): `internal/parser/parser/pdf_parser_common.go`, `chunk_process.go`, plus associated e2e and unit tests. ### Parse type validation (shared logic) - **`internal/service/parser_mode.go`** (new) — `ValidateParseTypeMode`: shared function that validates `parse_type` (1=BuiltIn/parser_id, 2=Pipeline/pipeline_id) and ensures the corresponding field is present. Used by both dataset and document update endpoints. - **`internal/service/dataset/crud.go`** / `update.go` — replaces inline `isPipelineMode`/`isBuiltinMode` computation with the shared `service.ValidateParseTypeMode`. - **`internal/service/document/document_dataset_update.go`** — adds strict `parse_type` validation in `validateDatasetDocumentUpdate`, simplifies the reparse logic to a two-way switch (isBuiltin/isPipeline) now that parse_type is always valid. - **`internal/service/document/document.go`** — adds `ParseType` field to `UpdateDatasetDocumentRequest`. - **`internal/service/document/document_dataset_update.go`** — `updateDocumentParserConfig` fallback path when DSL loading fails. - **`internal/service/parser_mode_test.go`** (new) — test coverage for nil, invalid, and missing-field scenarios. ### Frontend - **`web/src/interfaces/request/document.ts`** — adds `parseType` to `IChangeParserRequestBody`. - **`web/src/hooks/use-document-request.ts`** — `useSetDocumentPipelineParser` sends `parse_type` in the PATCH payload. - **`web/src/pages/dataset/dataset/use-change-document-parser.ts`** — Go/Python branching for the document parser config dialog. - **`web/src/components/document-pipeline-dialog/use-document-pipeline-form.ts`** — `buildSubmitData` returns `parseType` (bugfix: was dropped from the return value). ### Test changes - **Removed**: 2 tests that verified the old "mutually exclusive" error (replaced by `ValidateParseTypeMode` coverage). - **Modified**: 6 tests across document and dataset packages to include `ParseType` in request structs. - **Added**: new e2e tests for pages parsing (`pages_e2e_test.go`, `pdf_parser_pages_e2e_test.go`) and unit tests for `NormalizePDFPages`, `NormalizeParserConfigPages`, `resolvePagesToProcess`. ## Backward compatibility - The `parse_type` field is **required** when `parser_id` or `pipeline_id` is sent. This changes the contract for both dataset and document PATCH endpoints, but aligns the Go backend with the existing frontend behavior (the frontend already sends `parse_type`). Callers that omit `parse_type` when updating parser/pipeline selections will receive a clear error message. - Existing callers that only update fields like `name`, `enabled`, or `meta_fields` are unaffected. - Test updates ensure all known call sites are compliant.
398 lines
11 KiB
Go
398 lines
11 KiB
Go
package dataset
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"ragflow/internal/service"
|
|
)
|
|
|
|
// TestValidateParserID_AcceptsRegistryRefs verifies that every
|
|
// canonical builtin pipeline id passes validation.
|
|
func TestValidateParserID_AcceptsRegistryRefs(t *testing.T) {
|
|
for _, id := range []string{"general", "book", "audio", "qa", "table"} {
|
|
if err := validateParserID(id); err != nil {
|
|
t.Errorf("validateParserID(%q) = %v, want nil", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestValidateParserID_AcceptsNaiveAlias verifies the legacy
|
|
// parser_id "naive" still validates (alias for general).
|
|
func TestValidateParserID_AcceptsNaiveAlias(t *testing.T) {
|
|
if err := validateParserID("naive"); err != nil {
|
|
t.Errorf("validateParserID(naive) = %v, want nil (alias for general)", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateParserID_RejectsUnknown verifies unknown/empty
|
|
// values are rejected with an error that lists the valid options.
|
|
func TestValidateParserID_RejectsUnknown(t *testing.T) {
|
|
for _, id := range []string{"", "unknown", "NAIVE"} {
|
|
err := validateParserID(id)
|
|
if err == nil {
|
|
t.Errorf("validateParserID(%q) = nil, want error", id)
|
|
}
|
|
}
|
|
|
|
err := validateParserID("unknown")
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown parser id")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "general") {
|
|
t.Errorf("error message %q should mention general", msg)
|
|
}
|
|
}
|
|
|
|
// --- validateDatasetAvatar ---
|
|
|
|
func TestValidateDatasetAvatar_MissingPrefix(t *testing.T) {
|
|
err := validateDatasetAvatar("iVBORw0KGgo=")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing MIME prefix")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetAvatar_InvalidPrefix(t *testing.T) {
|
|
err := validateDatasetAvatar("wrong:image/png;base64,iVBORw0KGgo=")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid prefix")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetAvatar_UnsupportedMIME(t *testing.T) {
|
|
err := validateDatasetAvatar("data:image/gif;base64,iVBORw0KGgo=")
|
|
if err == nil {
|
|
t.Fatal("expected error for unsupported MIME")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetAvatar_Valid(t *testing.T) {
|
|
err := validateDatasetAvatar("data:image/png;base64,iVBORw0KGgo=")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
err = validateDatasetAvatar("data:image/jpeg;base64,/9j/4AAQ==")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
// --- validateDatasetEmbeddingModel ---
|
|
|
|
func TestValidateDatasetEmbeddingModel_Empty(t *testing.T) {
|
|
err := validateDatasetEmbeddingModel("")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty model")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetEmbeddingModel_NameOnlyNoProvider(t *testing.T) {
|
|
// A bare model name without @provider (and not a 32-char hex model ID) is
|
|
// rejected, mirroring the Python contract.
|
|
if err := validateDatasetEmbeddingModel("BAAI/bge-large-zh-v1.5"); err == nil {
|
|
t.Fatal("expected error for name without @provider")
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetEmbeddingModel_HexModelID(t *testing.T) {
|
|
if err := validateDatasetEmbeddingModel("aabbccdd11223344aabbccdd11223344"); err != nil {
|
|
t.Fatalf("expected nil for 32-char hex model ID, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetEmbeddingModel_NameWithProvider(t *testing.T) {
|
|
if err := validateDatasetEmbeddingModel("BAAI/bge-large-zh-v1.5@Builtin"); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetEmbeddingModel_EmptyPart(t *testing.T) {
|
|
err := validateDatasetEmbeddingModel("model@")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty provider")
|
|
}
|
|
err = validateDatasetEmbeddingModel("@provider")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty model name")
|
|
}
|
|
}
|
|
|
|
// --- normalizeDatasetPipelineID ---
|
|
|
|
func TestNormalizeDatasetPipelineID_Empty(t *testing.T) {
|
|
result, err := normalizeDatasetPipelineID("")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if result != nil {
|
|
t.Fatalf("expected nil result for empty input")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetPipelineID_Spaces(t *testing.T) {
|
|
result, err := normalizeDatasetPipelineID(" ")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if result != nil {
|
|
t.Fatalf("expected nil result for whitespace-only input")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetPipelineID_WrongLength(t *testing.T) {
|
|
_, err := normalizeDatasetPipelineID("abc123")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong length")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetPipelineID_InvalidChars(t *testing.T) {
|
|
_, err := normalizeDatasetPipelineID("abcdef01-23456789abcdef0123456789")
|
|
if err == nil {
|
|
t.Fatal("expected error for non-hex chars")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetPipelineID_Valid(t *testing.T) {
|
|
result, err := normalizeDatasetPipelineID("ABCDEF0123456789ABCDEF0123456789")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
if *result != "abcdef0123456789abcdef0123456789" {
|
|
t.Errorf("expected lowercased, got %q", *result)
|
|
}
|
|
}
|
|
|
|
// --- validateDatasetParserConfigSize ---
|
|
|
|
func TestValidateDatasetParserConfigSize_Empty(t *testing.T) {
|
|
if err := validateDatasetParserConfigSize(map[string]interface{}{}); err != nil {
|
|
t.Fatalf("expected nil for empty, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetParserConfigSize_UnderLimit(t *testing.T) {
|
|
cfg := map[string]interface{}{
|
|
"Parser:abc": map[string]interface{}{
|
|
"chunk_size": float64(512),
|
|
},
|
|
}
|
|
if err := validateDatasetParserConfigSize(cfg); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateDatasetParserConfigSize_OverLimit(t *testing.T) {
|
|
// Build a config that exceeds 65535 bytes.
|
|
bigVal := strings.Repeat("x", 66000)
|
|
cfg := map[string]interface{}{
|
|
"Parser:abc": map[string]interface{}{
|
|
"big_field": bigVal,
|
|
},
|
|
}
|
|
err := validateDatasetParserConfigSize(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for oversized parser_config")
|
|
}
|
|
if !strings.Contains(err.Error(), "exceeds size limit") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- normalizeDatasetID ---
|
|
|
|
func TestNormalizeDatasetID_Invalid(t *testing.T) {
|
|
_, err := normalizeDatasetID("not-a-uuid")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid UUID")
|
|
}
|
|
_, err = normalizeDatasetID("")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty string")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetID_Valid(t *testing.T) {
|
|
raw := "550e8400-e29b-41d4-a716-446655440000"
|
|
result, err := normalizeDatasetID(raw)
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
expected := "550e8400e29b41d4a716446655440000"
|
|
if result != expected {
|
|
t.Errorf("expected %q, got %q", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetID_StripsHyphens(t *testing.T) {
|
|
// UUID with hyphens already removed.
|
|
raw := "550e8400e29b41d4a716446655440000"
|
|
result, err := normalizeDatasetID(raw)
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if result != raw {
|
|
t.Errorf("expected %q, got %q", raw, result)
|
|
}
|
|
}
|
|
|
|
// --- normalizeDatasetUpdateExt ---
|
|
|
|
func TestNormalizeDatasetUpdateExt_Nil(t *testing.T) {
|
|
if result := normalizeDatasetUpdateExt(nil); result != nil {
|
|
t.Fatalf("expected nil for nil input")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetUpdateExt_PassesThrough(t *testing.T) {
|
|
ext := map[string]interface{}{
|
|
"description": "test",
|
|
"language": "English",
|
|
}
|
|
result := normalizeDatasetUpdateExt(ext)
|
|
if result["description"] != "test" {
|
|
t.Errorf("expected description preserved, got %v", result["description"])
|
|
}
|
|
if result["language"] != "English" {
|
|
t.Errorf("expected language preserved, got %v", result["language"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetUpdateExt_RenamesChunkMethod(t *testing.T) {
|
|
ext := map[string]interface{}{
|
|
"chunk_method": "book",
|
|
}
|
|
result := normalizeDatasetUpdateExt(ext)
|
|
if result["parser_id"] != "book" {
|
|
t.Errorf("expected parser_id=book, got %v", result["parser_id"])
|
|
}
|
|
if _, ok := result["chunk_method"]; ok {
|
|
t.Error("expected chunk_method to be renamed to parser_id")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetUpdateExt_SkipsTokenAndChunkNum(t *testing.T) {
|
|
ext := map[string]interface{}{
|
|
"token_num": float64(1000),
|
|
"chunk_num": float64(50),
|
|
"parser_config": map[string]interface{}{"key": "val"},
|
|
}
|
|
result := normalizeDatasetUpdateExt(ext)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty map, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetUpdateExt_ConvertsPagerank(t *testing.T) {
|
|
ext := map[string]interface{}{
|
|
"pagerank": float64(3),
|
|
}
|
|
result := normalizeDatasetUpdateExt(ext)
|
|
if result["pagerank"] != int64(3) {
|
|
t.Errorf("expected pagerank=int64(3), got %T(%v)", result["pagerank"], result["pagerank"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDatasetUpdateExt_NonFloatPagerankSkipped(t *testing.T) {
|
|
// Non-float64 pagerank values are not convertible and are dropped.
|
|
ext := map[string]interface{}{
|
|
"pagerank": "auto",
|
|
}
|
|
result := normalizeDatasetUpdateExt(ext)
|
|
if _, ok := result["pagerank"]; ok {
|
|
t.Error("expected non-float pagerank to be skipped")
|
|
}
|
|
}
|
|
|
|
// --- normalizeMetadataConfigFields ---
|
|
|
|
func TestNormalizeMetadataConfigFields_EmptyKey(t *testing.T) {
|
|
fields := []service.MetadataConfigField{
|
|
{Key: "", Type: "string"},
|
|
}
|
|
_, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty key")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetadataConfigFields_KeyTooLong(t *testing.T) {
|
|
longKey := strings.Repeat("k", 256)
|
|
fields := []service.MetadataConfigField{
|
|
{Key: longKey, Type: "string"},
|
|
}
|
|
_, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err == nil {
|
|
t.Fatal("expected error for too-long key")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetadataConfigFields_InvalidType(t *testing.T) {
|
|
fields := []service.MetadataConfigField{
|
|
{Key: "my_field", Type: "boolean"},
|
|
}
|
|
_, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid type")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetadataConfigFields_DescriptionTooLong(t *testing.T) {
|
|
longDesc := strings.Repeat("d", 65536)
|
|
fields := []service.MetadataConfigField{
|
|
{Key: "my_field", Type: "string", Description: &longDesc},
|
|
}
|
|
_, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err == nil {
|
|
t.Fatal("expected error for too-long description")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetadataConfigFields_Valid(t *testing.T) {
|
|
desc := "A description"
|
|
fields := []service.MetadataConfigField{
|
|
{Key: "field1", Type: "string", Description: &desc},
|
|
{Key: "field2", Type: "list"},
|
|
}
|
|
result, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected 2 fields, got %d", len(result))
|
|
}
|
|
if result[0]["key"] != "field1" {
|
|
t.Errorf("expected key=field1, got %v", result[0]["key"])
|
|
}
|
|
if result[0]["type"] != "string" {
|
|
t.Errorf("expected type=string, got %v", result[0]["type"])
|
|
}
|
|
if result[0]["description"] != &desc {
|
|
t.Errorf("expected description preserved")
|
|
}
|
|
if result[1]["key"] != "field2" {
|
|
t.Errorf("expected key=field2, got %v", result[1]["key"])
|
|
}
|
|
if result[1]["type"] != "list" {
|
|
t.Errorf("expected type=list, got %v", result[1]["type"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetadataConfigFields_TrimsKey(t *testing.T) {
|
|
fields := []service.MetadataConfigField{
|
|
{Key: " my_field ", Type: "number"},
|
|
}
|
|
result, err := normalizeMetadataConfigFields(fields, "metadata")
|
|
if err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
if result[0]["key"] != "my_field" {
|
|
t.Errorf("expected trimmed key 'my_field', got %v", result[0]["key"])
|
|
}
|
|
}
|