Files
ragflow/internal/agent/component/message_phase8b_test.go
Haruko386 3fe9d291fb fix: support doc generator component (#16858)
### Summary

As title

<img width="3165" height="2011" alt="image"
src="https://github.com/user-attachments/assets/6947aa4f-5dfd-4d8c-b6af-82015b1f1537"
/>

---

<img width="3735" height="1721" alt="image"
src="https://github.com/user-attachments/assets/3a7ec8e8-d5b9-4195-bba9-c435218f6142"
/>
2026-07-14 13:17:30 +08:00

295 lines
9.1 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 component
import (
"context"
"strings"
"testing"
"ragflow/internal/agent/audio"
"ragflow/internal/agent/canvas"
)
// fakeSynthesizer is a programmable audio.Synthesizer for
// integration tests. It records the request and returns the
// canned response / error configured at construction time.
type fakeSynthesizer struct {
resp *audio.SynthesizeResponse
err error
got *audio.SynthesizeRequest
}
func (f *fakeSynthesizer) Synthesize(_ context.Context, req audio.SynthesizeRequest) (*audio.SynthesizeResponse, error) {
if f.got != nil {
*f.got = req
}
return f.resp, f.err
}
// TestMessage_OutputFormatParam: when the DSL declares
// output_format=html, the rendered body is HTML-wrapped.
func TestMessage_OutputFormatParam(t *testing.T) {
c, _ := NewMessageComponent(map[string]any{
"text": "hello",
"output_format": "html",
})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
out, err := c.Invoke(ctx, map[string]any{"text": "hello", "stream": false})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
got, _ := out["content"].(string)
if !strings.Contains(got, `<div class="rf-message">hello`) {
t.Errorf("expected HTML wrap, got %q", got)
}
}
// TestMessage_OutputFormatInputOverride: the inputs["output_format"]
// key wins over the per-instance declaration.
func TestMessage_OutputFormatInputOverride(t *testing.T) {
c, _ := NewMessageComponent(map[string]any{
"text": "hi",
"output_format": "html",
})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
out, err := c.Invoke(ctx, map[string]any{
"text": "hi",
"output_format": "plain",
})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
got, _ := out["content"].(string)
if got != "hi" {
t.Errorf("expected plain passthrough, got %q", got)
}
}
// TestMessage_DownloadsExtraction: a {doc_id, filename, mime_type}
// entry in inputs is surfaced under outputs["downloads"].
func TestMessage_DownloadsExtraction(t *testing.T) {
c, _ := NewMessageComponent(map[string]any{"text": "see attachment"})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
dl := map[string]any{
"doc_id": "d-1",
"filename": "report.csv",
"mime_type": "text/csv",
"url": "/dl/d-1",
}
out, err := c.Invoke(ctx, map[string]any{
"text": "see attachment",
"stream": false,
"attach": dl,
})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
dls, ok := out["downloads"].([]DownloadInfo)
if !ok {
t.Fatalf("downloads key missing or wrong type: %T", out["downloads"])
}
if len(dls) != 1 || dls[0].DocID != "d-1" || dls[0].Filename != "report.csv" {
t.Errorf("unexpected downloads: %+v", dls)
}
// With default plain format, the rendered body does NOT embed
// the download descriptor (Python's _stringify_message_value
// returns "" for download-only inputs); we only assert the
// downloads key is populated.
if !strings.Contains(out["content"].(string), "see attachment") {
t.Errorf("body not preserved: %q", out["content"])
}
}
func TestMessage_DownloadJSONStringSuppressesContent(t *testing.T) {
c, _ := NewMessageComponent(map[string]any{"text": "unused"})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
downloadJSON := `{"doc_id":"d-1","filename":"report.md","mime_type":"text/markdown","url":"/api/v1/agents/attachments/d-1/download","include_download_info_in_content":true}`
out, err := c.Invoke(ctx, map[string]any{
"text": downloadJSON,
"stream": false,
})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
dls, ok := out["downloads"].([]DownloadInfo)
if !ok {
t.Fatalf("downloads key missing or wrong type: %T", out["downloads"])
}
if len(dls) != 1 || dls[0].DocID != "d-1" || dls[0].Filename != "report.md" {
t.Fatalf("unexpected downloads: %+v", dls)
}
if got, _ := out["content"].(string); got != "" {
t.Fatalf("content = %q, want empty download-only message", got)
}
}
// TestMessage_AutoPlay_NoEngine: when auto_play is enabled but no
// real synthesizer is registered, the textual content is still
// returned and outputs["audio_error"] surfaces the deferred
// state.
func TestMessage_AutoPlay_NoEngine(t *testing.T) {
// Force the stub (the default) so the test does not depend on
// what previous tests installed.
audio.SetSynthesizer(nil)
c, _ := NewMessageComponent(map[string]any{
"text": "hi",
"auto_play": true,
})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
out, err := c.Invoke(ctx, map[string]any{"text": "hi", "stream": false})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if _, ok := out["audio"]; ok {
t.Errorf("expected no audio key, got %+v", out["audio"])
}
if _, ok := out["audio_error"]; !ok {
t.Errorf("expected audio_error key, got %+v", out)
}
}
// TestMessage_AutoPlay_Success: with a fake synthesizer that
// returns bytes, outputs["audio"] is populated with the right
// envelope shape.
func TestMessage_AutoPlay_Success(t *testing.T) {
got := audio.SynthesizeRequest{}
prev := audio.GetSynthesizer()
audio.SetSynthesizer(&fakeSynthesizer{
resp: &audio.SynthesizeResponse{Audio: []byte("abc"), MediaType: "audio/mpeg"},
got: &got,
})
defer audio.SetSynthesizer(prev)
c, _ := NewMessageComponent(map[string]any{
"text": "hi",
"auto_play": "gtts",
"voice": "en",
"lang": "en",
})
state := canvas.NewCanvasState("r1", "t1")
ctx := withStateForTest(context.Background(), state)
out, err := c.Invoke(ctx, map[string]any{"text": "hi", "stream": false})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
env, ok := out["audio"].(map[string]any)
if !ok {
t.Fatalf("expected audio envelope, got %T", out["audio"])
}
if env["media_type"] != "audio/mpeg" {
t.Errorf("media_type: got %v, want audio/mpeg", env["media_type"])
}
if got.Engine != audio.EngineGTTS {
t.Errorf("engine forwarded: got %q, want gtts", got.Engine)
}
if got.Voice != "en" {
t.Errorf("voice forwarded: got %q, want en", got.Voice)
}
if got.Lang != "en" {
t.Errorf("lang forwarded: got %q, want en", got.Lang)
}
}
// TestMessage_MemorySave_NoService: with memory_save=true and no
// MemorySaver registered, the textual content flows and
// outputs["memory_error"] surfaces the deferred state.
func TestMessage_MemorySave_NoService(t *testing.T) {
SetMemorySaver(nil)
c, _ := NewMessageComponent(map[string]any{"text": "hi"})
state := canvas.NewCanvasState("run-x", "task-x")
state.Sys["query"] = "what?"
ctx := withStateForTest(context.Background(), state)
out, err := c.Invoke(ctx, map[string]any{
"text": "hi",
"stream": false,
"memory_save": true,
"memory_ids": []string{"m1"},
})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if got, _ := out["content"].(string); got != "hi" {
t.Errorf("content: got %q, want 'hi'", got)
}
if _, ok := out["memory_error"]; !ok {
t.Errorf("expected memory_error key, got %+v", out)
}
}
// TestMessage_MemorySave_Success: with a custom MemorySaver,
// Save is called with the right fields.
func TestMessage_MemorySave_Success(t *testing.T) {
var saved MemorySaveRequest
saveFn := memSaverFunc(func(_ context.Context, req MemorySaveRequest) error {
saved = req
return nil
})
SetMemorySaver(&saveFn)
defer SetMemorySaver(nil)
c, _ := NewMessageComponent(map[string]any{"text": "hi"})
state := canvas.NewCanvasState("run-y", "task-y")
state.Sys["query"] = "what?"
ctx := withStateForTest(context.Background(), state)
_, err := c.Invoke(ctx, map[string]any{
"text": "hi",
"stream": false,
"memory_save": true,
"memory_ids": []string{"m1", "m2"},
})
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if len(saved.MemoryIDs) != 2 || saved.MemoryIDs[0] != "m1" || saved.MemoryIDs[1] != "m2" {
t.Errorf("MemoryIDs: %+v", saved.MemoryIDs)
}
if saved.AgentID != "task-y" {
t.Errorf("AgentID: got %q, want task-y", saved.AgentID)
}
if saved.SessionID != "run-y" {
t.Errorf("SessionID: got %q, want run-y", saved.SessionID)
}
if saved.UserInput != "what?" {
t.Errorf("UserInput: got %q, want what?", saved.UserInput)
}
if saved.AgentResponse != "hi" {
t.Errorf("AgentResponse: got %q, want hi", saved.AgentResponse)
}
}
// memSaverFunc adapts a closure to the MemorySaver interface so
// tests can record the call inline.
type memSaverFunc func(ctx context.Context, req MemorySaveRequest) error
func (f memSaverFunc) Save(ctx context.Context, req MemorySaveRequest) error {
return f(ctx, req)
}