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) }