mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
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
191 lines
6.1 KiB
Go
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())
|
|
}
|
|
}
|