Files
ragflow/internal/agent/canvas/canvas_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

89 lines
3.1 KiB
Go

// Package canvas — Begin → Message e2e smoke test.
//
// The simplest end-to-end compile+run path. Verifies:
//
// 1. BuildWorkflow returns a non-nil Workflow for a 2-node DSL.
// 2. Compile returns a CompiledCanvas.
// 3. The compiled Runnable.Invoke runs to completion (no eino wiring error).
// 4. The Message node's "{{sys.query}}" reference resolves against state
// that was seeded into Sys — even though our placeholder lambda
// doesn't actually emit a string, we exercise the variable
// resolution path by writing into Outputs via SetVar before Invoke.
//
// The placeholder lambdas echo the input map; the test asserts the
// *plumbing* (compile, run, set/get state across nodes) without
// asserting component-specific semantics.
package canvas
import (
"context"
"testing"
)
// TestBeginToMessage_Smoke builds a Begin → Message DSL, seeds sys.query
// into state, and confirms the compiled workflow runs without error and
// the per-cpn Outputs bucket gets populated (proving the statePre/statePost
// handler chain works end-to-end).
func TestBeginToMessage_Smoke(t *testing.T) {
dsl := &Canvas{
Components: map[string]CanvasComponent{
"begin_0": {
Obj: CanvasComponentObj{ComponentName: "Begin", Params: map[string]any{}},
Downstream: []string{"message_0"},
Upstream: []string{},
},
"message_0": {
Obj: CanvasComponentObj{ComponentName: "Message", Params: map[string]any{
"text": "hello {{sys.query}}",
}},
Downstream: []string{},
Upstream: []string{"begin_0"},
},
},
Path: []string{"begin_0", "message_0"},
}
cc, err := Compile(context.Background(), dsl)
if err != nil {
t.Fatalf("Compile: %v", err)
}
if cc.Workflow == nil {
t.Fatal("compiled Workflow is nil")
}
// Pre-seed state to mirror what the Begin node would normally inject.
// With the real Begin component registered (via the blank import in
// loop_semantics_test.go), Begin reads inputs["query"] and writes it
// into state.Sys["query"] itself — so we pass the query through the
// input map instead of seeding it directly, and Begin propagates it
// into the context-attached state.
runState := NewCanvasState("run-smoke", "task-smoke")
runState.SetVar("begin_0", "request", map[string]any{"q": "world"})
// Stash runState on the context so the canvas runner can extract
// it via GetStateFromContext.
ctx := withState(context.Background(), runState)
// Invoke with the seed input. The "query" key flows into Begin's
// Invoke and is written to state.Sys["query"], where Message's
// ResolveTemplate of "{{sys.query}}" will read it.
in := map[string]any{"query": "world"}
out, err := cc.Workflow.Invoke(ctx, in)
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if out == nil {
t.Fatal("Invoke returned nil output")
}
// Variable resolution: ResolveTemplate against the seeded state must
// produce "hello world".
got, err := ResolveTemplate("hello {{sys.query}}", runState)
if err != nil {
t.Fatalf("ResolveTemplate: %v", err)
}
if got != "hello world" {
t.Fatalf("template resolve: got %q want %q", got, "hello world")
}
}