Files
ragflow/internal/agent/component/excel_processor_test.go
Zhichang Yu 3fa15c0e2f feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.

## What's included

### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages

### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |

### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7

### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)

### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs

### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00

191 lines
6.1 KiB
Go

//
// 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 component
import (
"bytes"
"context"
"fmt"
"reflect"
"testing"
"github.com/xuri/excelize/v2"
"ragflow/internal/agent/canvas"
)
// excelCtx returns a fresh canvas-state context for component tests.
func excelCtx(t *testing.T) context.Context {
t.Helper()
state := canvas.NewCanvasState("run-xlsx", "task-xlsx")
return canvas.WithState(context.Background(), state)
}
// TestExcelProcessor_WriteThenRead: write a 2x2 grid, then read it
// back from the produced bytes; rows should round-trip.
func TestExcelProcessor_WriteThenRead(t *testing.T) {
grid := [][]any{
{"a", "b"},
{1, 2},
}
w, err := NewExcelProcessorComponent(map[string]any{
"operation": "write",
"output_data": grid,
})
if err != nil {
t.Fatalf("NewExcelProcessorComponent (write): %v", err)
}
out, err := w.Invoke(excelCtx(t), map[string]any{})
if err != nil {
t.Fatalf("write Invoke: %v", err)
}
raw, ok := out["bytes"].([]byte)
if !ok || len(raw) == 0 {
t.Fatalf("write: bytes output missing or empty (got %T)", out["bytes"])
}
if size, _ := out["size"].(int); size != len(raw) {
t.Errorf("write: size=%d, want %d (len bytes)", size, len(raw))
}
if names, _ := out["sheet_names"].([]string); len(names) == 0 {
t.Errorf("write: sheet_names empty, want >=1")
}
// ZIP magic header.
if !(raw[0] == 'P' && raw[1] == 'K' && raw[2] == 3 && raw[3] == 4) {
t.Errorf("write: bytes do not start with PK\\x03\\x04 ZIP magic: %x", raw[:4])
}
// Read those bytes back with a fresh component.
r, err := NewExcelProcessorComponent(map[string]any{"operation": "read"})
if err != nil {
t.Fatalf("NewExcelProcessorComponent (read): %v", err)
}
rout, err := r.Invoke(excelCtx(t), map[string]any{"bytes": raw})
if err != nil {
t.Fatalf("read Invoke: %v", err)
}
rows, _ := rout["rows"].([][]any)
if len(rows) != 2 {
t.Fatalf("read: got %d rows, want 2", len(rows))
}
// excelize returns everything as strings via GetRows. Compare cell-
// by-cell so 1 (int we wrote) and "1" (string excelize reports) line
// up via fmt.Sprintf("%v", ...).
if got, want := fmt.Sprintf("%v", rows[0][0]), "a"; got != want {
t.Errorf("read rows[0][0] = %q, want %q", got, want)
}
if got, want := fmt.Sprintf("%v", rows[0][1]), "b"; got != want {
t.Errorf("read rows[0][1] = %q, want %q", got, want)
}
if got, want := fmt.Sprintf("%v", rows[1][0]), "1"; got != want {
t.Errorf("read rows[1][0] = %q, want %q", got, want)
}
if got, want := fmt.Sprintf("%v", rows[1][1]), "2"; got != want {
t.Errorf("read rows[1][1] = %q, want %q", got, want)
}
}
// TestExcelProcessor_ReadSheetNames: build a workbook with two sheets
// directly via excelize, then read it back via the component and
// confirm both names appear.
func TestExcelProcessor_ReadSheetNames(t *testing.T) {
f := excelize.NewFile()
defer f.Close()
if _, err := f.NewSheet("Alpha"); err != nil {
t.Fatalf("NewSheet Alpha: %v", err)
}
if err := f.SetCellValue("Alpha", "A1", "x"); err != nil {
t.Fatalf("SetCellValue: %v", err)
}
var buf bytes.Buffer
if err := f.Write(&buf); err != nil {
t.Fatalf("Write: %v", err)
}
r, _ := NewExcelProcessorComponent(map[string]any{"operation": "read"})
out, err := r.Invoke(excelCtx(t), map[string]any{"bytes": buf.Bytes()})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
names, _ := out["sheet_names"].([]string)
if !reflect.DeepEqual(names, []string{"Sheet1", "Alpha"}) {
t.Errorf("sheet_names = %v, want [Sheet1 Alpha]", names)
}
}
// TestExcelProcessor_EmptyFile: writing an empty grid produces a
// valid (small) xlsx; reading it back returns zero rows but a valid
// sheet list.
func TestExcelProcessor_EmptyFile(t *testing.T) {
w, _ := NewExcelProcessorComponent(map[string]any{
"operation": "write",
"output_data": [][]any{},
})
out, err := w.Invoke(excelCtx(t), map[string]any{})
if err != nil {
t.Fatalf("write Invoke: %v", err)
}
raw, _ := out["bytes"].([]byte)
if len(raw) == 0 {
t.Fatal("write: expected non-empty bytes for an empty-grid xlsx")
}
if !(raw[0] == 'P' && raw[1] == 'K' && raw[2] == 3 && raw[3] == 4) {
t.Errorf("write: bytes do not start with PK\\x03\\x04 ZIP magic: %x", raw[:4])
}
r, _ := NewExcelProcessorComponent(map[string]any{"operation": "read"})
rout, err := r.Invoke(excelCtx(t), map[string]any{"bytes": raw})
if err != nil {
t.Fatalf("read Invoke: %v", err)
}
rows, _ := rout["rows"].([][]any)
if len(rows) != 0 {
t.Errorf("read empty: got %d rows, want 0", len(rows))
}
names, _ := rout["sheet_names"].([]string)
if len(names) == 0 {
t.Errorf("read empty: sheet_names should still list the default sheet")
}
}
// TestExcelProcessor_ParamCheck: invalid operation rejected.
func TestExcelProcessor_ParamCheck(t *testing.T) {
if _, err := NewExcelProcessorComponent(map[string]any{"operation": "bogus"}); err == nil {
t.Fatal("expected error for bogus operation, got nil")
}
}
// TestExcelProcessor_ReadMissingBytes: read without inputs.bytes
// surfaces a ParamError.
func TestExcelProcessor_ReadMissingBytes(t *testing.T) {
r, _ := NewExcelProcessorComponent(map[string]any{"operation": "read"})
_, err := r.Invoke(excelCtx(t), map[string]any{})
if err == nil {
t.Fatal("expected error for missing bytes, got nil")
}
}
// TestExcelProcessor_Registered: factory lookup.
func TestExcelProcessor_Registered(t *testing.T) {
c, err := New("ExcelProcessor", map[string]any{"operation": "read"})
if err != nil {
t.Fatalf("registry lookup: %v", err)
}
if c.Name() != "ExcelProcessor" {
t.Errorf("Name()=%q, want ExcelProcessor", c.Name())
}
}