// // 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 canvas import ( "context" "errors" "testing" "time" "ragflow/internal/agent/runtime" ) // blockingComponent is a runtime.Component whose Invoke blocks until ctx // is cancelled. Used to test the per-component timeout wrapper in // realComponentBody. type blockingComponent struct{} func (b *blockingComponent) Name() string { return "blocking" } func (b *blockingComponent) Invoke(ctx context.Context, _ map[string]any) (map[string]any, error) { <-ctx.Done() return nil, ctx.Err() } func (b *blockingComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) { return nil, nil } func (b *blockingComponent) Inputs() map[string]string { return nil } func (b *blockingComponent) Outputs() map[string]string { return nil } // TestRealComponentBody_RespectsTimeout verifies that a component whose // Invoke blocks longer than the configured timeout causes the body to // return a deadline-exceeded error within a small slack window of the // timeout, not hang indefinitely. func TestRealComponentBody_RespectsTimeout(t *testing.T) { t.Setenv("COMPONENT_EXEC_TIMEOUT", "1") comp := &blockingComponent{} body := realComponentBody("test-cpn", "TestBlocking", comp) if body == nil { t.Fatalf("realComponentBody returned nil") } start := time.Now() _, err := body(context.Background(), map[string]any{"x": 1}) elapsed := time.Since(start) if err == nil { t.Fatalf("expected error, got nil") } if !errors.Is(err, context.DeadlineExceeded) { t.Errorf("expected context.DeadlineExceeded wrapped error, got: %v", err) } if elapsed > 3*time.Second { t.Errorf("body did not honour 1s timeout: elapsed=%s", elapsed) } } // TestRealComponentBody_RespectsParentCancellation verifies that when // the parent context is already cancelled, the body surfaces a wrapped // context.Canceled error rather than a timeout (or a generic wrap). func TestRealComponentBody_RespectsParentCancellation(t *testing.T) { t.Setenv("COMPONENT_EXEC_TIMEOUT", "60") comp := &blockingComponent{} body := realComponentBody("test-cpn", "TestBlocking", comp) parentCtx, cancel := context.WithCancel(context.Background()) cancel() // pre-cancel _, err := body(parentCtx, map[string]any{"x": 1}) if err == nil { t.Fatalf("expected error, got nil") } if !errors.Is(err, context.Canceled) { t.Errorf("expected context.Canceled wrapped error, got: %v", err) } } // TestRealComponentBody_NoTimeoutWhenFast verifies that a component // returning immediately does not incur any timeout-induced latency or // error wrapping. func TestRealComponentBody_NoTimeoutWhenFast(t *testing.T) { t.Setenv("COMPONENT_EXEC_TIMEOUT", "60") // Stub component that returns immediately. comp := &echoComponent{} body := realComponentBody("test-cpn", "TestEcho", comp) out, err := body(context.Background(), map[string]any{"x": 1}) if err != nil { t.Fatalf("unexpected error: %v", err) } if out["__cpn_id__"] != "test-cpn" { t.Errorf("expected __cpn_id__=test-cpn, got %v", out["__cpn_id__"]) } if out["x"] != 1 { t.Errorf("expected input to pass through, got x=%v", out["x"]) } } // TestComponentTimeout_Default verifies the default is 600s when the env // var is unset. func TestComponentTimeout_Default(t *testing.T) { t.Setenv("COMPONENT_EXEC_TIMEOUT", "") if got := componentTimeout(); got != 600*time.Second { t.Errorf("default timeout: got %s, want 600s", got) } } // TestComponentTimeout_HonoursEnv verifies a valid env value is parsed. func TestComponentTimeout_HonoursEnv(t *testing.T) { t.Setenv("COMPONENT_EXEC_TIMEOUT", "42") if got := componentTimeout(); got != 42*time.Second { t.Errorf("env timeout: got %s, want 42s", got) } } // TestComponentTimeout_InvalidEnvFallsBack verifies that non-numeric or // non-positive env values fall back to the default — invalid input must // never widen the timeout silently. func TestComponentTimeout_InvalidEnvFallsBack(t *testing.T) { for _, v := range []string{"abc", "0", "-5"} { t.Setenv("COMPONENT_EXEC_TIMEOUT", v) if got := componentTimeout(); got != 600*time.Second { t.Errorf("invalid env %q: got %s, want default 600s", v, got) } } } // echoComponent is a minimal runtime.Component used by the no-timeout test. // It returns the input map unchanged plus a __cpn_id__ tag (the body will // overwrite the tag, but that's fine). type echoComponent struct{} func (e *echoComponent) Name() string { return "echo" } func (e *echoComponent) Invoke(_ context.Context, in map[string]any) (map[string]any, error) { out := make(map[string]any, len(in)) for k, v := range in { out[k] = v } return out, nil } func (e *echoComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) { return nil, nil } func (e *echoComponent) Inputs() map[string]string { return nil } func (e *echoComponent) Outputs() map[string]string { return nil } // Compile-time check that the stubs satisfy the interface. var ( _ runtime.Component = (*blockingComponent)(nil) _ runtime.Component = (*echoComponent)(nil) )