103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
package workbenchruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func TestBuildCacheKeyActorIsolationAndAuthority(t *testing.T) {
|
|
base := cacheKeyParts{Class: "sessions.summary", ActorID: "actor-a", SessionID: "ses-1", TraceID: "trace-1", Cursor: "idx:0", ProjectionSeq: 42}
|
|
keyA, err := buildCacheKey(512, base)
|
|
if err != nil {
|
|
t.Fatalf("build key actor a: %v", err)
|
|
}
|
|
base.ActorID = "actor-b"
|
|
keyB, err := buildCacheKey(512, base)
|
|
if err != nil {
|
|
t.Fatalf("build key actor b: %v", err)
|
|
}
|
|
if keyA == keyB {
|
|
t.Fatalf("expected actor-isolated keys, got identical key %q", keyA)
|
|
}
|
|
if !strings.Contains(keyA, cacheKeySchemaVersion) || !strings.Contains(keyA, "class=sessions.summary") {
|
|
t.Fatalf("key missing schema/class: %s", keyA)
|
|
}
|
|
if strings.Contains(keyA, "actor-a") || strings.Contains(keyB, "actor-b") {
|
|
t.Fatalf("cache key should hash raw actor ids: %s / %s", keyA, keyB)
|
|
}
|
|
|
|
_, err = buildCacheKey(512, cacheKeyParts{Class: "sessions.summary", ActorID: "actor-a"})
|
|
if cacheErrKind(err) != "cache_key_invalid" {
|
|
t.Fatalf("expected missing authority to be rejected, got %v", err)
|
|
}
|
|
_, err = buildCacheKey(512, cacheKeyParts{Class: "sessions.summary", ProjectionSeq: 1})
|
|
if cacheErrKind(err) != "cache_key_invalid" {
|
|
t.Fatalf("expected missing actor to be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCachePayloadBoundary(t *testing.T) {
|
|
_, err := cachePayloadBytes(map[string]any{"sessionId": "ses-1", "summary": map[string]any{"messageCount": 2}}, 1024)
|
|
if err != nil {
|
|
t.Fatalf("safe payload rejected: %v", err)
|
|
}
|
|
|
|
for _, payload := range []map[string]any{
|
|
{"authorization": "bearer secret"},
|
|
{"session": map[string]any{"prompt": "full prompt text"}},
|
|
{"providerPayload": map[string]any{"raw": true}},
|
|
{"stdout": strings.Repeat("x", 16)},
|
|
} {
|
|
_, err := cachePayloadBytes(payload, 1024)
|
|
if cacheErrKind(err) != "cache_payload_rejected" {
|
|
t.Fatalf("expected payload rejection for %#v, got %v", payload, err)
|
|
}
|
|
}
|
|
|
|
_, err = cachePayloadBytes(map[string]any{"sessionId": strings.Repeat("x", 64)}, 16)
|
|
if cacheErrKind(err) != "cache_payload_too_large" {
|
|
t.Fatalf("expected payload too large, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCacheErrorClassification(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
kind string
|
|
}{
|
|
{redis.Nil, "cache_miss"},
|
|
{context.DeadlineExceeded, "cache_timeout"},
|
|
{context.Canceled, "cache_context_canceled"},
|
|
{errors.New("dial tcp 127.0.0.1:6379: connect: connection refused"), "cache_unavailable"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := classifyCacheError(tc.err); got != tc.kind {
|
|
t.Fatalf("classifyCacheError(%v)=%s, want %s", tc.err, got, tc.kind)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisabledCacheDoesNotFailReadPath(t *testing.T) {
|
|
cache := newDerivedCache(cacheConfig{Enabled: false})
|
|
hit, err := cache.Get(context.Background(), "ignored", &map[string]any{})
|
|
if err != nil || hit {
|
|
t.Fatalf("disabled cache should be transparent, hit=%v err=%v", hit, err)
|
|
}
|
|
diag := cache.Diagnostic(context.Background())
|
|
if diag["enabled"] != false || diag["available"] != false {
|
|
t.Fatalf("unexpected disabled diagnostic: %#v", diag)
|
|
}
|
|
}
|
|
|
|
func cacheErrKind(err error) string {
|
|
var cacheErr cacheError
|
|
if errors.As(err, &cacheErr) {
|
|
return cacheErr.Kind
|
|
}
|
|
return classifyCacheError(err)
|
|
}
|