// // 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 handler import ( "encoding/json" "errors" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" _ "ragflow/internal/agent/component" // registers the production factory "ragflow/internal/dao" "ragflow/internal/entity" ) // componentCtx builds a Gin context for one of the // /agents/:canvas_id/components/:component_id/* endpoints. The // supplied `body` is bound as the request body (empty for GET). func componentCtx(t *testing.T, method, path, body string) (*gin.Context, *httptest.ResponseRecorder) { t.Helper() gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(method, path, strings.NewReader(body)) if body != "" { c.Request.Header.Set("Content-Type", "application/json") } c.Set("user", &entity.User{ID: "u-1"}) return c, w } // beginCanvas returns a UserCanvas whose DSL has a single Begin // component with both an input_form and a params block. Used for // happy-path tests. func beginCanvas() *entity.UserCanvas { return &entity.UserCanvas{ ID: "c1", DSL: map[string]any{ "components": map[string]any{ "begin": map[string]any{ "obj": map[string]any{ "component_name": "Begin", "params": map[string]any{ "mode": "Manual", }, "input_form": map[string]any{ "query": map[string]any{"type": "string"}, }, }, }, }, }, } } // ---------- GetComponentInputForm ---------- func TestGetComponentInputForm_HappyPath(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} c, w := componentCtx(t, "GET", "/api/v1/agents/c1/components/begin/input-form", "") c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.GetComponentInputForm(c) if w.Code != 200 { t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String()) } var env struct { Code int `json:"code"` Data map[string]interface{} `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil { t.Fatalf("decode: %v (body=%s)", err, w.Body.String()) } q, ok := env.Data["query"].(map[string]interface{}) if !ok { t.Fatalf("data.query = %v, want a map", env.Data["query"]) } if q["type"] != "string" { t.Errorf("data.query.type = %v, want string", q["type"]) } } func TestGetComponentInputForm_CanvasNotFound(t *testing.T) { h := &AgentHandler{loader: &fakeCanvasLoader{err: dao.ErrUserCanvasNotFound}} c, w := componentCtx(t, "GET", "/api/v1/agents/c1/components/begin/input-form", "") c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.GetComponentInputForm(c) code, msg := errBody(t, w.Body.Bytes()) if code != 103 { // CodeOperatingError — python @_require_canvas_access parity t.Errorf("code = %d, want 103; msg=%q", code, msg) } want := "Make sure you have permission to access the agent." if msg != want { t.Errorf("msg = %q, want %q", msg, want) } } func TestGetComponentInputForm_ComponentNotFound(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} c, w := componentCtx(t, "GET", "/api/v1/agents/c1/components/missing/input-form", "") c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "missing"}, } h.GetComponentInputForm(c) code, msg := errBody(t, w.Body.Bytes()) if code != 102 { // CodeDataError t.Errorf("code = %d, want 102; msg=%q", code, msg) } if !strings.Contains(msg, "component not found") { t.Errorf("msg = %q, want 'component not found'", msg) } } func TestGetComponentInputForm_NoInputForm(t *testing.T) { cv := &entity.UserCanvas{ ID: "c1", DSL: map[string]any{ "components": map[string]any{ "answer": map[string]any{ "obj": map[string]any{ "component_name": "Answer", // no input_form }, }, }, }, } h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} c, w := componentCtx(t, "GET", "/api/v1/agents/c1/components/answer/input-form", "") c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "answer"}, } h.GetComponentInputForm(c) code, msg := errBody(t, w.Body.Bytes()) if code != 102 { t.Errorf("code = %d, want 102; msg=%q", code, msg) } if !strings.Contains(msg, "no input_form") { t.Errorf("msg = %q, want 'no input_form'", msg) } } // ---------- DebugComponent ---------- func TestDebugComponent_HappyPath_Begin(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} body := `{"params":{"query":{"value":"hello world"}}}` c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/begin/debug", body) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.DebugComponent(c) if w.Code != 200 { t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String()) } // Begin's Invoke is a passthrough — it returns the input map as // outputs (see internal/agent/component/begin.go:96-98). Pin the // exact passthrough so a regression that drops the `query` field // would be caught (architect review A2). var env struct { Code int `json:"code"` Data map[string]interface{} `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil { t.Fatalf("decode: %v (body=%s)", err, w.Body.String()) } if env.Code != 0 { t.Errorf("code = %d, want 0; body=%s", env.Code, w.Body.String()) } if env.Data["query"] != "hello world" { t.Errorf("data.query = %v, want 'hello world'", env.Data["query"]) } } func TestDebugComponent_InvalidParams_MissingField(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} // No `params` field. c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/begin/debug", `{}`) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.DebugComponent(c) code, msg := errBody(t, w.Body.Bytes()) if code != 101 { // CodeArgumentError t.Errorf("code = %d, want 101; msg=%q", code, msg) } if !strings.Contains(msg, "params") { t.Errorf("msg = %q, want 'params'", msg) } } // TestDebugComponent_InvalidParams_MissingValue covers CodeRabbit // PR review #2: `{"params":{"q":{}}}` (no `value` key) should // fail-fast with 101 rather than silently invoking the component // with inputs["q"]=nil. func TestDebugComponent_InvalidParams_MissingValue(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/begin/debug", `{"params":{"q":{}}}`) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.DebugComponent(c) code, msg := errBody(t, w.Body.Bytes()) if code != 101 { t.Errorf("code = %d, want 101; msg=%q", code, msg) } if !strings.Contains(msg, "value") { t.Errorf("msg = %q, want mention of 'value'", msg) } } func TestDebugComponent_UnknownComponent(t *testing.T) { cv := beginCanvas() h := &AgentHandler{loader: &fakeCanvasLoader{canvas: cv}} body := `{"params":{"x":{"value":1}}}` c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/missing/debug", body) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "missing"}, } h.DebugComponent(c) code, msg := errBody(t, w.Body.Bytes()) if code != 102 { t.Errorf("code = %d, want 102; msg=%q", code, msg) } if !strings.Contains(msg, "component not found") { t.Errorf("msg = %q, want 'component not found'", msg) } } func TestDebugComponent_CannotAccessCanvas(t *testing.T) { h := &AgentHandler{loader: &fakeCanvasLoader{err: dao.ErrUserCanvasNotFound}} body := `{"params":{"x":{"value":1}}}` c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/begin/debug", body) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.DebugComponent(c) code, msg := errBody(t, w.Body.Bytes()) if code != 103 { // CodeOperatingError t.Errorf("code = %d, want 103; msg=%q", code, msg) } want := "Make sure you have permission to access the agent." if msg != want { t.Errorf("msg = %q, want %q", msg, want) } } func TestDebugComponent_LoaderError_NonNotFound(t *testing.T) { h := &AgentHandler{loader: &fakeCanvasLoader{err: errors.New("db conn lost")}} body := `{"params":{"x":{"value":1}}}` c, w := componentCtx(t, "POST", "/api/v1/agents/c1/components/begin/debug", body) c.Params = gin.Params{ {Key: "canvas_id", Value: "c1"}, {Key: "component_id", Value: "begin"}, } h.DebugComponent(c) code, _ := errBody(t, w.Body.Bytes()) if code != 500 { // CodeServerError t.Errorf("code = %d, want 500", code) } }