Ctrl K
ComunidadGuías…

internal/app/memory_maintenance_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package app

import (
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"github.com/yeomyeonggeori/bluememo"

	"github.com/yeomyeonggeori/blueclaw/internal/capability"
	"github.com/yeomyeonggeori/blueclaw/internal/llm"
	"github.com/yeomyeonggeori/blueclaw/internal/memory"
	"github.com/yeomyeonggeori/blueclaw/internal/memory/memorytest"
	runtimelogging "github.com/yeomyeonggeori/blueclaw/internal/runtime"
)

type embeddingRequestRecorder struct {
	mutex    sync.Mutex
	requests []map[string]any
}

func (recorder *embeddingRequestRecorder) handler(t *testing.T) http.HandlerFunc {
	return func(responseWriter http.ResponseWriter, request *http.Request) {
		var received map[string]any
		if errorValue := json.NewDecoder(request.Body).Decode(&received); errorValue != nil {
			t.Error(errorValue)
		}
		recorder.mutex.Lock()
		recorder.requests = append(recorder.requests, received)
		recorder.mutex.Unlock()
		inputs, _ := received["input"].([]any)
		embeddings := make([][]float64, len(inputs))
		for index := range embeddings {
			embeddings[index] = make([]float64, llm.DefaultEmbeddingDimensions)
			embeddings[index][0] = 1
		}
		_ = json.NewEncoder(responseWriter).Encode(map[string]any{"embeddings": embeddings})
	}
}

func TestStartupMaintenanceReembedsMemoriesWrittenUnderAnotherModel(t *testing.T) {
	stores, root := memorytest.OpenEmbeddedBy(t, "baai/bge-m3")
	memorytest.Remember(t, stores, memory.WorkspaceScope(), "이샘플 keeps the quarterly ledger")
	if errorValue := stores.Close(); errorValue != nil {
		t.Fatal(errorValue)
	}
	recorder := &embeddingRequestRecorder{}
	server := httptest.NewServer(recorder.handler(t))
	defer server.Close()
	reopened := memory.NewStores(root, bluememo.Configuration{
		Embedder: llm.CapabilityEmbeddingClient{
			CapabilityClient: capability.Client{Endpoint: server.URL, HTTPClient: server.Client()},
			ModelName:        llm.DefaultEmbeddingModelName,
			OutputDimensions: llm.DefaultEmbeddingDimensions,
		},
	}, memorytest.ReadsInThisProcess())
	t.Cleanup(func() { _ = reopened.Close() })
	application := &Application{
		memoryStores:  reopened,
		runtimeLogger: &runtimelogging.PersistentLogger{Logger: slog.Default()},
	}

	application.startMemoryMaintenance()
	t.Cleanup(application.memoryMaintenanceCancel)

	waitForNoStaleMemory(t, reopened)
	recorder.mutex.Lock()
	defer recorder.mutex.Unlock()
	if len(recorder.requests) == 0 || recorder.requests[0]["model"] != "google/embeddinggemma-2" || recorder.requests[0]["inputType"] != llm.EmbeddingInputTypeDocument || recorder.requests[0]["outputDimensions"] != float64(768) {
		t.Fatalf("expected a document embedding by embeddinggemma at 768 dimensions, got %v", recorder.requests)
	}
}

func waitForNoStaleMemory(t *testing.T, stores *memory.Stores) {
	t.Helper()
	store, errorValue := stores.Store(context.Background(), memory.WorkspaceScope())
	if errorValue != nil {
		t.Fatal(errorValue)
	}
	deadline := time.Now().Add(10 * time.Second)
	for time.Now().Before(deadline) {
		state, errorValue := store.IndexState(context.Background())
		if errorValue != nil {
			t.Fatal(errorValue)
		}
		if state.Stale == 0 && state.Current > 0 {
			if state.EmbeddingModel != "google/embeddinggemma-2" {
				t.Fatalf("expected the index to be current for embeddinggemma, got %q", state.EmbeddingModel)
			}
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatal("startup maintenance left memories embedded by the previous model")
}