mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +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>
151 lines
4.0 KiB
Go
151 lines
4.0 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
|
|
//
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const sampleDSL = `{
|
|
"components": {
|
|
"Begin:abc": {
|
|
"downstream": ["Message:def"],
|
|
"upstream": [],
|
|
"obj": {"component_name": "Begin", "params": {}}
|
|
},
|
|
"Message:def": {
|
|
"downstream": [],
|
|
"upstream": ["Begin:abc"],
|
|
"obj": {"component_name": "Message", "params": {"content": "hello"}}
|
|
}
|
|
},
|
|
"globals": {"sys.query": "world"},
|
|
"path": ["Begin:abc"]
|
|
}`
|
|
|
|
func writeTempFile(t *testing.T, name, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write temp: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func runMainCaptureStderr(t *testing.T, args []string) (stdout, stderr string, exitOK bool) {
|
|
t.Helper()
|
|
oldStdout, oldStderr := os.Stdout, os.Stderr
|
|
rOut, wOut, _ := os.Pipe()
|
|
rErr, wErr, _ := os.Pipe()
|
|
os.Stdout, os.Stderr = wOut, wErr
|
|
defer func() { os.Stdout, os.Stderr = oldStdout, oldStderr }()
|
|
|
|
// Re-parse flags from args.
|
|
origArgs := os.Args
|
|
os.Args = append([]string{"migrate-canvas"}, args...)
|
|
defer func() { os.Args = origArgs }()
|
|
|
|
done := make(chan struct{})
|
|
var outBuf, errBuf bytes.Buffer
|
|
go func() { _, _ = io.Copy(&outBuf, rOut) }()
|
|
go func() { _, _ = io.Copy(&errBuf, rErr) }()
|
|
|
|
main()
|
|
wOut.Close()
|
|
wErr.Close()
|
|
<-done
|
|
out, _ := io.ReadAll(rOut)
|
|
er, _ := io.ReadAll(rErr)
|
|
_ = out
|
|
_ = er
|
|
return outBuf.String(), errBuf.String(), true
|
|
}
|
|
|
|
// TestRunOne_PrettyPrintNoDrift exercises the happy path: a valid
|
|
// DSL is normalised, the JSON is well-formed, and the output
|
|
// contains the same top-level components we put in.
|
|
func TestRunOne_PrettyPrintNoDrift(t *testing.T) {
|
|
path := writeTempFile(t, "ok.json", sampleDSL)
|
|
|
|
rOut, wOut, _ := os.Pipe()
|
|
oldStdout := os.Stdout
|
|
os.Stdout = wOut
|
|
defer func() { os.Stdout = oldStdout }()
|
|
|
|
if err := runOne(path, "", false); err != nil {
|
|
t.Fatalf("runOne ok: %v", err)
|
|
}
|
|
wOut.Close()
|
|
got, _ := io.ReadAll(rOut)
|
|
|
|
// Output must be valid JSON.
|
|
var v map[string]any
|
|
if err := json.Unmarshal(got, &v); err != nil {
|
|
t.Fatalf("output not valid JSON: %v\nbody: %s", err, got)
|
|
}
|
|
if _, ok := v["components"]; !ok {
|
|
t.Errorf("normalised output missing 'components' top-level key: %v", v)
|
|
}
|
|
}
|
|
|
|
// TestRunOne_GoldenMatch writes a golden, then re-reads the same
|
|
// DSL and asserts the normalisation is stable (idempotent).
|
|
func TestRunOne_GoldenMatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "x.json")
|
|
if err := os.WriteFile(path, []byte(sampleDSL), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Phase 1: write the golden.
|
|
if err := runOne(path, "", true); err != nil {
|
|
t.Fatalf("write-golden: %v", err)
|
|
}
|
|
golden := path + ".golden"
|
|
if _, err := os.Stat(golden); err != nil {
|
|
t.Fatalf("golden not written: %v", err)
|
|
}
|
|
|
|
// Phase 2: re-read the same DSL, expect a match.
|
|
if err := runOne(path, golden, false); err != nil {
|
|
t.Errorf("golden match: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRunOne_GoldenDrift covers the "drift detected" path: after
|
|
// NormalizeForCanvas changes (hypothetically), a stale golden
|
|
// surfaces as an error.
|
|
func TestRunOne_GoldenDrift(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "x.json")
|
|
if err := os.WriteFile(path, []byte(sampleDSL), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Stale golden that doesn't match the normalised output.
|
|
golden := filepath.Join(dir, "stale.json")
|
|
if err := os.WriteFile(golden, []byte(`{"totally": "different"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := runOne(path, golden, false); err == nil {
|
|
t.Errorf("expected drift error, got nil")
|
|
} else if !strings.Contains(err.Error(), "drift") {
|
|
t.Errorf("expected drift error, got: %v", err)
|
|
}
|
|
}
|