// // 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 — Phase 8b real MemorySaver port // tests. // // Coverage focuses on the synchronous parts (memory lookup + raw // message construction + result aggregation). The embedder call // is exercised only to verify it loud-fails with // ErrEmbedderNotWired — the embedder port itself is a separate // follow-up. package service import ( "context" "errors" "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 field the Python extractor reads. func TestBuildRawMessage_EnvelopeShape(t *testing.T) { mem := &CreateMemoryResponse{} mem.EmbdID = "embd-1" raw := buildRawMessage(42, "mem-1", mem, 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", "_memory_embd_id": "embd-1", } 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) } } 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) } } // TestBuildRawMessage_NilMemory: when the lookup returned nil // (e.g. caller already filtered NotFound), buildRawMessage // doesn't panic and omits _memory_embd_id. func TestBuildRawMessage_NilMemory(t *testing.T) { raw := buildRawMessage(1, "m", nil, MemoryMessage{AgentID: "a"}) if _, ok := raw["_memory_embd_id"]; ok { t.Errorf("_memory_embd_id should be absent when mem is nil") } // The other fields still land. if raw["agent_id"] != "a" { t.Errorf("agent_id missing or wrong: %v", raw["agent_id"]) } } // 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["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) } } // TestEmbedAndSave_LoudFails: until the embedder port lands, // the call must return ErrEmbedderNotWired — not panic, not // silently succeed. A successful embed_and_save would mask the // deferred state and let callers persist data that never gets // embedded. func TestEmbedAndSave_LoudFails(t *testing.T) { err := embedAndSave(context.Background(), nil, nil) if !errors.Is(err, ErrEmbedderNotWired) { t.Errorf("err = %v, want ErrEmbedderNotWired", err) } } // 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) } }