feat(memory): add Temporal activities integration for memory service
- Implement 12 Temporal activities for memory operations - Activities: create, update, search, context, diagnose, analyze, document - Add activity registration and worker setup - Full retry/timeout configuration with observability - Include workflow patterns and examples - All tests passing (23/23) Documentation: - MEMORY_INTEGRATION.md: High-level integration guide - MEMORY_ACTIVITIES.md: Complete activities reference - REGISTERED_ACTIVITIES.md: Registry and calling conventions
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"go.temporal.io/sdk/testsuite"
|
||||
)
|
||||
|
||||
func TestActivityCreateKnowledge(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-123"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.CreateKnowledgeActivity)
|
||||
|
||||
record := &KnowledgeRecord{
|
||||
Level: "L1",
|
||||
Content: "test",
|
||||
}
|
||||
|
||||
result, err := env.ExecuteActivity(activities.CreateKnowledgeActivity, record)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var id string
|
||||
if err := result.Get(&id); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if id != "chunk-123" {
|
||||
t.Errorf("expected chunk-123, got %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivitySearchKnowledge(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(QueryResponse{
|
||||
Results: []QueryResult{
|
||||
{
|
||||
ID: "chunk-123",
|
||||
Text: "matching knowledge",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.SearchKnowledgeActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.SearchKnowledgeActivity, "test query", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var records []KnowledgeRecord
|
||||
if err := result.Get(&records); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if len(records) != 1 {
|
||||
t.Errorf("expected 1 record, got %d", len(records))
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityGetContext(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(ContextResponse{
|
||||
Tier: 1,
|
||||
Lessons: []ContextLesson{
|
||||
{
|
||||
Tier: 1,
|
||||
Text: "lesson text",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.GetContextActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.GetContextActivity, "kubectl", "debug", 8192)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var ctx *ServiceContext
|
||||
if err := result.Get(&ctx); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if ctx.Tier != 1 {
|
||||
t.Errorf("expected tier 1, got %d", ctx.Tier)
|
||||
}
|
||||
|
||||
if len(ctx.Lessons) != 1 {
|
||||
t.Errorf("expected 1 lesson, got %d", len(ctx.Lessons))
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityDiagnoseIssue(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(ContextResponse{
|
||||
Tier: 1,
|
||||
Lessons: []ContextLesson{
|
||||
{
|
||||
Tier: 1,
|
||||
Text: "diagnosis: check logs",
|
||||
},
|
||||
},
|
||||
Skills: []ContextSkill{
|
||||
{
|
||||
Name: "debug-skill",
|
||||
Why: "matched",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.DiagnoseIssueActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.DiagnoseIssueActivity, "kubectl", "pod-crash")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var recommendations []string
|
||||
if err := result.Get(&recommendations); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if len(recommendations) == 0 {
|
||||
t.Error("expected recommendations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityAnalyzeError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(QueryResponse{
|
||||
Results: []QueryResult{
|
||||
{
|
||||
ID: "chunk-123",
|
||||
Level: "L1",
|
||||
Text: "solution: restart pod",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.AnalyzeErrorActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.AnalyzeErrorActivity, "CrashLoopBackOff")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var records []KnowledgeRecord
|
||||
if err := result.Get(&records); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if len(records) != 1 {
|
||||
t.Errorf("expected 1 record, got %d", len(records))
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityHealthCheck(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.HealthCheckActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.HealthCheckActivity)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var healthy bool
|
||||
if err := result.Get(&healthy); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if !healthy {
|
||||
t.Error("expected healthy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityLearnFromExecution(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-456"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.LearnFromExecutionActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(activities.LearnFromExecutionActivity, "task-123", "success", []string{"tag1"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var id string
|
||||
if err := result.Get(&id); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if id != "chunk-456" {
|
||||
t.Errorf("expected chunk-456, got %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityDocumentDecision(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(IngestResponse{ID: "chunk-789"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
suite := &testsuite.WorkflowTestSuite{}
|
||||
env := suite.NewTestActivityEnvironment()
|
||||
|
||||
svc := NewService(server.URL, "test-token", "poimen")
|
||||
activities := NewActivities(svc)
|
||||
|
||||
env.RegisterActivity(activities.DocumentDecisionActivity)
|
||||
|
||||
result, err := env.ExecuteActivity(
|
||||
activities.DocumentDecisionActivity,
|
||||
"scaling",
|
||||
"scale to 5 replicas",
|
||||
"high CPU usage",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("activity failed: %v", err)
|
||||
}
|
||||
|
||||
var id string
|
||||
if err := result.Get(&id); err != nil {
|
||||
t.Fatalf("get result failed: %v", err)
|
||||
}
|
||||
|
||||
if id != "chunk-789" {
|
||||
t.Errorf("expected chunk-789, got %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityOptions(t *testing.T) {
|
||||
opts := DefaultActivityOptions()
|
||||
|
||||
if opts.RetryAttempts != 3 {
|
||||
t.Errorf("expected 3 retry attempts, got %d", opts.RetryAttempts)
|
||||
}
|
||||
|
||||
if opts.StartTimeout == 0 {
|
||||
t.Error("expected non-zero start timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityError(t *testing.T) {
|
||||
err := &MemoryActivityError{
|
||||
ActivityName: "test-activity",
|
||||
Attempt: 2,
|
||||
Err: context.Canceled,
|
||||
}
|
||||
|
||||
msg := err.Error()
|
||||
if msg == "" {
|
||||
t.Error("expected error message")
|
||||
}
|
||||
|
||||
if !contains(msg, "test-activity") {
|
||||
t.Error("expected activity name in error")
|
||||
}
|
||||
|
||||
if !contains(msg, "attempt 2") {
|
||||
t.Error("expected attempt number in error")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i < len(s)-len(substr)+1; i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user