Files
ragflow/internal/service/memory_message_service_test.go

167 lines
5.5 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.
//
// memory_message_service_test.go — MemorySaver port tests.
//
// Coverage focuses on the synchronous parts (memory lookup + raw
// message construction + result aggregation).
package service
import (
"context"
"strings"
"testing"
)
// TestQueueSaveToMemoryTask_NilService: a nil receiver surfaces
// a clear error rather than panicking.
func TestQueueSaveToMemoryTask_NilService(t *testing.T) {
var s *MemoryMessageService
_, err := s.QueueSaveToMemoryTask(context.Background(), []string{"m1"}, MemoryMessage{AgentID: "a1"})
if err == nil {
t.Fatal("expected error from nil service")
}
if !strings.Contains(err.Error(), "nil") {
t.Errorf("error = %v, want nil-service error", err)
}
}
// TestQueueSaveToMemoryTask_EmptyMemoryList: an empty input
// short-circuits to an empty result with no error.
func TestQueueSaveToMemoryTask_EmptyMemoryList(t *testing.T) {
s := &MemoryMessageService{memories: nil} // no lookups happen
res, err := s.QueueSaveToMemoryTask(context.Background(), nil, MemoryMessage{AgentID: "a1"})
if err != nil {
t.Fatalf("QueueSaveToMemoryTask: %v", err)
}
if len(res.NotFound) != 0 || len(res.Failed) != 0 {
t.Errorf("expected empty result, got %+v", res)
}
}
// TestQueueSaveToMemoryTask_MissingAgentID: AgentID is required
// for the row envelope; an empty AgentID surfaces a clear error
// up front.
func TestQueueSaveToMemoryTask_MissingAgentID(t *testing.T) {
s := &MemoryMessageService{}
_, err := s.QueueSaveToMemoryTask(context.Background(), []string{"m1"}, MemoryMessage{})
if err == nil {
t.Fatal("expected error for missing AgentID")
}
if !strings.Contains(err.Error(), "AgentID") {
t.Errorf("error = %v, want AgentID-required error", err)
}
}
// TestBuildRawMessage_EnvelopeShape: the row envelope carries
// every logical field the Python extractor reads. Storage-level
// fields (message_type_kwd, content_ltks, tokenized_content_ltks,
// status_int) are the doc engine's concern and must not be set here.
func TestBuildRawMessage_EnvelopeShape(t *testing.T) {
raw := buildRawMessage(42, "mem-1", MemoryMessage{
UserID: "u1",
AgentID: "a1",
SessionID: "s1",
UserInput: "hi",
AgentResponse: "hello",
})
want := map[string]any{
"message_id": int64(42),
"message_type": "raw",
"memory_id": "mem-1",
"user_id": "u1",
"agent_id": "a1",
"session_id": "s1",
"status": true,
}
for k, want := range want {
if got := raw[k]; got != want {
t.Errorf("raw[%q] = %v (%T), want %v (%T)", k, got, got, want, want)
}
}
for _, storageField := range []string{"message_type_kwd", "content_ltks", "tokenized_content_ltks", "status_int"} {
if _, ok := raw[storageField]; ok {
t.Errorf("raw[%q] is a storage field and must not be set by the service layer", storageField)
}
}
content, _ := raw["content"].(string)
if !strings.Contains(content, "User Input: hi") {
t.Errorf("content missing user input: %q", content)
}
if !strings.Contains(content, "Agent Response: hello") {
t.Errorf("content missing agent response: %q", content)
}
if !strings.Contains(content, "\n") {
t.Errorf("content should have user/agent on separate lines: %q", content)
}
}
// TestBuildTaskRow_Shape: the task row mirrors the Python Task
// entity's memory-task shape.
func TestBuildTaskRow_Shape(t *testing.T) {
row := buildTaskRow(99, "mem-1")
if row["task_type"] != "memory" {
t.Errorf("task_type = %v, want \"memory\"", row["task_type"])
}
if row["doc_id"] != "mem-1" {
t.Errorf("doc_id = %v, want \"mem-1\"", row["doc_id"])
}
if row["progress"] != 0.0 {
t.Errorf("progress = %v, want 0.0", row["progress"])
}
if row["progress_msg"] != "" {
t.Errorf("progress_msg = %v, want empty string", row["progress_msg"])
}
if row["digest"] != "99" {
t.Errorf("digest = %v, want \"99\"", row["digest"])
}
if id, _ := row["id"].(string); len(id) != 32 {
t.Errorf("id = %q, want 32-char uuid", id)
}
}
func TestTaskFromRow_InitializesProgressMessage(t *testing.T) {
task := taskFromRow(buildTaskRow(99, "mem-1"))
if task.ProgressMsg == nil {
t.Fatal("ProgressMsg is nil")
}
if *task.ProgressMsg != "" {
t.Fatalf("ProgressMsg = %q, want empty string", *task.ProgressMsg)
}
}
// TestGenerateRawMessageID_Unique: two calls produce different
// values. (Wall-clock based today; the Redis-backed counter will
// be added when the project's Redis client lands.)
func TestGenerateRawMessageID_Unique(t *testing.T) {
a := generateRawMessageID()
b := generateRawMessageID()
if a == b {
// Allow a 1-second tie when the clock hasn't ticked.
// GenerateRawMessageID uses Unix seconds; two calls
// within the same second will collide. The Redis
// counter is the eventual fix; this test only
// verifies monotonic-ish behaviour.
t.Logf("note: raw_message_ids collided (%d) — within the same second; the Redis counter will fix this", a)
}
if a <= 0 || b <= 0 {
t.Errorf("expected positive ids, got a=%d b=%d", a, b)
}
}