internal/ingestion/pipeline/pipeline_failure_message_test.go
Rama main · réplica de lectura del repositorio original.
//
// 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
})
}
}