Files
ragflow/internal/agent/canvas/checkpoint_store_test.go
Zhichang Yu 3fa15c0e2f feat(agent): Go port — canvas engine, 22 components, DSL v2, 13 endpoints (#15952)
Ports the agent canvas subsystem from Python to Go.

## What's included

### Canvas Engine (Phase 0/1)
- State engine, scheduler, variable resolver, Redis checkpoint store,
cancel protocol
- **209 tests** across canvas / component / io packages

### 22 Components (P0–P4)
| Tier | Components |
|---|---|
| P0 T1+T2+T3 | LLM, Agent, ExitLoop, Switch, Categorize, Begin,
Message, Invoke |
| P1 T3 | VariableAggregator, VariableAssigner, StringTransform,
ListOperations, DataOperations |
| P2 T3 | Iteration, IterationItem, Loop, LoopItem |
| P3 T3 | UserFillUp, Fillup |
| P4 T5 | Browser, ExcelProcessor, DocsGenerator |

### DSL v2 Schema (Phase 2.5)
- Typed v2 in-memory model with v1-to-v2 auto-detect converter
- v1 legacy field stripping per plan §2.11.7

### HTTP Endpoints & Bug Fixes (Plans PR1–PR3)
- **DELETE SQL bug fix**: gorm v2 `Where("id = ?", id).Delete(...)`
pattern
- **CreateAgent validation**: title/DSL required, duplicate check, 103
envelope
- **13 new endpoints**: templates, prompts, tags, sessions CRUD,
chat/completions (SSE + non-stream stubs), rerun, test_db_connection,
logs, webhook/logs
- **756 Go unit tests** (745 → 756, +18)
- **17 → 0 Python integration test failures** (test_agents.py +
test_session_management/)

### Tools
21 eino tools: HTTPHelper, search tools, financial/data tools, mandatory
stubs

### Infrastructure
OTel observability, NATS message queue, DeepDoc gRPC client, SSRF
guards, IDOR mitigation
2026-06-12 22:58:28 +08:00

142 lines
4.3 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 canvas
import (
"context"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
// newTestStore spins up a miniredis-backed store for table-driven tests.
// Returns the store, the miniredis handle (caller must Close()), and a
// cleanup function. We construct the struct directly so we can inject the
// *redis.Client — NewRedisCheckPointStore reads from the global cache
// which is nil in unit tests.
func newTestStore(t *testing.T, ttl time.Duration) (*RedisCheckPointStore, *miniredis.Miniredis) {
t.Helper()
mr, err := miniredis.Run()
if err != nil {
t.Fatalf("miniredis.Run: %v", err)
}
t.Cleanup(mr.Close)
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { _ = client.Close() })
return &RedisCheckPointStore{client: client, ttl: ttl}, mr
}
func TestRedisCheckPointStore_RoundTrip(t *testing.T) {
store, _ := newTestStore(t, 30*24*time.Hour)
ctx := context.Background()
// missing key → (nil, false, nil)
got, ok, err := store.Get(ctx, "absent")
if err != nil || ok || got != nil {
t.Fatalf("Get(absent) = (%v, %v, %v); want (nil, false, nil)", got, ok, err)
}
// Set + Get round trip
payload := []byte("eino-serialized-bytes-\x00\x01\x02")
if err := store.Set(ctx, "cpn_42", payload); err != nil {
t.Fatalf("Set: %v", err)
}
got, ok, err = store.Get(ctx, "cpn_42")
if err != nil {
t.Fatalf("Get after Set: %v", err)
}
if !ok {
t.Fatalf("Get after Set: ok = false, want true")
}
if string(got) != string(payload) {
t.Fatalf("Get payload = %q, want %q", got, payload)
}
// Overwrite (eino re-uses ids; last write wins)
updated := []byte("replacement-payload")
if err := store.Set(ctx, "cpn_42", updated); err != nil {
t.Fatalf("Set overwrite: %v", err)
}
got, _, _ = store.Get(ctx, "cpn_42")
if string(got) != string(updated) {
t.Fatalf("Get after overwrite = %q, want %q", got, updated)
}
}
func TestRedisCheckPointStore_TTL(t *testing.T) {
store, mr := newTestStore(t, 2*time.Second)
ctx := context.Background()
if err := store.Set(ctx, "cpn_ttl", []byte("x")); err != nil {
t.Fatalf("Set: %v", err)
}
// miniredis exposes TTL on a key.
if d := mr.TTL(checkpointKeyPrefix + "cpn_ttl"); d != 2*time.Second {
t.Fatalf("TTL after Set = %v, want 2s", d)
}
// Fast-forward miniredis' internal clock past the TTL.
mr.FastForward(3 * time.Second)
_, ok, err := store.Get(ctx, "cpn_ttl")
if err != nil {
t.Fatalf("Get after expiry: %v", err)
}
if ok {
t.Fatalf("Get after expiry: ok = true, want false (key should be gone)")
}
}
func TestRedisCheckPointStore_Delete(t *testing.T) {
store, _ := newTestStore(t, time.Minute)
ctx := context.Background()
// Delete on missing key is a no-op (no error).
if err := store.Delete(ctx, "absent"); err != nil {
t.Fatalf("Delete absent: %v", err)
}
// Set then Delete then Get → missing.
if err := store.Set(ctx, "cpn_del", []byte("payload")); err != nil {
t.Fatalf("Set: %v", err)
}
if err := store.Delete(ctx, "cpn_del"); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, ok, _ := store.Get(ctx, "cpn_del"); ok {
t.Fatalf("Get after Delete: ok = true, want false")
}
}
func TestRedisCheckPointStore_NilClient(t *testing.T) {
// Cache uninitialized → NewRedisCheckPointStore returns a store with
// nil client. Operations must error rather than panic.
store := &RedisCheckPointStore{client: nil, ttl: time.Minute}
ctx := context.Background()
if _, _, err := store.Get(ctx, "x"); err == nil {
t.Fatal("Get with nil client: err = nil, want error")
}
if err := store.Set(ctx, "x", []byte("y")); err == nil {
t.Fatal("Set with nil client: err = nil, want error")
}
if err := store.Delete(ctx, "x"); err == nil {
t.Fatal("Delete with nil client: err = nil, want error")
}
}