mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-30 07:51:10 +08:00
Replaces the Python agent canvas runtime with a Go implementation that runs inside `cmd/server_main`. The canvas compiles into an eino Workflow that pauses on wait-for-user via native Interrupt/Resume (no sentinel flag) and resumes from a Redis-backed CheckPointStore. All 21 Python agent components and ~35 tools are ported with functional parity. Sandbox providers now read their JSON config from the admin-panel system_settings table with env fallback. 234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate added); drops the v2 DSL E2E step and the gap-analysis plan (both redundant after the port ships). ## Type of change - [x] Refactoring - [x] New feature - [x] Bug fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
139 lines
5.1 KiB
Go
139 lines
5.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 (
|
|
"testing"
|
|
|
|
"ragflow/internal/agent/canvas"
|
|
)
|
|
|
|
// runOp is a small test helper that evaluates a single Switch
|
|
// clause and returns whether the group matched. It wraps the
|
|
// internal `evaluateClause` directly so the operator matrix is
|
|
// easy to assert without spinning up a full SwitchComponent +
|
|
// Invoke round-trip.
|
|
func runOp(t *testing.T, left string, op string, right any, sys map[string]any) bool {
|
|
t.Helper()
|
|
state := canvas.NewCanvasState("run-op", "task-op")
|
|
for k, v := range sys {
|
|
state.Sys[k] = v
|
|
}
|
|
clause := map[string]any{
|
|
"left": left,
|
|
"op": op,
|
|
"right": right,
|
|
}
|
|
matched, err := evaluateClause(clause, state)
|
|
if err != nil {
|
|
t.Fatalf("evaluateClause(op=%q): %v", op, err)
|
|
}
|
|
return matched
|
|
}
|
|
|
|
// TestSwitch_Operators_NotContains covers the `not contains` operator
|
|
// (Python `switch.py` parity; OQ #13 follow-up).
|
|
func TestSwitch_Operators_NotContains(t *testing.T) {
|
|
if matched := runOp(t, "hello world", "not contains", "foo", nil); !matched {
|
|
t.Errorf("not contains(haystack,absent) should match")
|
|
}
|
|
if matched := runOp(t, "hello world", "not contains", "world", nil); matched {
|
|
t.Errorf("not contains(haystack,present) should NOT match")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_StartWith covers `start with` (prefix match,
|
|
// case-insensitive).
|
|
func TestSwitch_Operators_StartWith(t *testing.T) {
|
|
if matched := runOp(t, "Hello World", "start with", "hello", nil); !matched {
|
|
t.Errorf("start with should be case-insensitive: 'Hello World' starts with 'hello' should match")
|
|
}
|
|
if matched := runOp(t, "Hello World", "start with", "world", nil); matched {
|
|
t.Errorf("'Hello World' starts with 'world' should NOT match")
|
|
}
|
|
if matched := runOp(t, "/api/v1/canvas", "start with", "/api/", nil); !matched {
|
|
t.Errorf("path prefix match failed")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_EndWith covers `end with` (suffix match,
|
|
// case-insensitive).
|
|
func TestSwitch_Operators_EndWith(t *testing.T) {
|
|
if matched := runOp(t, "report.PDF", "end with", ".pdf", nil); !matched {
|
|
t.Errorf("end with should be case-insensitive: 'report.PDF' ends with '.pdf' should match")
|
|
}
|
|
if matched := runOp(t, "image.png", "end with", ".jpg", nil); matched {
|
|
t.Errorf("'image.png' ends with '.jpg' should NOT match")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_NotEmpty covers `not empty` (negation of
|
|
// `empty`).
|
|
func TestSwitch_Operators_NotEmpty(t *testing.T) {
|
|
if matched := runOp(t, "{{sys.body}}", "not empty", nil, map[string]any{"body": "hello"}); !matched {
|
|
t.Errorf("not empty on 'hello' should match")
|
|
}
|
|
if matched := runOp(t, "{{sys.body}}", "not empty", nil, map[string]any{"body": ""}); matched {
|
|
t.Errorf("not empty on '' should NOT match")
|
|
}
|
|
// When a var ref fails to resolve, leftValue returns the raw
|
|
// template literal (e.g. "{{sys.absent}}") so that == / != can
|
|
// still operate and a misconfigured ref doesn't crash the run.
|
|
// The raw literal is non-empty, so `not empty` evaluates to
|
|
// true. This is documented in leftValue's comment.
|
|
if matched := runOp(t, "{{sys.absent}}", "not empty", nil, map[string]any{}); !matched {
|
|
t.Errorf("not empty on unresolved var ref (raw literal) should match (raw is non-empty)")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_GE covers the ≥ (greater-or-equal) operator.
|
|
func TestSwitch_Operators_GE(t *testing.T) {
|
|
if matched := runOp(t, "{{sys.x}}", ">=", 5, map[string]any{"x": 5}); !matched {
|
|
t.Errorf("5 >= 5 should match")
|
|
}
|
|
if matched := runOp(t, "{{sys.x}}", ">=", 5, map[string]any{"x": 6}); !matched {
|
|
t.Errorf("6 >= 5 should match")
|
|
}
|
|
if matched := runOp(t, "{{sys.x}}", ">=", 5, map[string]any{"x": 4}); matched {
|
|
t.Errorf("4 >= 5 should NOT match")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_LE covers the ≤ (less-or-equal) operator.
|
|
func TestSwitch_Operators_LE(t *testing.T) {
|
|
if matched := runOp(t, "{{sys.x}}", "<=", 5, map[string]any{"x": 5}); !matched {
|
|
t.Errorf("5 <= 5 should match")
|
|
}
|
|
if matched := runOp(t, "{{sys.x}}", "<=", 5, map[string]any{"x": 4}); !matched {
|
|
t.Errorf("4 <= 5 should match")
|
|
}
|
|
if matched := runOp(t, "{{sys.x}}", "<=", 5, map[string]any{"x": 6}); matched {
|
|
t.Errorf("6 <= 5 should NOT match")
|
|
}
|
|
}
|
|
|
|
// TestSwitch_Operators_EqualFolded confirms `==` is now case-
|
|
// insensitive (Python switch.py parity).
|
|
func TestSwitch_Operators_EqualFolded(t *testing.T) {
|
|
if matched := runOp(t, "Hello", "==", "hello", nil); !matched {
|
|
t.Errorf("== should be case-insensitive: 'Hello' == 'hello' should match")
|
|
}
|
|
if matched := runOp(t, "HELLO", "==", "hello", nil); !matched {
|
|
t.Errorf("== should be case-insensitive: 'HELLO' == 'hello' should match")
|
|
}
|
|
}
|