Ctrl K
ComunidadGuías…

internal/ingestion/pipeline/pipeline_failure_message_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package pipeline

import (
	"context"
	"errors"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"strings"
	"testing"
)

// TestFormatComponentFailure covers the operator-facing failure formatting:
// per-component timeout guidance, prefix stripping, and the bare fallbacks.
// These messages are what an operator reads when a document fails, so the
// component identity in them must keep matching the registered names.
func TestFormatComponentFailure(t *testing.T) {
	t.Run("nil error reports the component only", func(t *testing.T) {
		if got := formatComponentFailure("Extractor", nil); got != "Extractor Error" {
			t.Fatalf("formatComponentFailure(nil) = %q, want %q", got, "Extractor Error")
		}
	})

	t.Run("extractor timeout gets extraction guidance", func(t *testing.T) {
		got := formatComponentFailure("Extractor", context.DeadlineExceeded)
		want := "Extractor: Document extraction timed out while waiting for the configured LLM. Check that the model service is reachable, then retry."
		if got != want {
			t.Fatalf("formatComponentFailure = %q, want %q", got, want)
		}
	})

	t.Run("compiler timeout gets compilation guidance", func(t *testing.T) {
		got := formatComponentFailure("Compiler", context.DeadlineExceeded)
		want := "Compiler: Document compilation timed out. Check that the configured model service is reachable and responsive, then retry."
		if got != want {
			t.Fatalf("formatComponentFailure = %q, want %q", got, want)
		}
	})

	t.Run("other components fall through to the generic form", func(t *testing.T) {
		got := formatComponentFailure("Tokenizer", context.DeadlineExceeded)
		want := "Tokenizer: context deadline exceeded"
		if got != want {
			t.Fatalf("formatComponentFailure = %q, want %q", got, want)
		}
	})

	t.Run("wrapped deadline is detected", func(t *testing.T) {
		err := fmt.Errorf("component failed: %w", context.DeadlineExceeded)
		got := formatComponentFailure("Extractor", err)
		if !strings.Contains(got, "timed out") {
			t.Fatalf("formatComponentFailure(wrapped) = %q, want the timeout guidance", got)
		}
	})

	t.Run("timeout wording inside the message is detected", func(t *testing.T) {
		err := errors.New("LLM call failed: CONTEXT DEADLINE EXCEEDED")
		got := formatComponentFailure("Extractor", err)
		if !strings.Contains(got, "timed out") {
			t.Fatalf("formatComponentFailure(mixed case) = %q, want the timeout guidance", got)
		}
	})

	t.Run("component prefix is stripped before reformatting", func(t *testing.T) {
		got := formatComponentFailure("Extractor", errors.New("Extractor: model quota exceeded"))
		if got != "Extractor: model quota exceeded" {
			t.Fatalf("formatComponentFailure = %q, want the prefix not doubled", got)
		}
	})

	t.Run("mixed case prefix is stripped", func(t *testing.T) {
		got := formatComponentFailure("Extractor", errors.New("extractor: model quota exceeded"))
		if got != "Extractor: model quota exceeded" {
			t.Fatalf("formatComponentFailure = %q, want the case-insensitive prefix stripped", got)
		}
	})

	t.Run("empty component name returns the bare message", func(t *testing.T) {
		got := formatComponentFailure("", errors.New("boom"))
		if got != "boom" {
			t.Fatalf("formatComponentFailure(\"\") = %q, want %q", got, "boom")
		}
	})

	t.Run("empty component name and nil error", func(t *testing.T) {
		if got := formatComponentFailure("", nil); got != " Error" {
			t.Fatalf("formatComponentFailure(\"\", nil) = %q, want %q", got, " Error")
		}
	})
}

// TestPipelineHasNoBareComponentNameLiterals guards the constant consolidation:
// component identity strings in the pipeline package must come from the shared
// schema constants, not inline literals, so a rename touches one place instead
// of a grep. The scan is AST-based so comments (which may mention a name in
// prose) are not flagged.
func TestPipelineHasNoBareComponentNameLiterals(t *testing.T) {
	banned := []string{
		`"Extractor"`, `"extractor"`,
		`"Compiler"`, `"compiler"`,
		`"Parser"`, `"parser"`,
	}
	entries, err := os.ReadDir(".")
	if err != nil {
		t.Fatalf("read pipeline package dir: %v", err)
	}
	for _, entry := range entries {
		name := entry.Name()
		if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
			continue
		}
		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, name, nil, 0)
		if err != nil {
			t.Fatalf("parse %s: %v", name, err)
		}
		ast.Inspect(f, func(n ast.Node) bool {
			lit, ok := n.(*ast.BasicLit)
			if !ok || lit.Kind != token.STRING {
				return true
			}
			for _, want := range banned {
				if lit.Value == want {
					t.Errorf("%s contains the bare component-name literal %s; use the shared schema constant", name, want)
				}
			}
			return true
		})
	}
}