Files
ragflow/internal/agent/component/switch_operators_test.go
Zhichang Yu e45659868a feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)
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>
2026-06-17 13:24:03 +08:00

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")
}
}