CVE-2024-31452: Insufficient Information (NVD-CWE-noinfo)
value: OpenFGA is a high-performance and flexible authorization/permission engine. Some end users of OpenFGA v1.5.0 or later are vulnerable to authorization bypass when calling Check or ListObjects APIs. You are very likely affected if your model involves exclusion (e.g. `a but not b`) or intersection (e.g. `a and b`). This vulnerability is fixed in v1.5.3.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-16T22:15Z
Repository: https://github.com/openfga/openfga
Language: Go
Files changed: assets/tests/consolidated_1_1_tests.yaml, internal/graph/cached_resolver.go, internal/graph/cached_resolver_test.go, internal/graph/check.go, internal/graph/check_test.go, internal/graph/cycle_check_resolver.go, internal/graph/cycle_check_resolver_test.go, internal/graph/graph.go, pkg/server/commands/list_objects.go, pkg/server/server.go, pkg/server/server_test.go
Commit date: 2024-04-16 14:27:19 -0700
Commit message: Merge pull request from GHSA-8cph-m685-6v6r

* fix: error handling leading to false positives

* test: add a failing test due to Check and ListObjects differences

* test: change test expectation for ListObjects assertion

* fix: union CheckFuncReducer and associated tests

* fix: intersection CheckFuncReducer

* fix: exclusion CheckFuncReducer

* fix: rewire the cycle detection mechanics to avoid error propagation

* test: add more edge case tests for cyclical evaluations

* test: rename new test that was poorly named

* chore: revert minor change that was unintended

* chore: tidy up changes and drop commented out code

* chore: minor touchups based on some feedback

* test: add test for CloneResolveCheckResponse

* test: add more tests for error around intersection

* test: more test assertions around cloned cached response

* test: add unit test to make sure we cache CycleDetected

* test: add more assertions around CycleDetected field

* chore: rename test names

* chore: fix datastore query count under reporting in intersection reducer
Diff: diff --git a/assets/tests/consolidated_1_1_tests.yaml b/assets/tests/consolidated_1_1_tests.yaml
index b5e2dcd0..d5a2cbf0 100644
--- a/assets/tests/consolidated_1_1_tests.yaml
+++ b/assets/tests/consolidated_1_1_tests.yaml
@@ -5293,5 +5293,366 @@ tests:
               user: user:jon
               type: document
               relation: viewer
             expectation:
               - document:1
+  - name: cycle_or_cycle_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+
+          type document
+            relations
+              define editor: [user, document#viewer]
+              define viewer: [document#editor] or editor
+        tuples:
+          - user: document:1#viewer
+            relation: editor
+            object: document:1
+          - user: document:1#editor
+            relation: viewer
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: immediate_cycle_through_computed_userset
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+
+          type document
+            relations
+              define editor: [user, document#viewer]
+              define viewer: editor
+        tuples:
+          - user: document:1#viewer
+            relation: editor
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: immediate_cycle_through_computed_userset
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+
+          type document
+            relations
+              define editor: [user, document#viewer]
+              define viewer: editor
+        tuples:
+          - user: document:1#viewer
+            relation: editor
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: true_butnot_cycle_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+
+          type document
+            relations
+              define restricted: [user, document#viewer]
+              define viewer: [user] but not restricted
+        tuples:
+          - user: user:jon
+            relation: viewer
+            object: document:1
+          - user: document:1#viewer
+            relation: restricted
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: cycle_and_cycle_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+
+          type user
+
+          type document
+            relations
+              define editor: [user, document#viewer]
+              define viewer: [user, document#editor] and editor
+        tuples:
+          - user: document:1#editor
+            relation: viewer
+            object: document:1
+          - user: document:1#viewer
+            relation: editor
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: cycle_and_true_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+
+          type user
+
+          type document
+            relations
+              define allowed: [user]
+              define viewer: [user, document#viewer] and allowed
+        tuples:
+          - user: user:jon
+            relation: allowed
+            object: document:1
+          - user: document:1#viewer
+            relation: viewer
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: immediate_cycle_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+
+          type user
+
+          type document
+            relations
+              define viewer: [user, document#viewer]
+        tuples:
+          - user: document:1#viewer
+            relation: viewer
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: cycle_butnot_false_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+
+          type user
+
+          type document
+            relations
+              define restricted: [user]
+              define viewer: [user, document#viewer] but not restricted
+        tuples:
+          - user: document:1#viewer
+            relation: viewer
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: false_butnot_cycle_return_false
+    stages:
+      - model: |
+          model
+            schema 1.1
+
+          type user
+
+          type document
+            relations
+              define restricted: [user, document#viewer]
+              define viewer: [user] but not restricted
+        tuples:
+          - user: document:1#viewer
+            relation: restricted
+            object: document:1
+        checkAssertions:
+          - tuple:
+              object: document:1
+              relation: viewer
+              user: user:jon
+            expectation: false
+        listObjectsAssertions:
+          - request:
+              user: user:jon
+              type: document
+              relation: viewer
+            expectation:
+  - name: err_and_err_return_err
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+          type resource
+            relations
+              define a1: a2
+              define a2: a3
+              define a3: a4
+              define a4: a5
+              define a5: a6
+              define a6: a7
+              define a7: a8
+              define a8: a9
+              define a9: a10
+              define a10: a11
+              define a11: a12
+              define a12: a13
+              define a13: a14
+              define a14: a15
+              define a15: a16
+              define a16: a17
+              define a17: a18
+              define a18: a19
+              define a19: a20
+              define a20: a21
+              define a21: a22
+              define a22: a23
+              define a23: a24
+              define a24: a25
+              define a25: a26
+              define a26: [user]
+              define can_view: a1 and a1
+        tuples:
+          - object: resource:abc
+            relation: a26
+            user: user:maria
+        checkAssertions:
+          - tuple:
+              object: resource:abc
+              relation: can_view
+              user: user:maria
+            errorCode: 2002
+        listObjectsAssertions:
+          - request:
+              type: resource
+              relation: can_view
+              user: user:maria
+            errorCode: 2002
+  - name: err_and_true_return_err
+    stages:
+      - model: |
+          model
+            schema 1.1
+          type user
+          type resource
+            relations
+              define a1: a2
+              define a2: a3
+              define a3: a4
+              define a4: a5
+              define a5: a6
+              define a6: a7
+              define a7: a8
+              define a8: a9
+              define a9: a10
+              define a10: a11
+              define a11: a12
+              define a12: a13
+              define a13: a14
+              define a14: a15
+              define a15: a16
+              define a16: a17
+              define a17: a18
+              define a18: a19
+              define a19: a20
+              define a20: a21
+              define a21: a22
+              define a22: a23
+              define a23: a24
+              define a24: a25
+              define a25: a26
+              define a26: [user]
+              define can_view: a1 and a26
+        tuples:
+          - object: resource:abc
+            relation: a26
+            user: user:maria
+        checkAssertions:
+          - tuple:
+              object: resource:abc
+              relation: can_view
+              user: user:maria
+            errorCode: 2002
+        listObjectsAssertions:
+          - request:
+              type: resource
+              relation: can_view
+              user: user:maria
+            errorCode: 2002
\ No newline at end of file
diff --git a/internal/graph/cached_resolver.go b/internal/graph/cached_resolver.go
index 0b5454fb..557a316e 100644
--- a/internal/graph/cached_resolver.go
+++ b/internal/graph/cached_resolver.go
@@ -37,37 +37,15 @@ var (
 		Name:      "check_cache_hit_count",
 		Help:      "The total number of cache hits for ResolveCheck.",
 	})
 )
 
-// CachedResolveCheckResponse is very similar to ResolveCheckResponse except we
-// do not store the ResolutionData. This is due to the fact that the resolution metadata
-// will be incorrect as data is served from cache instead of actual database read.
-type CachedResolveCheckResponse struct {
-	Allowed bool
-}
-
-func (c *CachedResolveCheckResponse) convertToResolveCheckResponse() *ResolveCheckResponse {
-	return &ResolveCheckResponse{
-		Allowed: c.Allowed,
-		ResolutionMetadata: &ResolveCheckResponseMetadata{
-			DatastoreQueryCount: 0,
-		},
-	}
-}
-
-func newCachedResolveCheckResponse(r *ResolveCheckResponse) *CachedResolveCheckResponse {
-	return &CachedResolveCheckResponse{
-		Allowed: r.Allowed,
-	}
-}
-
 // CachedCheckResolver attempts to resolve check sub-problems via prior computations before
 // delegating the request to some underlying CheckResolver.
 type CachedCheckResolver struct {
 	delegate     CheckResolver
-	cache        *ccache.Cache[*CachedResolveCheckResponse]
+	cache        *ccache.Cache[*ResolveCheckResponse]
 	maxCacheSize int64
 	cacheTTL     time.Duration
 	logger       logger.Logger
 	// allocatedCache is used to denote whether the cache is allocated by this struct.
 	// If so, CachedCheckResolver is responsible for cleaning up.
@@ -96,11 +74,11 @@ func WithCacheTTL(ttl time.Duration) CachedCheckResolverOpt {
 }
 
 // WithExistingCache sets the cache to the specified cache.
 // Note that the original cache will not be stopped as it may still be used by others. It is up to the caller
 // to check whether the original cache should be stopped.
-func WithExistingCache(cache *ccache.Cache[*CachedResolveCheckResponse]) CachedCheckResolverOpt {
+func WithExistingCache(cache *ccache.Cache[*ResolveCheckResponse]) CachedCheckResolverOpt {
 	return func(ccr *CachedCheckResolver) {
 		ccr.cache = cache
 	}
 }
 
@@ -129,11 +107,11 @@ func NewCachedCheckResolver(opts ...CachedCheckResolverOpt) *CachedCheckResolver
 	}
 
 	if checker.cache == nil {
 		checker.allocatedCache = true
 		checker.cache = ccache.New(
-			ccache.Configure[*CachedResolveCheckResponse]().MaxSize(checker.maxCacheSize),
+			ccache.Configure[*ResolveCheckResponse]().MaxSize(checker.maxCacheSize),
 		)
 	}
 
 	return checker
 }
@@ -177,21 +155,28 @@ func (c *CachedCheckResolver) ResolveCheck(
 
 	cachedResp := c.cache.Get(cacheKey)
 	if cachedResp != nil && !cachedResp.Expired() {
 		checkCacheHitCounter.Inc()
 		span.SetAttributes(attribute.Bool("is_cached", true))
-		return cachedResp.Value().convertToResolveCheckResponse(), nil
+
+		return cachedResp.Value(), nil
 	}
 	span.SetAttributes(attribute.Bool("is_cached", false))
 
 	resp, err := c.delegate.ResolveCheck(ctx, req)
 	if err != nil {
 		telemetry.TraceError(span, err)
 		return nil, err
 	}
 
-	c.cache.Set(cacheKey, newCachedResolveCheckResponse(resp), c.cacheTTL)
+	// the cached subproblem's resolution metadata doesn't necessarily reflect
+	// the actual number of database reads for the inflight request, so set it
+	// to 0 so it doesn't bias the resolution metadata negatively
+	clonedResp := CloneResolveCheckResponse(resp)
+	clonedResp.ResolutionMetadata.DatastoreQueryCount = 0
+
+	c.cache.Set(cacheKey, clonedResp, c.cacheTTL)
 	return resp, nil
 }
 
 // CheckRequestCacheKey converts the ResolveCheckRequest into a canonical cache key that can be
 // used for Check resolution cache key lookups in a stable way.
diff --git a/internal/graph/cached_resolver_test.go b/internal/graph/cached_resolver_test.go
index 073ba24b..7cdd52dc 100644
--- a/internal/graph/cached_resolver_test.go
+++ b/internal/graph/cached_resolver_test.go
@@ -451,10 +451,43 @@ func TestResolveCheckExpired(t *testing.T) {
 	actualResult, err = dut.ResolveCheck(ctx, req)
 	require.Equal(t, result.Allowed, actualResult.Allowed)
 	require.NoError(t, err)
 }
 
+func TestCachedCheckResolver_CycleDetected(t *testing.T) {
+	cachedCheckResolver := NewCachedCheckResolver()
+	defer cachedCheckResolver.Close()
+
+	mockCtrl := gomock.NewController(t)
+	t.Cleanup(mockCtrl.Finish)
+
+	mockCheckResolver := NewMockCheckResolver(mockCtrl)
+	cachedCheckResolver.SetDelegate(mockCheckResolver)
+
+	mockCheckResolver.EXPECT().
+		ResolveCheck(gomock.Any(), gomock.Any()).
+		Return(&ResolveCheckResponse{
+			Allowed: false,
+			ResolutionMetadata: &ResolveCheckResponseMetadata{
+				DatastoreQueryCount: 1,
+				CycleDetected:       true,
+			},
+		}, nil)
+
+	resp, err := cachedCheckResolver.ResolveCheck(context.Background(), &ResolveCheckRequest{})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, uint32(1), resp.GetResolutionMetadata().DatastoreQueryCount)
+	require.True(t, resp.GetResolutionMetadata().CycleDetected)
+
+	resp, err = cachedCheckResolver.ResolveCheck(context.Background(), &ResolveCheckRequest{})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
+	require.True(t, resp.GetResolutionMetadata().CycleDetected)
+}
+
 func TestCachedCheckDatastoreQueryCount(t *testing.T) {
 	t.Parallel()
 
 	ds := memory.New()
 	defer ds.Close()
@@ -498,11 +531,11 @@ type document
 	)
 
 	ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)
 
 	checkCache := ccache.New(
-		ccache.Configure[*CachedResolveCheckResponse]().MaxSize(100),
+		ccache.Configure[*ResolveCheckResponse]().MaxSize(100),
 	)
 	defer checkCache.Stop()
 
 	cachedCheckResolver := NewCachedCheckResolver(
 		WithExistingCache(checkCache),
diff --git a/internal/graph/check.go b/internal/graph/check.go
index 58a0e0d6..773804a0 100644
--- a/internal/graph/check.go
+++ b/internal/graph/check.go
@@ -24,14 +24,10 @@ import (
 	"github.com/openfga/openfga/pkg/typesystem"
 )
 
 var tracer = otel.Tracer("internal/graph/check")
 
-var (
-	ErrCycleDetected = errors.New("a cycle has been detected")
-)
-
 type ResolveCheckRequest struct {
 	StoreID              string
 	AuthorizationModelID string
 	TupleKey             *openfgav1.TupleKey
 	ContextualTuples     []*openfgav1.TupleKey
@@ -54,15 +50,44 @@ func clone(r *ResolveCheckRequest) *ResolveCheckRequest {
 		},
 		VisitedPaths: maps.Clone(r.VisitedPaths),
 	}
 }
 
+// CloneResolveCheckResponse clones the provided ResolveCheckResponse.
+//
+// If 'r' defines a nil ResolutionMetadata then this function returns
+// an empty value struct for the resolution metadata instead of nil.
+func CloneResolveCheckResponse(r *ResolveCheckResponse) *ResolveCheckResponse {
+	resolutionMetadata := &ResolveCheckResponseMetadata{
+		DatastoreQueryCount: 0,
+		CycleDetected:       false,
+	}
+
+	if r.GetResolutionMetadata() != nil {
+		resolutionMetadata.DatastoreQueryCount = r.GetResolutionMetadata().DatastoreQueryCount
+		resolutionMetadata.CycleDetected = r.GetResolutionMetadata().CycleDetected
+	}
+
+	return &ResolveCheckResponse{
+		Allowed:            r.GetAllowed(),
+		ResolutionMetadata: resolutionMetadata,
+	}
+}
+
 type ResolveCheckResponse struct {
 	Allowed            bool
 	ResolutionMetadata *ResolveCheckResponseMetadata
 }
 
+func (r *ResolveCheckResponse) GetCycleDetected() bool {
+	if r != nil {
+		return r.GetResolutionMetadata().CycleDetected
+	}
+
+	return false
+}
+
 func (r *ResolveCheckResponse) GetAllowed() bool {
 	if r != nil {
 		return r.Allowed
 	}
 
@@ -284,20 +309,23 @@ func union(ctx context.Context, concurrencyLimit uint32, handlers ...CheckHandle
 		close(resultChan)
 	}()
 
 	var dbReads uint32
 	var err error
+	var cycleDetected bool
 	for i := 0; i < len(handlers); i++ {
 		select {
 		case result := <-resultChan:
 			if result.err != nil {
-				if errors.Is(result.err, ErrCycleDetected) {
-					continue
-				}
 				err = result.err
 				continue
 			}
+
+			if result.resp.GetCycleDetected() {
+				cycleDetected = true
+			}
+
 			dbReads += result.resp.GetResolutionMetadata().DatastoreQueryCount
 
 			if result.resp.GetAllowed() {
 				result.resp.GetResolutionMetadata().DatastoreQueryCount = dbReads
 				return result.resp, nil
@@ -305,16 +333,21 @@ func union(ctx context.Context, concurrencyLimit uint32, handlers ...CheckHandle
 		case <-ctx.Done():
 			return nil, ctx.Err()
 		}
 	}
 
+	if err != nil {
+		return nil, err
+	}
+
 	return &ResolveCheckResponse{
 		Allowed: false,
 		ResolutionMetadata: &ResolveCheckResponseMetadata{
 			DatastoreQueryCount: dbReads,
+			CycleDetected:       cycleDetected,
 		},
-	}, err
+	}, nil
 }
 
 // intersection implements a CheckFuncReducer that requires all of the provided CheckHandlerFunc to resolve
 // to an allowed outcome. The first falsey or erroneous outcome causes premature termination of the reducer.
 func intersection(ctx context.Context, concurrencyLimit uint32, handlers ...CheckHandlerFunc) (*ResolveCheckResponse, error) {
@@ -343,41 +376,36 @@ func intersection(ctx context.Context, concurrencyLimit uint32, handlers ...Chec
 	for i := 0; i < len(handlers); i++ {
 		select {
 		case result := <-resultChan:
 			if result.err != nil {
 				span.RecordError(result.err)
-
-				if errors.Is(result.err, ErrCycleDetected) {
-					return &ResolveCheckResponse{
-						Allowed: false,
-						ResolutionMetadata: &ResolveCheckResponseMetadata{
-							DatastoreQueryCount: dbReads,
-						},
-					}, nil
-				}
-
 				err = errors.Join(err, result.err)
 				continue
 			}
 
 			dbReads += result.resp.GetResolutionMetadata().DatastoreQueryCount
 
-			if !result.resp.GetAllowed() {
+			if result.resp.GetCycleDetected() || !result.resp.GetAllowed() {
 				result.resp.GetResolutionMetadata().DatastoreQueryCount = dbReads
 				return result.resp, nil
 			}
 		case <-ctx.Done():
 			return nil, ctx.Err()
 		}
 	}
 
+	// all operands are either truthy or we've seen at least one error
+	if err != nil {
+		return nil, err
+	}
+
 	return &ResolveCheckResponse{
 		Allowed: true,
 		ResolutionMetadata: &ResolveCheckResponseMetadata{
 			DatastoreQueryCount: dbReads,
 		},
-	}, err
+	}, nil
 }
 
 // exclusion implements a CheckFuncReducer that requires a 'base' CheckHandlerFunc to resolve to an allowed
 // outcome and a 'sub' CheckHandlerFunc to resolve to a falsey outcome. The base and sub computations are
 // handled concurrently relative to one another.
@@ -438,60 +466,66 @@ func exclusion(ctx context.Context, concurrencyLimit uint32, handlers ...CheckHa
 	for i := 0; i < len(handlers); i++ {
 		select {
 		case baseResult := <-baseChan:
 			if baseResult.err != nil {
 				span.RecordError(baseResult.err)
-
-				if errors.Is(baseResult.err, ErrCycleDetected) {
-					return &ResolveCheckResponse{
-						Allowed: false,
-						ResolutionMetadata: &ResolveCheckResponseMetadata{
-							DatastoreQueryCount: dbReads,
-						},
-					}, nil
-				}
-
 				baseErr = baseResult.err
 				continue
 			}
 
 			dbReads += baseResult.resp.GetResolutionMetadata().DatastoreQueryCount
 
+			if baseResult.resp.GetCycleDetected() {
+				return &ResolveCheckResponse{
+					Allowed: false,
+					ResolutionMetadata: &ResolveCheckResponseMetadata{
+						DatastoreQueryCount: dbReads,
+						CycleDetected:       true,
+					},
+				}, nil
+			}
+
 			if !baseResult.resp.GetAllowed() {
 				response.GetResolutionMetadata().DatastoreQueryCount = dbReads
 				return response, nil
 			}
 
 		case subResult := <-subChan:
 			if subResult.err != nil {
 				span.RecordError(subResult.err)
-
-				if !errors.Is(subResult.err, ErrCycleDetected) {
-					subErr = subResult.err
-				}
-
+				subErr = subResult.err
 				continue
 			}
 
 			dbReads += subResult.resp.GetResolutionMetadata().DatastoreQueryCount
 
+			if subResult.resp.GetCycleDetected() {
+				return &ResolveCheckResponse{
+					Allowed: false,
+					ResolutionMetadata: &ResolveCheckResponseMetadata{
+						DatastoreQueryCount: dbReads,
+						CycleDetected:       true,
+					},
+				}, nil
+			}
+
 			if subResult.resp.GetAllowed() {
 				response.GetResolutionMetadata().DatastoreQueryCount = dbReads
 				return response, nil
 			}
 		case <-ctx.Done():
 			return nil, ctx.Err()
 		}
 	}
 
-	if baseErr != nil && subErr != nil {
-		return &ResolveCheckResponse{
-			Allowed: false,
-			ResolutionMetadata: &ResolveCheckResponseMetadata{
-				DatastoreQueryCount: 0,
-			},
-		}, errors.Join(baseErr, subErr)
+	// base is either (true) or error, sub is either (false) or error:
+	// true, false - true
+	// true, error - error
+	// error, false - error
+	// error, error - error
+	if baseErr != nil || subErr != nil {
+		return nil, errors.Join(baseErr, subErr)
 	}
 
 	return &ResolveCheckResponse{
 		Allowed: true,
 		ResolutionMetadata: &ResolveCheckResponseMetadata{
@@ -513,11 +547,11 @@ func (c *LocalChecker) dispatch(_ context.Context, parentReq *ResolveCheckReques
 		childRequest.TupleKey = tk
 		childRequest.GetRequestMetadata().Depth--
 
 		resp, err := c.delegate.ResolveCheck(ctx, childRequest)
 		if err != nil {
-			return resp, err
+			return nil, err
 		}
 		return resp, nil
 	}
 }
 
@@ -555,11 +589,11 @@ func (c *LocalChecker) ResolveCheck(
 	rel, err := typesys.GetRelation(objectType, relation)
 	if err != nil {
 		return nil, fmt.Errorf("relation '%s' undefined for object type '%s'", relation, objectType)
 	}
 
-	resp, err := union(ctx, c.concurrencyLimit, c.checkRewrite(ctx, req, rel.GetRewrite()))
+	resp, err := c.checkRewrite(ctx, req, rel.GetRewrite())(ctx)
 	if err != nil {
 		telemetry.TraceError(span, err)
 		return nil, err
 	}
 
@@ -612,11 +646,11 @@ func (c *LocalChecker) checkDirect(parentctx context.Context, req *ResolveCheckR
 			if err != nil {
 				if errors.Is(err, storage.ErrNotFound) {
 					return response, nil
 				}
 
-				return response, err
+				return nil, err
 			}
 
 			// filter out invalid tuples yielded by the database query
 			tupleKey := t.GetKey()
 			err = validation.ValidateTuple(typesys, tupleKey)
@@ -667,11 +701,11 @@ func (c *LocalChecker) checkDirect(parentctx context.Context, req *ResolveCheckR
 				Object:                      reqTupleKey.GetObject(),
 				Relation:                    reqTupleKey.GetRelation(),
 				AllowedUserTypeRestrictions: directlyRelatedUsersetTypes,
 			})
 			if err != nil {
-				return response, err
+				return nil, err
 			}
 			defer iter.Stop()
 
 			// filter out invalid tuples yielded by the database iterator
 			filteredIter := storage.NewFilteredTupleKeyIterator(
@@ -687,11 +721,11 @@ func (c *LocalChecker) checkDirect(parentctx context.Context, req *ResolveCheckR
 				if err != nil {
 					if errors.Is(err, storage.ErrIteratorDone) {
 						break
 					}
 
-					return response, err
+					return nil, err
 				}
 
 				condEvalResult, err := eval.EvaluateTupleCondition(ctx, t, typesys, req.GetContext())
 				if err != nil {
 					errs = multierror.Append(errs, err)
@@ -831,23 +865,17 @@ func (c *LocalChecker) checkTTU(parentctx context.Context, req *ResolveCheckRequ
 		object := tk.GetObject()
 
 		span.SetAttributes(attribute.String("tupleset_relation", fmt.Sprintf("%s#%s", tuple.GetType(object), tuplesetRelation)))
 		span.SetAttributes(attribute.String("computed_relation", computedRelation))
 
-		response := &ResolveCheckResponse{
-			Allowed: false,
-			ResolutionMetadata: &ResolveCheckResponseMetadata{
-				DatastoreQueryCount: req.GetRequestMetadata().DatastoreQueryCount + 1,
-			},
-		}
 		iter, err := ds.Read(
 			ctx,
 			req.GetStoreID(),
 			tuple.NewTupleKey(object, tuplesetRelation, ""),
 		)
 		if err != nil {
-			return response, err
+			return nil, err
 		}
 		defer iter.Stop()
 
 		// filter out invalid tuples yielded by the database iterator
 		filteredIter := storage.NewFilteredTupleKeyIterator(
@@ -863,11 +891,11 @@ func (c *LocalChecker) checkTTU(parentctx context.Context, req *ResolveCheckRequ
 			if err != nil {
 				if err == storage.ErrIteratorDone {
 					break
 				}
 
-				return response, err
+				return nil, err
 			}
 
 			condEvalResult, err := eval.EvaluateTupleCondition(ctx, t, typesys, req.GetContext())
 			if err != nil {
 				errs = multierror.Append(errs, err)
diff --git a/internal/graph/check_test.go b/internal/graph/check_test.go
index c48c6cdd..611bcbe5 100644
--- a/internal/graph/check_test.go
+++ b/internal/graph/check_test.go
@@ -1,9 +1,10 @@
 package graph
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"sync"
 	"testing"
 	"time"
 
@@ -44,11 +45,23 @@ var (
 	depthExceededHandler = func(context.Context) (*ResolveCheckResponse, error) {
 		return nil, ErrResolutionDepthExceeded
 	}
 
 	cyclicErrorHandler = func(context.Context) (*ResolveCheckResponse, error) {
-		return nil, ErrCycleDetected
+		return &ResolveCheckResponse{
+			Allowed: false,
+			ResolutionMetadata: &ResolveCheckResponseMetadata{
+				DatastoreQueryCount: 1,
+				CycleDetected:       true,
+			},
+		}, nil
+	}
+
+	simulatedDBErrorMessage = "simulated db error"
+
+	generalErrorHandler = func(context.Context) (*ResolveCheckResponse, error) {
+		return nil, errors.New(simulatedDBErrorMessage)
 	}
 )
 
 func TestExclusionCheckFuncReducer(t *testing.T) {
 	t.Cleanup(func() {
@@ -75,71 +88,182 @@ func TestExclusionCheckFuncReducer(t *testing.T) {
 		require.NotPanics(t, func() {
 			_, _ = exclusion(ctx, concurrencyLimit, falseHandler, falseHandler)
 		})
 	})
 
-	t.Run("base_handler_is_falsy_return_allowed:false", func(t *testing.T) {
+	t.Run("true_butnot_true_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("true_butnot_false_return_true", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, falseHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.Equal(t, uint32(1+1), resp.GetResolutionMetadata().DatastoreQueryCount)
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_butnot_true_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_butnot_false_return_false", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
 		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("true_butnot_err_return_err", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("true_butnot_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
 	})
 
-	t.Run("base_handler_is_falsy_subtract_handler_with_error_return_allowed:false", func(t *testing.T) {
+	t.Run("true_butnot_cycle_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_butnot_err_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_butnot_errResolutionDepth_return_false", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, depthExceededHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_butnot_cycle_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
 	})
 
-	t.Run("base_handler_with_error_subtract_handler_is_truthy_return_allowed:false", func(t *testing.T) {
+	t.Run("err_butnot_true_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("errResolutionDepth_butnot_true_return_false", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("base_handler_with_error_subtract_handler_with_error_return_error", func(t *testing.T) {
-		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
-		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+	t.Run("cycle_butnot_true_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
 	})
 
-	t.Run("base_handler_with_cycle_subtract_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
+	t.Run("err_butnot_false_return_err", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("errResolutionDepth_butnot_false_return_errResolutionDepth", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("cycle_butnot_false_return_false", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("base_handler_with_cycle_subtract_handler_is_truthy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
-		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
+	t.Run("cycle_butnot_err_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("base_handler_is_falsy_subtract_handler_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
-		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
+	t.Run("cycle_butnot_errResolutionDepth_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("base_handler_is_truthy_subtract_handler_with_cycle_return_allowed:true_with_a_nil_error", func(t *testing.T) {
-		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
+	t.Run("err_butnot_cycle_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
 		require.NoError(t, err)
-		require.True(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("errResolutionDepth_butnot_cycle_return_false", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("base_handler_with_cycle_subtract_handler_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
+	t.Run("cycle_butnot_cycle_return_false", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, cyclicErrorHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+2))
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("err_butnot_err_return_err", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, generalErrorHandler)
+		require.ErrorContains(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("errResolutionDepth_butnot_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
+		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
 	})
 
 	t.Run("aggregate_truthy_and_falsy_handlers_datastore_query_count", func(t *testing.T) {
 		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, trueHandler)
 		require.NoError(t, err)
@@ -174,10 +298,58 @@ func TestExclusionCheckFuncReducer(t *testing.T) {
 		require.False(t, resp.GetAllowed())
 
 		wg.Wait() // just to make sure to avoid test leaks
 	})
 
+	t.Run("return_allowed:false_if_sub_handler_evaluated_before_context_cancelled", func(t *testing.T) {
+		ctx, cancel := context.WithCancel(context.Background())
+		t.Cleanup(cancel)
+
+		var wg sync.WaitGroup
+
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			time.Sleep(10 * time.Millisecond)
+			cancel()
+		}()
+
+		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, trueHandler)
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+
+		wg.Wait() // just to make sure to avoid test leaks
+	})
+
+	t.Run("return_allowed:false_if_sub_handler_evaluated_before_base_cancelled", func(t *testing.T) {
+		ctx, cancel := context.WithCancel(context.Background())
+		t.Cleanup(cancel)
+
+		var wg sync.WaitGroup
+
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
+			time.Sleep(50 * time.Millisecond)
+
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
+		}
+
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			time.Sleep(10 * time.Millisecond)
+			cancel()
+		}()
+
+		resp, err := exclusion(ctx, concurrencyLimit, slowTrueHandler, trueHandler)
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+
+		wg.Wait() // just to make sure to avoid test leaks
+	})
+
 	t.Run("return_allowed:false_if_subtract_handler_evaluated_before_context_cancelled", func(t *testing.T) {
 		ctx, cancel := context.WithCancel(context.Background())
 		t.Cleanup(cancel)
 
 		var wg sync.WaitGroup
@@ -198,18 +370,27 @@ func TestExclusionCheckFuncReducer(t *testing.T) {
 
 	t.Run("return_error_if_context_deadline_before_resolution", func(t *testing.T) {
 		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
 		t.Cleanup(cancel)
 
-		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(50 * time.Millisecond)
-			return nil, nil
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
 		}
 
-		resp, err := exclusion(ctx, concurrencyLimit, slowHandler, slowHandler)
+		slowFalseHandler := func(context.Context) (*ResolveCheckResponse, error) {
+			time.Sleep(50 * time.Millisecond)
+			return &ResolveCheckResponse{
+				Allowed: false,
+			}, nil
+		}
+
+		resp, err := exclusion(ctx, concurrencyLimit, slowTrueHandler, slowFalseHandler)
 		require.ErrorIs(t, err, context.DeadlineExceeded)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 	})
 
 	t.Run("return_error_if_context_cancelled_before_resolution", func(t *testing.T) {
 		ctx, cancel := context.WithCancel(context.Background())
 		t.Cleanup(cancel)
@@ -223,16 +404,16 @@ func TestExclusionCheckFuncReducer(t *testing.T) {
 			cancel()
 		}()
 
 		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(50 * time.Millisecond)
-			return nil, nil
+			return &ResolveCheckResponse{}, nil
 		}
 
 		resp, err := exclusion(ctx, concurrencyLimit, slowHandler, slowHandler)
 		require.ErrorIs(t, err, context.Canceled)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 
 		wg.Wait() // just to make sure to avoid test leaks
 	})
 }
 
@@ -243,92 +424,252 @@ func TestIntersectionCheckFuncReducer(t *testing.T) {
 
 	ctx := context.Background()
 
 	concurrencyLimit := uint32(10)
 
-	t.Run("no_handlers_return_allowed:false", func(t *testing.T) {
+	t.Run("no_handlers_return_false", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 		require.NotNil(t, resp.GetResolutionMetadata())
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("one_handler_is_falsy_return_allowed:false", func(t *testing.T) {
+	t.Run("false_return_false", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit, falseHandler)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 		require.Equal(t, uint32(1), resp.GetResolutionMetadata().DatastoreQueryCount)
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("true_and_true_return_true", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, trueHandler)
+		require.NoError(t, err)
+		require.True(t, resp.GetAllowed())
+		require.Equal(t, uint32(2), resp.GetResolutionMetadata().DatastoreQueryCount)
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("one_handler_is_truthy_and_others_are_falsy_return_allowed:false", func(t *testing.T) {
-		resp, err := intersection(ctx, concurrencyLimit, trueHandler, falseHandler, falseHandler)
+	t.Run("true_and_false_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, falseHandler)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2))
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("all_handlers_are_falsy_return_allowed:false", func(t *testing.T) {
-		resp, err := intersection(ctx, concurrencyLimit, falseHandler, falseHandler, falseHandler)
+	t.Run("false_and_true_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, falseHandler, trueHandler)
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2))
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_and_false_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, falseHandler, falseHandler)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 		require.Equal(t, uint32(1), resp.GetResolutionMetadata().DatastoreQueryCount)
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("all_handlers_with_error_return_error", func(t *testing.T) {
-		_, err := intersection(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
+	t.Run("true_and_err_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("true_and_errResolutionDepth_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
 		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("true_and_cycle_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_and_err_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_and_errResolutionDepth_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, falseHandler, depthExceededHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_and_cycle_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+	})
+
+	t.Run("err_and_true_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
 	})
 
-	t.Run("one_handler_returns_error_but_other_handler_is_truthy_return_error_and_allowed:false", func(t *testing.T) {
-		_, err := intersection(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
+	t.Run("errResolutionDepth_and_true_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
 		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
 	})
 
-	t.Run("one_handler_returns_error_but_other_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
+	t.Run("cycle_and_true_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("err_and_false_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("errResolutionDepth_and_false_return_false", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("one_handler_errors_with_cycle_but_other_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
+	t.Run("cycle_and_false_return_false", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
 	})
 
-	t.Run("one_handler_errors_with_cycle_but_other_handler_is_truthy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
-		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
+	t.Run("cycle_and_err_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("cycle_and_errResolutionDepth_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("err_and_cycle_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("errResolutionDepth_and_cycle_return_false", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1))
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("both_handlers_errors_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
+	t.Run("cycle_and_cycle_return_false", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, cyclicErrorHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
-		require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
+		require.True(t, resp.GetCycleDetected())
+	})
+
+	t.Run("err_and_err_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, generalErrorHandler)
+		require.ErrorContains(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("errResolutionDepth_and_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("true_and_cycle_and_err_return_err", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler, generalErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
 	})
 
 	t.Run("aggregate_truthy_and_falsy_handlers_datastore_query_count", func(t *testing.T) {
 		resp, err := intersection(ctx, concurrencyLimit, falseHandler, trueHandler)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
 	})
 
+	t.Run("cycle_and_false_reports_correct_datastore_query_count", func(t *testing.T) {
+		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, falseHandler)
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
+	})
+
 	t.Run("return_allowed:false_if_falsy_handler_evaluated_before_context_deadline", func(t *testing.T) {
 		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
 		t.Cleanup(cancel)
 
 		resp, err := intersection(ctx, concurrencyLimit, falseHandler)
 		require.NoError(t, err)
 		require.False(t, resp.GetAllowed())
 	})
 
+	t.Run("return_true_if_truthy_handler_evaluated_before_context_deadline", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
+		t.Cleanup(cancel)
+
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler)
+		require.NoError(t, err)
+		require.True(t, resp.GetAllowed())
+	})
+
+	t.Run("return_error_if_context_deadline_before_truthy_handler", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
+		t.Cleanup(cancel)
+
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
+			time.Sleep(50 * time.Millisecond)
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
+		}
+
+		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
+		require.ErrorIs(t, err, context.DeadlineExceeded)
+		require.Nil(t, resp)
+	})
+
 	t.Run("return_allowed:false_if_falsy_handler_evaluated_before_context_cancelled", func(t *testing.T) {
 		ctx, cancel := context.WithCancel(context.Background())
 		t.Cleanup(cancel)
 
 		var wg sync.WaitGroup
@@ -349,18 +690,20 @@ func TestIntersectionCheckFuncReducer(t *testing.T) {
 
 	t.Run("return_error_if_context_deadline_before_resolution", func(t *testing.T) {
 		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
 		t.Cleanup(cancel)
 
-		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(50 * time.Millisecond)
-			return nil, nil
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
 		}
 
-		resp, err := intersection(ctx, concurrencyLimit, slowHandler)
+		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
 		require.ErrorIs(t, err, context.DeadlineExceeded)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 	})
 
 	t.Run("return_error_if_context_cancelled_before_resolution", func(t *testing.T) {
 		ctx, cancel := context.WithCancel(context.Background())
 		t.Cleanup(cancel)
@@ -372,23 +715,109 @@ func TestIntersectionCheckFuncReducer(t *testing.T) {
 			defer wg.Done()
 			time.Sleep(10 * time.Millisecond)
 			cancel()
 		}()
 
-		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(50 * time.Millisecond)
-			return nil, nil
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
 		}
 
-		resp, err := intersection(ctx, concurrencyLimit, slowHandler)
+		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
 		require.ErrorIs(t, err, context.Canceled)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 
 		wg.Wait() // just to make sure to avoid test leaks
 	})
 }
 
+func TestNonStratifiableCheckQueries(t *testing.T) {
+	t.Run("example_1", func(t *testing.T) {
+		ds := memory.New()
+
+		storeID := ulid.Make().String()
+
+		err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
+			tuple.NewTupleKey("document:1", "viewer", "user:jon"),
+			tuple.NewTupleKey("document:1", "restricted", "document:1#viewer"),
+		})
+		require.NoError(t, err)
+
+		checker := NewLocalCheckerWithCycleDetection()
+		t.Cleanup(checker.Close)
+
+		model := testutils.MustTransformDSLToProtoWithID(`model
+	schema 1.1
+	type user
+
+
+	type document
+	  relations
+		define viewer: [user] but not restricted
+		define restricted: [user, document#viewer]`)
+
+		ctx := typesystem.ContextWithTypesystem(
+			context.Background(),
+			typesystem.New(model),
+		)
+
+		ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)
+
+		resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
+			StoreID:         storeID,
+			TupleKey:        tuple.NewTupleKey("document:1", "viewer", "user:jon"),
+			RequestMetadata: NewCheckRequestMetadata(10),
+		})
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+	})
+
+	t.Run("example_2", func(t *testing.T) {
+		ds := memory.New()
+
+		storeID := ulid.Make().String()
+
+		err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
+			tuple.NewTupleKey("document:1", "viewer", "user:jon"),
+			tuple.NewTupleKey("document:1", "restrictedb", "document:1#viewer"),
+		})
+		require.NoError(t, err)
+
+		checker := NewLocalCheckerWithCycleDetection()
+		t.Cleanup(checker.Close)
+
+		model := testutils.MustTransformDSLToProtoWithID(`model
+	schema 1.1
+	type user
+
+
+	type document
+	  relations
+		define viewer: [user] but not restricteda
+		define restricteda: restrictedb
+		define restrictedb: [user, document#viewer]
+	`)
+
+		ctx := typesystem.ContextWithTypesystem(
+			context.Background(),
+			typesystem.New(model),
+		)
+
+		ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)
+
+		resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
+			StoreID:         storeID,
+			TupleKey:        tuple.NewTupleKey("document:1", "viewer", "user:jon"),
+			RequestMetadata: NewCheckRequestMetadata(10),
+		})
+		require.NoError(t, err)
+		require.False(t, resp.GetAllowed())
+	})
+}
+
 func TestResolveCheckDeterministic(t *testing.T) {
 	t.Run("resolution_depth_resolves_deterministically", func(t *testing.T) {
 		t.Parallel()
 
 		ds := memory.New()
@@ -857,105 +1286,10 @@ type document
 			}
 		})
 	}
 }
 
-// TestCheckWithUnexpectedCycle tests the LocalChecker to make sure that if a model includes a cycle
-// that should have otherwise been invalid according to the typesystem, then the check resolution will
-// consider the cycle a falsey allowed result and not bubble-up a cycle detected error.
-func TestCheckWithUnexpectedCycle(t *testing.T) {
-	ds := memory.New()
-	defer ds.Close()
-
-	storeID := ulid.Make().String()
-
-	err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
-		tuple.NewTupleKey("resource:1", "parent", "resource:1"),
-	})
-	require.NoError(t, err)
-
-	tests := []struct {
-		name     string
-		model    string
-		tupleKey *openfgav1.TupleKey
-	}{
-		{
-			name: "test_1",
-			model: `model
-	schema 1.1
-type user
-
-type resource
-  relations
-	define x: [user] but not y
-	define y: [user] but not z
-	define z: [user] or x`,
-			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
-		},
-		{
-			name: "test_2",
-			model: `model
-	schema 1.1
-type user
-
-type resource
-  relations
-	define x: [user] and y
-	define y: [user] and z
-	define z: [user] or x`,
-			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
-		},
-		{
-			name: "test_3",
-			model: `model
-	schema 1.1
-type resource
-  relations
-	define x: y
-	define y: x`,
-			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
-		},
-		{
-			name: "test_4",
-			model: `model
-	schema 1.1
-type resource
-  relations
-	define parent: [resource]
-	define x: [user] or x from parent`,
-			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
-		},
-	}
-
-	checker := NewLocalCheckerWithCycleDetection()
-	t.Cleanup(checker.Close)
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			model := testutils.MustTransformDSLToProtoWithID(test.model)
-
-			ctx := typesystem.ContextWithTypesystem(
-				context.Background(),
-				typesystem.New(model),
-			)
-			ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)
-
-			resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
-				StoreID:         storeID,
-				TupleKey:        test.tupleKey,
-				RequestMetadata: NewCheckRequestMetadata(25),
-			})
-
-			require.NoError(t, err)
-			require.False(t, resp.GetAllowed())
-
-			require.GreaterOrEqual(t, resp.ResolutionMetadata.DatastoreQueryCount, uint32(0)) // min of 0 (x) if x is cycle. TODO: accurately report datastore query count of cycle branches
-			require.LessOrEqual(t, resp.ResolutionMetadata.DatastoreQueryCount, uint32(3))    // max of 3 (x, y, z) before the cycle
-		})
-	}
-}
-
 func TestCheckConditions(t *testing.T) {
 	ds := memory.New()
 
 	storeID := ulid.Make().String()
 
@@ -1120,13 +1454,13 @@ type doc
 	t.Run("dispatch_count_multiple_direct_userset_lookups", func(t *testing.T) {
 		storeID := ulid.Make().String()
 
 		model := parser.MustTransformDSLToProto(`model
 	  schema 1.1
-	
+
 	type user
-	
+
 	type group
 	  relations
 	    define member: [user, group#member]
 
 	type document
@@ -1185,11 +1519,11 @@ type doc
 
 		model := parser.MustTransformDSLToProto(`model
 		schema 1.1
 
 		type user
-  	
+
 		type document
 			relations
 		   		define owner: [user]
 		   		define editor: [user] or owner`)
 
@@ -1268,79 +1602,181 @@ func TestUnionCheckFuncReducer(t *testing.T) {
 			Allowed:            true,
 			ResolutionMetadata: &ResolveCheckResponseMetadata{},
 		}, nil
 	}
 
-	t.Run("if_no_handlers_return_allowed_false", func(t *testing.T) {
+	t.Run("no_handlers_return_allowed_false", func(t *testing.T) {
 		resp, err := union(ctx, concurrencyLimit)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_one_handler_is_truthy_and_others_are_falsey_return_allowed_true", func(t *testing.T) {
-		resp, err := union(ctx, concurrencyLimit, trueHandler, falseHandler, falseHandler)
+	t.Run("true_or_true_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, trueHandler, trueHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_all_handlers_are_falsey_return_allowed_false", func(t *testing.T) {
-		resp, err := union(ctx, concurrencyLimit, falseHandler, falseHandler, falseHandler)
+	t.Run("true_or_false_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, trueHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_or_true_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("false_or_false_return_false", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, falseHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_a_handler_errors_but_other_handler_are_truthy_return_allowed_true", func(t *testing.T) {
-		depthExceededHandler := func(context.Context) (*ResolveCheckResponse, error) {
-			return nil, ErrResolutionDepthExceeded
-		}
+	t.Run("true_or_err_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
 
-		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
+	t.Run("true_or_errResolutionDepth_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_a_handler_errors_but_other_handler_is_falsey_return_error_and_allowed_false", func(t *testing.T) {
-		depthExceededHandler := func(context.Context) (*ResolveCheckResponse, error) {
-			return nil, ErrResolutionDepthExceeded
-		}
+	t.Run("true_or_cycle_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
 
-		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
-		require.ErrorIs(t, ErrResolutionDepthExceeded, err)
+	t.Run("false_or_err_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("false_or_errResolutionDepth_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("false_or_cycle_return_false", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_a_handler_errors_with_cycle_but_other_handler_is_falsey_return_allowed_false_with_a_nil_error", func(t *testing.T) {
-		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
-			return nil, ErrCycleDetected
-		}
+	t.Run("err_or_true_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("cycle_or_true_return_true", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
+		require.True(t, resp.GetAllowed())
+		require.False(t, resp.GetCycleDetected())
+	})
+
+	t.Run("err_or_false_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("errResolutionDepth_or_false_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
 
+	t.Run("cycle_or_false_return_false", func(t *testing.T) {
 		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, falseHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("both_handlers_with_cycle_return_allowed_false_with_nil_error", func(t *testing.T) {
-		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
-			return nil, ErrCycleDetected
-		}
+	t.Run("cycle_or_err_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("cycle_or_errResolutionDepth_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("err_or_cycle_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
+
+	t.Run("errResolutionDepth_or_cycle_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
 
+	t.Run("cycle_or_cycle_return_false", func(t *testing.T) {
 		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, cyclicErrorHandler)
 		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
 	})
 
-	t.Run("if_a_handler_errors_with_cycle_but_other_handler_is_truthy_return_allowed_true_with_a_nil_error", func(t *testing.T) {
-		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
-			return nil, ErrCycleDetected
-		}
+	t.Run("false_or_cycle_or_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler, generalErrorHandler)
+		require.EqualError(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
+	})
 
-		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
-		require.NoError(t, err)
-		require.True(t, resp.GetAllowed())
+	t.Run("err_or_err_return_err", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, generalErrorHandler, generalErrorHandler)
+		require.ErrorContains(t, err, simulatedDBErrorMessage)
+		require.Nil(t, resp)
 	})
 
-	t.Run("should_aggregate_DatastoreQueryCount_of_non_error handlers", func(t *testing.T) {
+	t.Run("errResolutionDepth_or_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
+		resp, err := union(ctx, concurrencyLimit, falseHandler, depthExceededHandler, depthExceededHandler)
+		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
+		require.Nil(t, resp)
+	})
+
+	t.Run("should_aggregate_DatastoreQueryCount_of_non_error_handlers", func(t *testing.T) {
 		trueHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(5 * time.Millisecond) // forces `trueHandler` to be resolved after `falseHandler`
 			return &ResolveCheckResponse{
 				Allowed: true,
 				ResolutionMetadata: &ResolveCheckResponseMetadata{
@@ -1400,16 +1836,16 @@ func TestUnionCheckFuncReducer(t *testing.T) {
 		ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
 		t.Cleanup(cancel)
 
 		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
 			time.Sleep(50 * time.Millisecond)
-			return nil, nil
+			return &ResolveCheckResponse{}, nil
 		}
 
 		resp, err := union(ctx, concurrencyLimit, slowHandler, falseHandler)
 		require.ErrorIs(t, err, context.DeadlineExceeded)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 	})
 
 	t.Run("should_handle_context_timeouts_even_with_eventual_truthy_handler", func(t *testing.T) {
 		ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
 		t.Cleanup(cancel)
@@ -1421,8 +1857,105 @@ func TestUnionCheckFuncReducer(t *testing.T) {
 			}, nil
 		}
 
 		resp, err := union(ctx, concurrencyLimit, trueHandler, falseHandler)
 		require.ErrorIs(t, err, context.DeadlineExceeded)
-		require.False(t, resp.GetAllowed())
+		require.Nil(t, resp)
 	})
+
+	t.Run("should_return_true_with_slow_falsey_handler", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
+		t.Cleanup(cancel)
+
+		falseSlowHandler := func(context.Context) (*ResolveCheckResponse, error) {
+			time.Sleep(25 * time.Millisecond)
+			return &ResolveCheckResponse{
+				Allowed: false,
+			}, nil
+		}
+
+		resp, err := union(ctx, concurrencyLimit, trueHandler, falseSlowHandler)
+		require.NoError(t, err)
+		require.True(t, resp.GetAllowed())
+	})
+
+	t.Run("return_error_if_context_cancelled_before_resolution", func(t *testing.T) {
+		ctx, cancel := context.WithCancel(context.Background())
+		t.Cleanup(cancel)
+
+		var wg sync.WaitGroup
+
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			time.Sleep(10 * time.Millisecond)
+			cancel()
+		}()
+
+		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
+			time.Sleep(50 * time.Millisecond)
+			return &ResolveCheckResponse{
+				Allowed: true,
+			}, nil
+		}
+
+		resp, err := union(ctx, concurrencyLimit, slowTrueHandler)
+		require.ErrorIs(t, err, context.Canceled)
+		require.Nil(t, resp)
+
+		wg.Wait() // just to make sure to avoid test leaks
+	})
+
+	t.Run("return_allowed:true_if_truthy_handler_evaluated_before_context_cancelled", func(t *testing.T) {
+		ctx, cancel := context.WithCancel(context.Background())
+		t.Cleanup(cancel)
+
+		var wg sync.WaitGroup
+
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			time.Sleep(10 * time.Millisecond)
+			cancel()
+		}()
+
+		resp, err := intersection(ctx, concurrencyLimit, trueHandler)
+		require.NoError(t, err)
+		require.True(t, resp.GetAllowed())
+
+		wg.Wait() // just to make sure to avoid test leaks
+	})
+}
+
+func TestCloneResolveCheckResponse(t *testing.T) {
+	resp1 := &ResolveCheckResponse{
+		Allowed: true,
+		ResolutionMetadata: &ResolveCheckResponseMetadata{
+			DatastoreQueryCount: 1,
+			CycleDetected:       false,
+		},
+	}
+	clonedResp1 := CloneResolveCheckResponse(resp1)
+
+	require.Equal(t, resp1, clonedResp1)
+	require.NotSame(t, resp1, clonedResp1)
+
+	// mutate the clone and ensure the original reference is
+	// unchanged
+	clonedResp1.Allowed = false
+	clonedResp1.ResolutionMetadata.DatastoreQueryCount = 2
+	clonedResp1.ResolutionMetadata.CycleDetected = true
+	require.True(t, resp1.GetAllowed())
+	require.Equal(t, uint32(1), resp1.GetResolutionMetadata().DatastoreQueryCount)
+	require.False(t, resp1.GetResolutionMetadata().CycleDetected)
+
+	resp2 := &ResolveCheckResponse{
+		Allowed: true,
+	}
+	clonedResp2 := CloneResolveCheckResponse(resp2)
+
+	require.NotSame(t, resp2, clonedResp2)
+	require.Equal(t, resp2.GetAllowed(), clonedResp2.GetAllowed())
+	require.NotNil(t, clonedResp2.ResolutionMetadata)
+	require.Equal(t, uint32(0), clonedResp2.GetResolutionMetadata().DatastoreQueryCount)
+	require.False(t, clonedResp2.GetResolutionMetadata().CycleDetected)
 }
diff --git a/internal/graph/cycle_check_resolver.go b/internal/graph/cycle_check_resolver.go
index d27dcfd5..8ac41699 100644
--- a/internal/graph/cycle_check_resolver.go
+++ b/internal/graph/cycle_check_resolver.go
@@ -41,11 +41,16 @@ func (c *CycleDetectionCheckResolver) ResolveCheck(
 	}
 
 	_, cycleDetected := req.VisitedPaths[key]
 	span.SetAttributes(attribute.Bool("cycle_detected", cycleDetected))
 	if cycleDetected {
-		return nil, ErrCycleDetected
+		return &ResolveCheckResponse{
+			Allowed: false,
+			ResolutionMetadata: &ResolveCheckResponseMetadata{
+				CycleDetected: true,
+			},
+		}, nil
 	}
 
 	req.VisitedPaths[key] = struct{}{}
 
 	return c.delegate.ResolveCheck(ctx, &ResolveCheckRequest{
diff --git a/internal/graph/cycle_check_resolver_test.go b/internal/graph/cycle_check_resolver_test.go
index 178b7da0..64f88613 100644
--- a/internal/graph/cycle_check_resolver_test.go
+++ b/internal/graph/cycle_check_resolver_test.go
@@ -40,12 +40,15 @@ func TestCycleDetectionCheckResolver(t *testing.T) {
 			TupleKey:        cyclicalTuple,
 			RequestMetadata: NewCheckRequestMetadata(defaultResolveNodeLimit),
 			VisitedPaths:    visitedPaths,
 		})
 
-		require.ErrorIs(t, err, ErrCycleDetected)
+		require.NoError(t, err)
+		require.NotNil(t, resp)
 		require.False(t, resp.GetAllowed())
+		require.True(t, resp.GetCycleDetected())
+		require.NotNil(t, resp.ResolutionMetadata)
 	})
 
 	t.Run("no_cycle_detected_delegates_request", func(t *testing.T) {
 		ctrl := gomock.NewController(t)
 		t.Cleanup(ctrl.Finish)
@@ -117,7 +120,8 @@ func TestIntegrationWithLocalChecker(t *testing.T) {
 		AuthorizationModelID: model.GetId(),
 		TupleKey:             tuple.NewTupleKey("group:1", "blocked", "user:jon"),
 		RequestMetadata:      NewCheckRequestMetadata(25),
 	})
 	require.NoError(t, err)
-	require.True(t, resp.GetAllowed())
+	require.NotNil(t, resp)
+	require.False(t, resp.GetAllowed())
 }
diff --git a/internal/graph/graph.go b/internal/graph/graph.go
index 9efe7d50..355f5bd6 100644
--- a/internal/graph/graph.go
+++ b/internal/graph/graph.go
@@ -71,10 +71,14 @@ type ResolveCheckResponseMetadata struct {
 	// Thinking of a Check as a tree of evaluations,
 	// If the solution is "allowed=true", one path was found. This is the value in the leaf node of that path, plus the sum of the paths that were
 	// evaluated and potentially discarded
 	// If the solution is "allowed=false", no paths were found. This is the sum of all the reads in all the paths that had to be evaluated
 	DatastoreQueryCount uint32
+
+	// Indicates if the ResolveCheck subproblem that was evaluated involved
+	// a cycle in the evaluation.
+	CycleDetected bool
 }
 
 type RelationshipEdgeType int
 
 const (
diff --git a/pkg/server/commands/list_objects.go b/pkg/server/commands/list_objects.go
index dfb52cbc..a7eb597d 100644
--- a/pkg/server/commands/list_objects.go
+++ b/pkg/server/commands/list_objects.go
@@ -331,11 +331,11 @@ func (q *ListObjectsQuery) evaluate(
 						ContextualTuples:     req.GetContextualTuples().GetTupleKeys(),
 						Context:              req.GetContext(),
 						RequestMetadata:      checkRequestMetadata,
 					})
 					if err != nil {
-						if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
+						if errors.Is(err, graph.ErrResolutionDepthExceeded) {
 							resultsChan <- ListObjectsResult{Err: serverErrors.AuthorizationModelResolutionTooComplex}
 							return
 						}
 
 						resultsChan <- ListObjectsResult{Err: err}
@@ -348,11 +348,11 @@ func (q *ListObjectsQuery) evaluate(
 						trySendObject(res.Object, &objectsFound, maxResults, resultsChan)
 					}
 				}(res)
 
 			case err := <-errChan:
-				if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
+				if errors.Is(err, graph.ErrResolutionDepthExceeded) {
 					err = serverErrors.AuthorizationModelResolutionTooComplex
 				}
 
 				resultsChan <- ListObjectsResult{Err: err}
 				break ConsumerReadLoop
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 256f980e..5b209eca 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -772,11 +772,11 @@ func (s *Server) Check(ctx context.Context, req *openfgav1.CheckRequest) (*openf
 		Context:              req.GetContext(),
 		RequestMetadata:      checkRequestMetadata,
 	})
 	if err != nil {
 		telemetry.TraceError(span, err)
-		if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
+		if errors.Is(err, graph.ErrResolutionDepthExceeded) {
 			return nil, serverErrors.AuthorizationModelResolutionTooComplex
 		}
 
 		if errors.Is(err, condition.ErrEvaluationFailed) {
 			return nil, serverErrors.ValidationError(err)
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 245e9929..44672f86 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -312,11 +312,11 @@ func TestAvoidDeadlockAcrossCheckRequests(t *testing.T) {
 	})
 	require.NoError(t, err)
 
 	var wg sync.WaitGroup
 
-	wg.Add(2)
+	wg.Add(3)
 
 	var resp1 *openfgav1.CheckResponse
 	var err1 error
 	go func() {
 		defer wg.Done()
@@ -338,17 +338,34 @@ func TestAvoidDeadlockAcrossCheckRequests(t *testing.T) {
 			AuthorizationModelId: modelID,
 			TupleKey:             tuple.NewCheckRequestTupleKey("document:1", "viewer", "user:jon"),
 		})
 	}()
 
+	var resp3 *openfgav1.CheckResponse
+	var err3 error
+	go func() {
+		defer wg.Done()
+
+		resp3, err3 = s.Check(context.Background(), &openfgav1.CheckRequest{
+			StoreId:              storeID,
+			AuthorizationModelId: modelID,
+			TupleKey:             tuple.NewCheckRequestTupleKey("document:1", "viewer", "user:andres"),
+		})
+	}()
+
 	wg.Wait()
 
 	require.NoError(t, err1)
+	require.NotNil(t, resp1)
 	require.False(t, resp1.GetAllowed())
 
 	require.NoError(t, err2)
-	require.False(t, resp2.GetAllowed())
+	require.NotNil(t, resp2)
+
+	require.NoError(t, err3)
+	require.NotNil(t, resp3)
+	require.True(t, resp3.GetAllowed())
 }
 
 func TestAvoidDeadlockWithinSingleCheckRequest(t *testing.T) {
 	t.Cleanup(func() {
 		goleak.VerifyNone(t)
@@ -403,11 +420,13 @@ func TestAvoidDeadlockWithinSingleCheckRequest(t *testing.T) {
 	resp, err := s.Check(context.Background(), &openfgav1.CheckRequest{
 		StoreId:              storeID,
 		AuthorizationModelId: modelID,
 		TupleKey:             tuple.NewCheckRequestTupleKey("document:1", "can_view", "user:jon"),
 	})
+
 	require.NoError(t, err)
+	require.NotNil(t, resp)
 	require.False(t, resp.GetAllowed())
 }
 
 func TestThreeProngThroughVariousLayers(t *testing.T) {
 	t.Cleanup(func() {

Vulnerable code: // CachedResolveCheckResponse is very similar to ResolveCheckResponse except we
// do not store the ResolutionData. This is due to the fact that the resolution metadata
// will be incorrect as data is served from cache instead of actual database read.
type CachedResolveCheckResponse struct {
	Allowed bool
}

func (c *CachedResolveCheckResponse) convertToResolveCheckResponse() *ResolveCheckResponse {
	return &ResolveCheckResponse{
		Allowed: c.Allowed,
		ResolutionMetadata: &ResolveCheckResponseMetadata{
			DatastoreQueryCount: 0,
		},
	}
}

func newCachedResolveCheckResponse(r *ResolveCheckResponse) *CachedResolveCheckResponse {
	return &CachedResolveCheckResponse{
		Allowed: r.Allowed,
	}
}

	cache        *ccache.Cache[*CachedResolveCheckResponse]
func WithExistingCache(cache *ccache.Cache[*CachedResolveCheckResponse]) CachedCheckResolverOpt {
			ccache.Configure[*CachedResolveCheckResponse]().MaxSize(checker.maxCacheSize),
		return cachedResp.Value().convertToResolveCheckResponse(), nil
	c.cache.Set(cacheKey, newCachedResolveCheckResponse(resp), c.cacheTTL)
		ccache.Configure[*CachedResolveCheckResponse]().MaxSize(100),
var (
	ErrCycleDetected = errors.New("a cycle has been detected")
)

				if errors.Is(result.err, ErrCycleDetected) {
					continue
				}
	}, err

				if errors.Is(result.err, ErrCycleDetected) {
					return &ResolveCheckResponse{
						Allowed: false,
						ResolutionMetadata: &ResolveCheckResponseMetadata{
							DatastoreQueryCount: dbReads,
						},
					}, nil
				}

			if !result.resp.GetAllowed() {
	}, err

				if errors.Is(baseResult.err, ErrCycleDetected) {
					return &ResolveCheckResponse{
						Allowed: false,
						ResolutionMetadata: &ResolveCheckResponseMetadata{
							DatastoreQueryCount: dbReads,
						},
					}, nil
				}


				if !errors.Is(subResult.err, ErrCycleDetected) {
					subErr = subResult.err
				}

	if baseErr != nil && subErr != nil {
		return &ResolveCheckResponse{
			Allowed: false,
			ResolutionMetadata: &ResolveCheckResponseMetadata{
				DatastoreQueryCount: 0,
			},
		}, errors.Join(baseErr, subErr)
			return resp, err
	resp, err := union(ctx, c.concurrencyLimit, c.checkRewrite(ctx, req, rel.GetRewrite()))
				return response, err
				return response, err
					return response, err
		response := &ResolveCheckResponse{
			Allowed: false,
			ResolutionMetadata: &ResolveCheckResponseMetadata{
				DatastoreQueryCount: req.GetRequestMetadata().DatastoreQueryCount + 1,
			},
		}
			return response, err
				return response, err
		return nil, ErrCycleDetected
	t.Run("base_handler_is_falsy_return_allowed:false", func(t *testing.T) {
	t.Run("base_handler_is_falsy_subtract_handler_with_error_return_allowed:false", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
	t.Run("base_handler_with_error_subtract_handler_is_truthy_return_allowed:false", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
	t.Run("base_handler_with_error_subtract_handler_with_error_return_error", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
	t.Run("base_handler_with_cycle_subtract_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
	t.Run("base_handler_with_cycle_subtract_handler_is_truthy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+1))
	t.Run("base_handler_is_falsy_subtract_handler_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
	t.Run("base_handler_is_truthy_subtract_handler_with_cycle_return_allowed:true_with_a_nil_error", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
		require.True(t, resp.GetAllowed())
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
	t.Run("base_handler_with_cycle_subtract_handler_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2+2))
		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, nil
		resp, err := exclusion(ctx, concurrencyLimit, slowHandler, slowHandler)
		require.False(t, resp.GetAllowed())
			return nil, nil
		require.False(t, resp.GetAllowed())
	t.Run("no_handlers_return_allowed:false", func(t *testing.T) {
	t.Run("one_handler_is_falsy_return_allowed:false", func(t *testing.T) {
	t.Run("one_handler_is_truthy_and_others_are_falsy_return_allowed:false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, falseHandler, falseHandler)
	t.Run("all_handlers_are_falsy_return_allowed:false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, falseHandler, falseHandler)
	t.Run("all_handlers_with_error_return_error", func(t *testing.T) {
		_, err := intersection(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
	t.Run("one_handler_returns_error_but_other_handler_is_truthy_return_error_and_allowed:false", func(t *testing.T) {
		_, err := intersection(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
	t.Run("one_handler_returns_error_but_other_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
	t.Run("one_handler_errors_with_cycle_but_other_handler_is_falsy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+2))
	t.Run("one_handler_errors_with_cycle_but_other_handler_is_truthy_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1))
	t.Run("both_handlers_errors_with_cycle_return_allowed:false_with_a_nil_error", func(t *testing.T) {
		require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, nil
		resp, err := intersection(ctx, concurrencyLimit, slowHandler)
		require.False(t, resp.GetAllowed())
		slowHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, nil
		resp, err := intersection(ctx, concurrencyLimit, slowHandler)
		require.False(t, resp.GetAllowed())
// TestCheckWithUnexpectedCycle tests the LocalChecker to make sure that if a model includes a cycle
// that should have otherwise been invalid according to the typesystem, then the check resolution will
// consider the cycle a falsey allowed result and not bubble-up a cycle detected error.
func TestCheckWithUnexpectedCycle(t *testing.T) {
	ds := memory.New()
	defer ds.Close()

	storeID := ulid.Make().String()

	err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
		tuple.NewTupleKey("resource:1", "parent", "resource:1"),
	})
	require.NoError(t, err)

	tests := []struct {
		name     string
		model    string
		tupleKey *openfgav1.TupleKey
	}{
		{
			name: "test_1",
			model: `model
	schema 1.1
type user

type resource
  relations
	define x: [user] but not y
	define y: [user] but not z
	define z: [user] or x`,
			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
		},
		{
			name: "test_2",
			model: `model
	schema 1.1
type user

type resource
  relations
	define x: [user] and y
	define y: [user] and z
	define z: [user] or x`,
			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
		},
		{
			name: "test_3",
			model: `model
	schema 1.1
type resource
  relations
	define x: y
	define y: x`,
			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
		},
		{
			name: "test_4",
			model: `model
	schema 1.1
type resource
  relations
	define parent: [resource]
	define x: [user] or x from parent`,
			tupleKey: tuple.NewTupleKey("resource:1", "x", "user:jon"),
		},
	}

	checker := NewLocalCheckerWithCycleDetection()
	t.Cleanup(checker.Close)

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			model := testutils.MustTransformDSLToProtoWithID(test.model)

			ctx := typesystem.ContextWithTypesystem(
				context.Background(),
				typesystem.New(model),
			)
			ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)

			resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
				StoreID:         storeID,
				TupleKey:        test.tupleKey,
				RequestMetadata: NewCheckRequestMetadata(25),
			})

			require.NoError(t, err)
			require.False(t, resp.GetAllowed())

			require.GreaterOrEqual(t, resp.ResolutionMetadata.DatastoreQueryCount, uint32(0)) // min of 0 (x) if x is cycle. TODO: accurately report datastore query count of cycle branches
			require.LessOrEqual(t, resp.ResolutionMetadata.DatastoreQueryCount, uint32(3))    // max of 3 (x, y, z) before the cycle
		})
	}
}

	
	
  	
	t.Run("if_no_handlers_return_allowed_false", func(t *testing.T) {
	t.Run("if_one_handler_is_truthy_and_others_are_falsey_return_allowed_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, falseHandler, falseHandler)
	t.Run("if_all_handlers_are_falsey_return_allowed_false", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, falseHandler, falseHandler)
	t.Run("if_a_handler_errors_but_other_handler_are_truthy_return_allowed_true", func(t *testing.T) {
		depthExceededHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, ErrResolutionDepthExceeded
		}
		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
	t.Run("if_a_handler_errors_but_other_handler_is_falsey_return_error_and_allowed_false", func(t *testing.T) {
		depthExceededHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, ErrResolutionDepthExceeded
		}
		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
		require.ErrorIs(t, ErrResolutionDepthExceeded, err)
	t.Run("if_a_handler_errors_with_cycle_but_other_handler_is_falsey_return_allowed_false_with_a_nil_error", func(t *testing.T) {
		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, ErrCycleDetected
		}
	t.Run("both_handlers_with_cycle_return_allowed_false_with_nil_error", func(t *testing.T) {
		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, ErrCycleDetected
		}
	t.Run("if_a_handler_errors_with_cycle_but_other_handler_is_truthy_return_allowed_true_with_a_nil_error", func(t *testing.T) {
		cyclicErrorHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return nil, ErrCycleDetected
		}
		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.NoError(t, err)
		require.True(t, resp.GetAllowed())
	t.Run("should_aggregate_DatastoreQueryCount_of_non_error handlers", func(t *testing.T) {
			return nil, nil
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetAllowed())
		return nil, ErrCycleDetected
		require.ErrorIs(t, err, ErrCycleDetected)
	require.True(t, resp.GetAllowed())
						if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
				if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
		if errors.Is(err, graph.ErrResolutionDepthExceeded) || errors.Is(err, graph.ErrCycleDetected) {
	wg.Add(2)
	require.False(t, resp2.GetAllowed())
Fixed code:   - name: cycle_or_cycle_return_false
    stages:
      - model: |
          model
            schema 1.1
          type user

          type document
            relations
              define editor: [user, document#viewer]
              define viewer: [document#editor] or editor
        tuples:
          - user: document:1#viewer
            relation: editor
            object: document:1
          - user: document:1#editor
            relation: viewer
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: immediate_cycle_through_computed_userset
    stages:
      - model: |
          model
            schema 1.1
          type user

          type document
            relations
              define editor: [user, document#viewer]
              define viewer: editor
        tuples:
          - user: document:1#viewer
            relation: editor
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: immediate_cycle_through_computed_userset
    stages:
      - model: |
          model
            schema 1.1
          type user

          type document
            relations
              define editor: [user, document#viewer]
              define viewer: editor
        tuples:
          - user: document:1#viewer
            relation: editor
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: true_butnot_cycle_return_false
    stages:
      - model: |
          model
            schema 1.1
          type user

          type document
            relations
              define restricted: [user, document#viewer]
              define viewer: [user] but not restricted
        tuples:
          - user: user:jon
            relation: viewer
            object: document:1
          - user: document:1#viewer
            relation: restricted
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: cycle_and_cycle_return_false
    stages:
      - model: |
          model
            schema 1.1

          type user

          type document
            relations
              define editor: [user, document#viewer]
              define viewer: [user, document#editor] and editor
        tuples:
          - user: document:1#editor
            relation: viewer
            object: document:1
          - user: document:1#viewer
            relation: editor
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: cycle_and_true_return_false
    stages:
      - model: |
          model
            schema 1.1

          type user

          type document
            relations
              define allowed: [user]
              define viewer: [user, document#viewer] and allowed
        tuples:
          - user: user:jon
            relation: allowed
            object: document:1
          - user: document:1#viewer
            relation: viewer
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: immediate_cycle_return_false
    stages:
      - model: |
          model
            schema 1.1

          type user

          type document
            relations
              define viewer: [user, document#viewer]
        tuples:
          - user: document:1#viewer
            relation: viewer
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: cycle_butnot_false_return_false
    stages:
      - model: |
          model
            schema 1.1

          type user

          type document
            relations
              define restricted: [user]
              define viewer: [user, document#viewer] but not restricted
        tuples:
          - user: document:1#viewer
            relation: viewer
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: false_butnot_cycle_return_false
    stages:
      - model: |
          model
            schema 1.1

          type user

          type document
            relations
              define restricted: [user, document#viewer]
              define viewer: [user] but not restricted
        tuples:
          - user: document:1#viewer
            relation: restricted
            object: document:1
        checkAssertions:
          - tuple:
              object: document:1
              relation: viewer
              user: user:jon
            expectation: false
        listObjectsAssertions:
          - request:
              user: user:jon
              type: document
              relation: viewer
            expectation:
  - name: err_and_err_return_err
    stages:
      - model: |
          model
            schema 1.1
          type user
          type resource
            relations
              define a1: a2
              define a2: a3
              define a3: a4
              define a4: a5
              define a5: a6
              define a6: a7
              define a7: a8
              define a8: a9
              define a9: a10
              define a10: a11
              define a11: a12
              define a12: a13
              define a13: a14
              define a14: a15
              define a15: a16
              define a16: a17
              define a17: a18
              define a18: a19
              define a19: a20
              define a20: a21
              define a21: a22
              define a22: a23
              define a23: a24
              define a24: a25
              define a25: a26
              define a26: [user]
              define can_view: a1 and a1
        tuples:
          - object: resource:abc
            relation: a26
            user: user:maria
        checkAssertions:
          - tuple:
              object: resource:abc
              relation: can_view
              user: user:maria
            errorCode: 2002
        listObjectsAssertions:
          - request:
              type: resource
              relation: can_view
              user: user:maria
            errorCode: 2002
  - name: err_and_true_return_err
    stages:
      - model: |
          model
            schema 1.1
          type user
          type resource
            relations
              define a1: a2
              define a2: a3
              define a3: a4
              define a4: a5
              define a5: a6
              define a6: a7
              define a7: a8
              define a8: a9
              define a9: a10
              define a10: a11
              define a11: a12
              define a12: a13
              define a13: a14
              define a14: a15
              define a15: a16
              define a16: a17
              define a17: a18
              define a18: a19
              define a19: a20
              define a20: a21
              define a21: a22
              define a22: a23
              define a23: a24
              define a24: a25
              define a25: a26
              define a26: [user]
              define can_view: a1 and a26
        tuples:
          - object: resource:abc
            relation: a26
            user: user:maria
        checkAssertions:
          - tuple:
              object: resource:abc
              relation: can_view
              user: user:maria
            errorCode: 2002
        listObjectsAssertions:
          - request:
              type: resource
              relation: can_view
              user: user:maria
            errorCode: 2002
	cache        *ccache.Cache[*ResolveCheckResponse]
func WithExistingCache(cache *ccache.Cache[*ResolveCheckResponse]) CachedCheckResolverOpt {
			ccache.Configure[*ResolveCheckResponse]().MaxSize(checker.maxCacheSize),

		return cachedResp.Value(), nil
	// the cached subproblem's resolution metadata doesn't necessarily reflect
	// the actual number of database reads for the inflight request, so set it
	// to 0 so it doesn't bias the resolution metadata negatively
	clonedResp := CloneResolveCheckResponse(resp)
	clonedResp.ResolutionMetadata.DatastoreQueryCount = 0

	c.cache.Set(cacheKey, clonedResp, c.cacheTTL)
func TestCachedCheckResolver_CycleDetected(t *testing.T) {
	cachedCheckResolver := NewCachedCheckResolver()
	defer cachedCheckResolver.Close()

	mockCtrl := gomock.NewController(t)
	t.Cleanup(mockCtrl.Finish)

	mockCheckResolver := NewMockCheckResolver(mockCtrl)
	cachedCheckResolver.SetDelegate(mockCheckResolver)

	mockCheckResolver.EXPECT().
		ResolveCheck(gomock.Any(), gomock.Any()).
		Return(&ResolveCheckResponse{
			Allowed: false,
			ResolutionMetadata: &ResolveCheckResponseMetadata{
				DatastoreQueryCount: 1,
				CycleDetected:       true,
			},
		}, nil)

	resp, err := cachedCheckResolver.ResolveCheck(context.Background(), &ResolveCheckRequest{})
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.Equal(t, uint32(1), resp.GetResolutionMetadata().DatastoreQueryCount)
	require.True(t, resp.GetResolutionMetadata().CycleDetected)

	resp, err = cachedCheckResolver.ResolveCheck(context.Background(), &ResolveCheckRequest{})
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.Equal(t, uint32(0), resp.GetResolutionMetadata().DatastoreQueryCount)
	require.True(t, resp.GetResolutionMetadata().CycleDetected)
}

		ccache.Configure[*ResolveCheckResponse]().MaxSize(100),
// CloneResolveCheckResponse clones the provided ResolveCheckResponse.
//
// If 'r' defines a nil ResolutionMetadata then this function returns
// an empty value struct for the resolution metadata instead of nil.
func CloneResolveCheckResponse(r *ResolveCheckResponse) *ResolveCheckResponse {
	resolutionMetadata := &ResolveCheckResponseMetadata{
		DatastoreQueryCount: 0,
		CycleDetected:       false,
	}

	if r.GetResolutionMetadata() != nil {
		resolutionMetadata.DatastoreQueryCount = r.GetResolutionMetadata().DatastoreQueryCount
		resolutionMetadata.CycleDetected = r.GetResolutionMetadata().CycleDetected
	}

	return &ResolveCheckResponse{
		Allowed:            r.GetAllowed(),
		ResolutionMetadata: resolutionMetadata,
	}
}

func (r *ResolveCheckResponse) GetCycleDetected() bool {
	if r != nil {
		return r.GetResolutionMetadata().CycleDetected
	}

	return false
}

	var cycleDetected bool

			if result.resp.GetCycleDetected() {
				cycleDetected = true
			}

	if err != nil {
		return nil, err
	}

			CycleDetected:       cycleDetected,
	}, nil
			if result.resp.GetCycleDetected() || !result.resp.GetAllowed() {
	// all operands are either truthy or we've seen at least one error
	if err != nil {
		return nil, err
	}

	}, nil
			if baseResult.resp.GetCycleDetected() {
				return &ResolveCheckResponse{
					Allowed: false,
					ResolutionMetadata: &ResolveCheckResponseMetadata{
						DatastoreQueryCount: dbReads,
						CycleDetected:       true,
					},
				}, nil
			}

				subErr = subResult.err
			if subResult.resp.GetCycleDetected() {
				return &ResolveCheckResponse{
					Allowed: false,
					ResolutionMetadata: &ResolveCheckResponseMetadata{
						DatastoreQueryCount: dbReads,
						CycleDetected:       true,
					},
				}, nil
			}

	// base is either (true) or error, sub is either (false) or error:
	// true, false - true
	// true, error - error
	// error, false - error
	// error, error - error
	if baseErr != nil || subErr != nil {
		return nil, errors.Join(baseErr, subErr)
			return nil, err
	resp, err := c.checkRewrite(ctx, req, rel.GetRewrite())(ctx)
				return nil, err
				return nil, err
					return nil, err
			return nil, err
				return nil, err
	"errors"
		return &ResolveCheckResponse{
			Allowed: false,
			ResolutionMetadata: &ResolveCheckResponseMetadata{
				DatastoreQueryCount: 1,
				CycleDetected:       true,
			},
		}, nil
	}

	simulatedDBErrorMessage = "simulated db error"

	generalErrorHandler = func(context.Context) (*ResolveCheckResponse, error) {
		return nil, errors.New(simulatedDBErrorMessage)
	t.Run("true_butnot_true_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("true_butnot_false_return_true", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, falseHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.Equal(t, uint32(1+1), resp.GetResolutionMetadata().DatastoreQueryCount)
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_butnot_true_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_butnot_false_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("true_butnot_err_return_err", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("true_butnot_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	t.Run("true_butnot_cycle_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("false_butnot_err_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_butnot_errResolutionDepth_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_butnot_cycle_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
	t.Run("err_butnot_true_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("errResolutionDepth_butnot_true_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("cycle_butnot_true_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
	t.Run("err_butnot_false_return_err", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("errResolutionDepth_butnot_false_return_errResolutionDepth", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})

	t.Run("cycle_butnot_false_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_butnot_err_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_butnot_errResolutionDepth_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("err_butnot_cycle_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("errResolutionDepth_butnot_cycle_return_false", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_butnot_cycle_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("err_butnot_err_return_err", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, generalErrorHandler, generalErrorHandler)
		require.ErrorContains(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("errResolutionDepth_butnot_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
		resp, err := exclusion(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	t.Run("return_allowed:false_if_sub_handler_evaluated_before_context_cancelled", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		t.Cleanup(cancel)

		var wg sync.WaitGroup

		wg.Add(1)
		go func() {
			defer wg.Done()
			time.Sleep(10 * time.Millisecond)
			cancel()
		}()

		resp, err := exclusion(ctx, concurrencyLimit, trueHandler, trueHandler)
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())

		wg.Wait() // just to make sure to avoid test leaks
	})

	t.Run("return_allowed:false_if_sub_handler_evaluated_before_base_cancelled", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		t.Cleanup(cancel)

		var wg sync.WaitGroup

		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			time.Sleep(50 * time.Millisecond)

			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		}

		wg.Add(1)
		go func() {
			defer wg.Done()
			time.Sleep(10 * time.Millisecond)
			cancel()
		}()

		resp, err := exclusion(ctx, concurrencyLimit, slowTrueHandler, trueHandler)
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())

		wg.Wait() // just to make sure to avoid test leaks
	})

		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		slowFalseHandler := func(context.Context) (*ResolveCheckResponse, error) {
			time.Sleep(50 * time.Millisecond)
			return &ResolveCheckResponse{
				Allowed: false,
			}, nil
		}

		resp, err := exclusion(ctx, concurrencyLimit, slowTrueHandler, slowFalseHandler)
		require.Nil(t, resp)
			return &ResolveCheckResponse{}, nil
		require.Nil(t, resp)
	t.Run("no_handlers_return_false", func(t *testing.T) {
		require.False(t, resp.GetCycleDetected())
	t.Run("false_return_false", func(t *testing.T) {
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("true_and_true_return_true", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, trueHandler)
		require.NoError(t, err)
		require.True(t, resp.GetAllowed())
		require.Equal(t, uint32(2), resp.GetResolutionMetadata().DatastoreQueryCount)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_and_false_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, falseHandler)
		require.False(t, resp.GetCycleDetected())
	t.Run("false_and_true_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, trueHandler)
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(2))
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_and_false_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, falseHandler)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_and_err_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("true_and_errResolutionDepth_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
		require.Nil(t, resp)
	})

	t.Run("true_and_cycle_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("false_and_err_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_and_errResolutionDepth_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, depthExceededHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_and_cycle_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
	})

	t.Run("err_and_true_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	t.Run("errResolutionDepth_and_true_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, trueHandler)
		require.Nil(t, resp)
	t.Run("cycle_and_true_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("err_and_false_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("errResolutionDepth_and_false_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("cycle_and_false_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
	t.Run("cycle_and_err_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("cycle_and_errResolutionDepth_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("err_and_cycle_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("errResolutionDepth_and_cycle_return_false", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_and_cycle_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	})

	t.Run("err_and_err_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, generalErrorHandler, generalErrorHandler)
		require.ErrorContains(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("errResolutionDepth_and_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, depthExceededHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})

	t.Run("true_and_cycle_and_err_return_err", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler, generalErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetAllowed())
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_and_false_reports_correct_datastore_query_count", func(t *testing.T) {
		resp, err := intersection(ctx, concurrencyLimit, cyclicErrorHandler, falseHandler)
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())
		require.LessOrEqual(t, resp.GetResolutionMetadata().DatastoreQueryCount, uint32(1+1))
	})

	t.Run("return_true_if_truthy_handler_evaluated_before_context_deadline", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
		t.Cleanup(cancel)

		resp, err := intersection(ctx, concurrencyLimit, trueHandler)
		require.NoError(t, err)
		require.True(t, resp.GetAllowed())
	})

	t.Run("return_error_if_context_deadline_before_truthy_handler", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
		t.Cleanup(cancel)

		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			time.Sleep(50 * time.Millisecond)
			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		}

		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
		require.ErrorIs(t, err, context.DeadlineExceeded)
		require.Nil(t, resp)
	})

		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
		require.Nil(t, resp)
		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		resp, err := intersection(ctx, concurrencyLimit, slowTrueHandler)
		require.Nil(t, resp)
func TestNonStratifiableCheckQueries(t *testing.T) {
	t.Run("example_1", func(t *testing.T) {
		ds := memory.New()

		storeID := ulid.Make().String()

		err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
			tuple.NewTupleKey("document:1", "viewer", "user:jon"),
			tuple.NewTupleKey("document:1", "restricted", "document:1#viewer"),
		})
		require.NoError(t, err)

		checker := NewLocalCheckerWithCycleDetection()
		t.Cleanup(checker.Close)

		model := testutils.MustTransformDSLToProtoWithID(`model
	schema 1.1
	type user


	type document
	  relations
		define viewer: [user] but not restricted
		define restricted: [user, document#viewer]`)

		ctx := typesystem.ContextWithTypesystem(
			context.Background(),
			typesystem.New(model),
		)

		ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)

		resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
			StoreID:         storeID,
			TupleKey:        tuple.NewTupleKey("document:1", "viewer", "user:jon"),
			RequestMetadata: NewCheckRequestMetadata(10),
		})
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())
	})

	t.Run("example_2", func(t *testing.T) {
		ds := memory.New()

		storeID := ulid.Make().String()

		err := ds.Write(context.Background(), storeID, nil, []*openfgav1.TupleKey{
			tuple.NewTupleKey("document:1", "viewer", "user:jon"),
			tuple.NewTupleKey("document:1", "restrictedb", "document:1#viewer"),
		})
		require.NoError(t, err)

		checker := NewLocalCheckerWithCycleDetection()
		t.Cleanup(checker.Close)

		model := testutils.MustTransformDSLToProtoWithID(`model
	schema 1.1
	type user


	type document
	  relations
		define viewer: [user] but not restricteda
		define restricteda: restrictedb
		define restrictedb: [user, document#viewer]
	`)

		ctx := typesystem.ContextWithTypesystem(
			context.Background(),
			typesystem.New(model),
		)

		ctx = storage.ContextWithRelationshipTupleReader(ctx, ds)

		resp, err := checker.ResolveCheck(ctx, &ResolveCheckRequest{
			StoreID:         storeID,
			TupleKey:        tuple.NewTupleKey("document:1", "viewer", "user:jon"),
			RequestMetadata: NewCheckRequestMetadata(10),
		})
		require.NoError(t, err)
		require.False(t, resp.GetAllowed())
	})
}




	t.Run("no_handlers_return_allowed_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_or_true_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, trueHandler)
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_or_false_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, falseHandler)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_or_true_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("false_or_false_return_false", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, falseHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_or_err_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, generalErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})
	t.Run("true_or_errResolutionDepth_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, depthExceededHandler)
		require.NotNil(t, resp)
		require.False(t, resp.GetCycleDetected())
	t.Run("true_or_cycle_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, trueHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})
	t.Run("false_or_err_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, generalErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("false_or_errResolutionDepth_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})

	t.Run("false_or_cycle_return_false", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("err_or_true_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("cycle_or_true_return_true", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, trueHandler)
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetAllowed())
		require.False(t, resp.GetCycleDetected())
	})

	t.Run("err_or_false_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, falseHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("errResolutionDepth_or_false_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, falseHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})
	t.Run("cycle_or_false_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("cycle_or_err_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, generalErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("cycle_or_errResolutionDepth_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, cyclicErrorHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})

	t.Run("err_or_cycle_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, generalErrorHandler, cyclicErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})

	t.Run("errResolutionDepth_or_cycle_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, depthExceededHandler, cyclicErrorHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})
	t.Run("cycle_or_cycle_return_false", func(t *testing.T) {
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
	t.Run("false_or_cycle_or_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, cyclicErrorHandler, generalErrorHandler)
		require.EqualError(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	})
	t.Run("err_or_err_return_err", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, generalErrorHandler, generalErrorHandler)
		require.ErrorContains(t, err, simulatedDBErrorMessage)
		require.Nil(t, resp)
	t.Run("errResolutionDepth_or_errResolutionDepth_return_errResolutionDepth", func(t *testing.T) {
		resp, err := union(ctx, concurrencyLimit, falseHandler, depthExceededHandler, depthExceededHandler)
		require.ErrorIs(t, err, ErrResolutionDepthExceeded)
		require.Nil(t, resp)
	})

	t.Run("should_aggregate_DatastoreQueryCount_of_non_error_handlers", func(t *testing.T) {
			return &ResolveCheckResponse{}, nil
		require.Nil(t, resp)
		require.Nil(t, resp)

	t.Run("should_return_true_with_slow_falsey_handler", func(t *testing.T) {
		ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
		t.Cleanup(cancel)

		falseSlowHandler := func(context.Context) (*ResolveCheckResponse, error) {
			time.Sleep(25 * time.Millisecond)
			return &ResolveCheckResponse{
				Allowed: false,
			}, nil
		}

		resp, err := union(ctx, concurrencyLimit, trueHandler, falseSlowHandler)
		require.NoError(t, err)
		require.True(t, resp.GetAllowed())
	})

	t.Run("return_error_if_context_cancelled_before_resolution", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		t.Cleanup(cancel)

		var wg sync.WaitGroup

		wg.Add(1)
		go func() {
			defer wg.Done()
			time.Sleep(10 * time.Millisecond)
			cancel()
		}()

		slowTrueHandler := func(context.Context) (*ResolveCheckResponse, error) {
			time.Sleep(50 * time.Millisecond)
			return &ResolveCheckResponse{
				Allowed: true,
			}, nil
		}

		resp, err := union(ctx, concurrencyLimit, slowTrueHandler)
		require.ErrorIs(t, err, context.Canceled)
		require.Nil(t, resp)

		wg.Wait() // just to make sure to avoid test leaks
	})

	t.Run("return_allowed:true_if_truthy_handler_evaluated_before_context_cancelled", func(t *testing.T) {
		ctx, cancel := context.WithCancel(context.Background())
		t.Cleanup(cancel)

		var wg sync.WaitGroup

		wg.Add(1)
		go func() {
			defer wg.Done()
			time.Sleep(10 * time.Millisecond)
			cancel()
		}()

		resp, err := intersection(ctx, concurrencyLimit, trueHandler)
		require.NoError(t, err)
		require.True(t, resp.GetAllowed())

		wg.Wait() // just to make sure to avoid test leaks
	})
}

func TestCloneResolveCheckResponse(t *testing.T) {
	resp1 := &ResolveCheckResponse{
		Allowed: true,
		ResolutionMetadata: &ResolveCheckResponseMetadata{
			DatastoreQueryCount: 1,
			CycleDetected:       false,
		},
	}
	clonedResp1 := CloneResolveCheckResponse(resp1)

	require.Equal(t, resp1, clonedResp1)
	require.NotSame(t, resp1, clonedResp1)

	// mutate the clone and ensure the original reference is
	// unchanged
	clonedResp1.Allowed = false
	clonedResp1.ResolutionMetadata.DatastoreQueryCount = 2
	clonedResp1.ResolutionMetadata.CycleDetected = true
	require.True(t, resp1.GetAllowed())
	require.Equal(t, uint32(1), resp1.GetResolutionMetadata().DatastoreQueryCount)
	require.False(t, resp1.GetResolutionMetadata().CycleDetected)

	resp2 := &ResolveCheckResponse{
		Allowed: true,
	}
	clonedResp2 := CloneResolveCheckResponse(resp2)

	require.NotSame(t, resp2, clonedResp2)
	require.Equal(t, resp2.GetAllowed(), clonedResp2.GetAllowed())
	require.NotNil(t, clonedResp2.ResolutionMetadata)
	require.Equal(t, uint32(0), clonedResp2.GetResolutionMetadata().DatastoreQueryCount)
	require.False(t, clonedResp2.GetResolutionMetadata().CycleDetected)
		return &ResolveCheckResponse{
			Allowed: false,
			ResolutionMetadata: &ResolveCheckResponseMetadata{
				CycleDetected: true,
			},
		}, nil
		require.NoError(t, err)
		require.NotNil(t, resp)
		require.True(t, resp.GetCycleDetected())
		require.NotNil(t, resp.ResolutionMetadata)
	require.NotNil(t, resp)
	require.False(t, resp.GetAllowed())

	// Indicates if the ResolveCheck subproblem that was evaluated involved
	// a cycle in the evaluation.
	CycleDetected bool
						if errors.Is(err, graph.ErrResolutionDepthExceeded) {
				if errors.Is(err, graph.ErrResolutionDepthExceeded) {
		if errors.Is(err, graph.ErrResolutionDepthExceeded) {
	wg.Add(3)
	var resp3 *openfgav1.CheckResponse
	var err3 error
	go func() {
		defer wg.Done()

		resp3, err3 = s.Check(context.Background(), &openfgav1.CheckRequest{
			StoreId:              storeID,
			AuthorizationModelId: modelID,
			TupleKey:             tuple.NewCheckRequestTupleKey("document:1", "viewer", "user:andres"),
		})
	}()

	require.NotNil(t, resp1)
	require.NotNil(t, resp2)

	require.NoError(t, err3)
	require.NotNil(t, resp3)
	require.True(t, resp3.GetAllowed())

	require.NotNil(t, resp)