package multiagent import ( "context" "errors" "testing" "github.com/cloudwego/eino/adk" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" ) func TestEinoCheckpointResumeHandlerSkipsWithoutCheckpoint(t *testing.T) { called := false handler := newEinoCheckpointResumeHandler(einoCheckpointResumeHandlerConfig{ Resume: func(string) (*adk.AsyncIterator[*adk.AgentEvent], error) { called = true return nil, nil }, }) if iter := handler.TryResume(); iter != nil { t.Fatalf("iter = %#v, want nil", iter) } if called { t.Fatal("resume should not be called without checkpoint state") } } func TestEinoCheckpointResumeHandlerResumesExistingCheckpoint(t *testing.T) { store, err := newFileCheckPointStore(t.TempDir()) if err != nil { t.Fatal(err) } if err := store.Set(context.Background(), "cp-1", []byte("checkpoint")); err != nil { t.Fatal(err) } var progressMessages []string var resumedID string core, logs := observer.New(zap.InfoLevel) wantIter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() defer gen.Close() handler := newEinoCheckpointResumeHandler(einoCheckpointResumeHandlerConfig{ Context: context.Background(), ConversationID: "conv-1", OrchMode: "deep", Store: store, CheckPointID: "cp-1", Logger: zap.New(core), Progress: func(eventType, message string, data interface{}) { if eventType != "progress" { return } progressMessages = append(progressMessages, message) m, _ := data.(map[string]interface{}) if m["conversationId"] != "conv-1" || m["orchestration"] != "deep" || m["checkPointID"] != "cp-1" { t.Fatalf("progress data = %#v", m) } }, Resume: func(checkPointID string) (*adk.AsyncIterator[*adk.AgentEvent], error) { resumedID = checkPointID return wantIter, nil }, }) got := handler.TryResume() if got != wantIter { t.Fatalf("iter = %#v, want resume iterator", got) } if resumedID != "cp-1" { t.Fatalf("resumed id = %q", resumedID) } if len(progressMessages) != 1 || progressMessages[0] != "检测到断点,正在从中断节点恢复执行..." { t.Fatalf("progress messages = %#v", progressMessages) } if logs.FilterMessage("eino runner: resume from checkpoint").Len() != 1 { t.Fatalf("expected resume log, got %d", logs.Len()) } } func TestEinoCheckpointResumeHandlerFallsBackOnResumeError(t *testing.T) { store, err := newFileCheckPointStore(t.TempDir()) if err != nil { t.Fatal(err) } if err := store.Set(context.Background(), "cp-1", []byte("checkpoint")); err != nil { t.Fatal(err) } var progressMessages []string core, logs := observer.New(zap.WarnLevel) handler := newEinoCheckpointResumeHandler(einoCheckpointResumeHandlerConfig{ Context: context.Background(), Store: store, CheckPointID: "cp-1", Logger: zap.New(core), Progress: func(eventType, message string, _ interface{}) { if eventType == "progress" { progressMessages = append(progressMessages, message) } }, Resume: func(string) (*adk.AsyncIterator[*adk.AgentEvent], error) { return nil, errors.New("resume failed") }, }) if iter := handler.TryResume(); iter != nil { t.Fatalf("iter = %#v, want nil fallback", iter) } if len(progressMessages) != 2 || progressMessages[1] != "断点恢复失败,已回退为全新执行。" { t.Fatalf("progress messages = %#v", progressMessages) } if logs.FilterMessage("eino runner: resume failed, fallback to fresh run").Len() != 1 { t.Fatalf("expected fallback log, got %d", logs.Len()) } } func TestEinoCheckpointResumeHandlerLogsPreflightError(t *testing.T) { store, err := newFileCheckPointStore(t.TempDir()) if err != nil { t.Fatal(err) } core, logs := observer.New(zap.WarnLevel) handler := newEinoCheckpointResumeHandler(einoCheckpointResumeHandlerConfig{ Context: context.Background(), Store: store, CheckPointID: "bad/id", Logger: zap.New(core), Resume: func(string) (*adk.AsyncIterator[*adk.AgentEvent], error) { t.Fatal("resume should not be called after preflight error") return nil, nil }, }) if iter := handler.TryResume(); iter != nil { t.Fatalf("iter = %#v, want nil", iter) } if logs.FilterMessage("eino checkpoint preflight get failed").Len() != 1 { t.Fatalf("expected preflight warning, got %d", logs.Len()) } }