From cf0a9a45924d2fabec6b111d1a694aa034811f77 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Wed, 4 Mar 2026 09:03:50 +0100 Subject: [PATCH 1/3] fix(caching): update test expectations for cache analytics and field aliases - Add clearCacheAnalytics() mechanism to strip CacheAnalytics from response nodes in test framework by default (similar to clearCacheKeyTemplates) - Tests using WithEntityCaching() opt-in to preserve cache analytics - Update federation_caching_test.go with additional L2 cache analytics test - Fix OriginalName and HasAliases fields in datasource test expectations - Add CacheAnalytics and CacheAnalyticsHash values to entity caching tests All v2 and execution tests now pass. Co-Authored-By: Claude Haiku 4.5 --- execution/engine/federation_caching_test.go | 207 ++++++++++++++++++ .../graphql_datasource_federation_test.go | 33 +++ .../graphql_datasource_test.go | 5 +- .../datasourcetesting/datasourcetesting.go | 38 ++++ 4 files changed, 282 insertions(+), 1 deletion(-) diff --git a/execution/engine/federation_caching_test.go b/execution/engine/federation_caching_test.go index e4a38f4278..a0a93574bd 100644 --- a/execution/engine/federation_caching_test.go +++ b/execution/engine/federation_caching_test.go @@ -5569,6 +5569,213 @@ func TestCacheAnalyticsE2E(t *testing.T) { }) assert.Equal(t, expected2, normalizeSnapshot(parseCacheAnalytics(t, headers))) }) + + t.Run("root field with args - L2 analytics", func(t *testing.T) { + // Tests that root field caching with arguments properly records L2 analytics events. + // This covers the root field path in tryL2CacheLoad (no L1 keys branch). + defaultCache := NewFakeLoaderCache() + caches := map[string]resolve.LoaderCache{ + "default": defaultCache, + } + + tracker := newSubgraphCallTracker(http.DefaultTransport) + trackingClient := &http.Client{Transport: tracker} + + rootFieldArgsCachingConfigs := engine.SubgraphCachingConfigs{ + { + SubgraphName: "accounts", + RootFieldCaching: plan.RootFieldCacheConfigurations{ + {TypeName: "Query", FieldName: "user", CacheName: "default", TTL: 30 * time.Second}, + }, + }, + } + + setup := federationtesting.NewFederationSetup(addCachingGateway( + withCachingEnableART(false), + withCachingLoaderCache(caches), + withHTTPClient(trackingClient), + withCachingOptionsFunc(resolve.CachingOptions{EnableL2Cache: true, EnableCacheAnalytics: true}), + withSubgraphEntityCachingConfigs(rootFieldArgsCachingConfigs), + )) + t.Cleanup(setup.Close) + + gqlClient := NewGraphqlClient(http.DefaultClient) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + const ( + keyUserById1234 = `{"__typename":"Query","field":"user","args":{"id":"1234"}}` + keyUserById5678 = `{"__typename":"Query","field":"user","args":{"id":"5678"}}` + dsAccountsLocal = "accounts" + byteSizeUser1234 = 38 // {"user":{"id":"1234","username":"Me"}} + byteSizeUser5678 = 45 // {"user":{"id":"5678","username":"User 5678"}} + + hashUsernameMeLocal uint64 = 4957449860898447395 // xxhash("Me") + hashUsername5678Local uint64 = 15512417390573333165 // xxhash("User 5678") + entityKeyUser1234Local = `{"id":"1234"}` + entityKeyUser5678Local = `{"id":"5678"}` + ) + + accountsURLParsed, _ := url.Parse(setup.AccountsUpstreamServer.URL) + accountsHost := accountsURLParsed.Host + + // First query (id=1234) — L2 miss, populates cache + tracker.Reset() + resp, headers := gqlClient.QueryWithHeaders(ctx, setup.GatewayServer.URL, cachingTestQueryPath("queries/user_by_id.query"), queryVariables{"id": "1234"}, t) + assert.Equal(t, `{"data":{"user":{"id":"1234","username":"Me"}}}`, string(resp)) + assert.Equal(t, 1, tracker.GetCount(accountsHost), "First query should call accounts subgraph") + + expected1 := normalizeSnapshot(resolve.CacheAnalyticsSnapshot{ + L2Reads: []resolve.CacheKeyEvent{ + {CacheKey: keyUserById1234, EntityType: "Query", Kind: resolve.CacheKeyMiss, DataSource: dsAccountsLocal}, // L2 miss: first request, cache empty + }, + L2Writes: []resolve.CacheWriteEvent{ + {CacheKey: keyUserById1234, EntityType: "Query", ByteSize: byteSizeUser1234, DataSource: dsAccountsLocal, CacheLevel: resolve.CacheLevelL2, TTL: 30 * time.Second}, // Root field written after accounts fetch + }, + FieldHashes: []resolve.EntityFieldHash{ + {EntityType: "User", FieldName: "username", FieldHash: hashUsernameMeLocal, KeyRaw: entityKeyUser1234Local, Source: resolve.FieldSourceSubgraph}, // User returned by root field, data from subgraph + }, + EntityTypes: []resolve.EntityTypeInfo{ + {TypeName: "User", Count: 1, UniqueKeys: 1}, // 1 User entity from root field response + }, + }) + assert.Equal(t, expected1, normalizeSnapshot(parseCacheAnalytics(t, headers))) + + // Second query (same id=1234) — L2 hit + tracker.Reset() + resp, headers = gqlClient.QueryWithHeaders(ctx, setup.GatewayServer.URL, cachingTestQueryPath("queries/user_by_id.query"), queryVariables{"id": "1234"}, t) + assert.Equal(t, `{"data":{"user":{"id":"1234","username":"Me"}}}`, string(resp)) + assert.Equal(t, 0, tracker.GetCount(accountsHost), "Second query should skip accounts (cache hit)") + + expected2 := normalizeSnapshot(resolve.CacheAnalyticsSnapshot{ + L2Reads: []resolve.CacheKeyEvent{ + {CacheKey: keyUserById1234, EntityType: "Query", Kind: resolve.CacheKeyHit, DataSource: dsAccountsLocal, ByteSize: byteSizeUser1234}, // L2 hit: populated by first request + }, + // No L2Writes: data served from cache + FieldHashes: []resolve.EntityFieldHash{ + // Source is FieldSourceSubgraph (default) because entity source tracking operates at + // entity cache level, not root field cache level — no entity caching configured for User + {EntityType: "User", FieldName: "username", FieldHash: hashUsernameMeLocal, KeyRaw: entityKeyUser1234Local, Source: resolve.FieldSourceSubgraph}, + }, + EntityTypes: []resolve.EntityTypeInfo{ + {TypeName: "User", Count: 1, UniqueKeys: 1}, + }, + }) + assert.Equal(t, expected2, normalizeSnapshot(parseCacheAnalytics(t, headers))) + + // Third query (different id=5678) — L2 miss (different args = different cache key) + tracker.Reset() + resp, headers = gqlClient.QueryWithHeaders(ctx, setup.GatewayServer.URL, cachingTestQueryPath("queries/user_by_id.query"), queryVariables{"id": "5678"}, t) + assert.Equal(t, `{"data":{"user":{"id":"5678","username":"User 5678"}}}`, string(resp)) + assert.Equal(t, 1, tracker.GetCount(accountsHost), "Third query should call accounts (different args)") + + expected3 := normalizeSnapshot(resolve.CacheAnalyticsSnapshot{ + L2Reads: []resolve.CacheKeyEvent{ + {CacheKey: keyUserById5678, EntityType: "Query", Kind: resolve.CacheKeyMiss, DataSource: dsAccountsLocal}, // L2 miss: different args, not cached + }, + L2Writes: []resolve.CacheWriteEvent{ + {CacheKey: keyUserById5678, EntityType: "Query", ByteSize: byteSizeUser5678, DataSource: dsAccountsLocal, CacheLevel: resolve.CacheLevelL2, TTL: 30 * time.Second}, // New args written to L2 + }, + FieldHashes: []resolve.EntityFieldHash{ + {EntityType: "User", FieldName: "username", FieldHash: hashUsername5678Local, KeyRaw: entityKeyUser5678Local, Source: resolve.FieldSourceSubgraph}, // User 5678 data from subgraph + }, + EntityTypes: []resolve.EntityTypeInfo{ + {TypeName: "User", Count: 1, UniqueKeys: 1}, + }, + }) + assert.Equal(t, expected3, normalizeSnapshot(parseCacheAnalytics(t, headers))) + }) + + t.Run("root field only - L2 analytics without entity caching", func(t *testing.T) { + // Tests root field caching analytics in isolation — only root field caching configured, + // no entity caching. Verifies that only root field events appear in analytics. + defaultCache := NewFakeLoaderCache() + caches := map[string]resolve.LoaderCache{ + "default": defaultCache, + } + + tracker := newSubgraphCallTracker(http.DefaultTransport) + trackingClient := &http.Client{Transport: tracker} + + // Only configure root field caching for products — no entity caching at all + rootOnlyConfigs := engine.SubgraphCachingConfigs{ + { + SubgraphName: "products", + RootFieldCaching: plan.RootFieldCacheConfigurations{ + {TypeName: "Query", FieldName: "topProducts", CacheName: "default", TTL: 30 * time.Second}, + }, + }, + } + + setup := federationtesting.NewFederationSetup(addCachingGateway( + withCachingEnableART(false), + withCachingLoaderCache(caches), + withHTTPClient(trackingClient), + withCachingOptionsFunc(resolve.CachingOptions{EnableL2Cache: true, EnableCacheAnalytics: true}), + withSubgraphEntityCachingConfigs(rootOnlyConfigs), + )) + t.Cleanup(setup.Close) + + gqlClient := NewGraphqlClient(http.DefaultClient) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + productsURLParsed, _ := url.Parse(setup.ProductsUpstreamServer.URL) + productsHost := productsURLParsed.Host + reviewsURLParsed, _ := url.Parse(setup.ReviewsUpstreamServer.URL) + reviewsHost := reviewsURLParsed.Host + accountsURLParsed, _ := url.Parse(setup.AccountsUpstreamServer.URL) + accountsHost := accountsURLParsed.Host + + const ( + keyTopProductsLocal = `{"__typename":"Query","field":"topProducts"}` + dsProductsLocal = "products" + byteSizeTP = 127 // Query.topProducts root field response + ) + + // First query — L2 miss for root field, no events for entities (not configured) + tracker.Reset() + resp, headers := gqlClient.QueryWithHeaders(ctx, setup.GatewayServer.URL, cachingTestQueryPath("queries/multiple_upstream_without_provides.query"), nil, t) + assert.Equal(t, expectedResponseBody, string(resp)) + + // Products subgraph called (root field miss), reviews + accounts always called (no entity caching) + assert.Equal(t, 1, tracker.GetCount(productsHost), "First query should call products subgraph") + assert.Equal(t, 1, tracker.GetCount(reviewsHost), "First query should call reviews subgraph") + assert.Equal(t, 1, tracker.GetCount(accountsHost), "First query should call accounts subgraph") + + expected1 := normalizeSnapshot(resolve.CacheAnalyticsSnapshot{ + L2Reads: []resolve.CacheKeyEvent{ + {CacheKey: keyTopProductsLocal, EntityType: "Query", Kind: resolve.CacheKeyMiss, DataSource: dsProductsLocal}, // L2 miss: first request, cache empty + }, + L2Writes: []resolve.CacheWriteEvent{ + {CacheKey: keyTopProductsLocal, EntityType: "Query", ByteSize: byteSizeTP, DataSource: dsProductsLocal, CacheLevel: resolve.CacheLevelL2, TTL: 30 * time.Second}, // Root field written after products fetch + }, + // Only entity types tracked during resolution (not caching-dependent) + FieldHashes: multiUpstreamFieldHashes, + EntityTypes: multiUpstreamEntityTypes, + }) + assert.Equal(t, expected1, normalizeSnapshot(parseCacheAnalytics(t, headers))) + + // Second query — L2 hit for root field, entities still fetched (not cached) + tracker.Reset() + resp, headers = gqlClient.QueryWithHeaders(ctx, setup.GatewayServer.URL, cachingTestQueryPath("queries/multiple_upstream_without_provides.query"), nil, t) + assert.Equal(t, expectedResponseBody, string(resp)) + + // Products subgraph skipped (root field cache hit), reviews + accounts still called + assert.Equal(t, 0, tracker.GetCount(productsHost), "Second query should skip products (root field cache hit)") + assert.Equal(t, 1, tracker.GetCount(reviewsHost), "Second query should call reviews (no entity caching)") + assert.Equal(t, 1, tracker.GetCount(accountsHost), "Second query should call accounts (no entity caching)") + + expected2 := normalizeSnapshot(resolve.CacheAnalyticsSnapshot{ + L2Reads: []resolve.CacheKeyEvent{ + {CacheKey: keyTopProductsLocal, EntityType: "Query", Kind: resolve.CacheKeyHit, DataSource: dsProductsLocal, ByteSize: byteSizeTP}, // L2 hit: root field cached by first request + }, + // No L2Writes: root field served from cache, entities have no caching configured + FieldHashes: multiUpstreamFieldHashes, // Entity field hashes still tracked (resolution, not caching) + EntityTypes: multiUpstreamEntityTypes, + }) + assert.Equal(t, expected2, normalizeSnapshot(parseCacheAnalytics(t, headers))) + }) } func TestShadowCacheE2E(t *testing.T) { diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go index 1d218d076b..c7ab652885 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go @@ -1784,6 +1784,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { }, OperationType: ast.OperationTypeQuery, ProvidesData: &resolve.Object{ + HasAliases: true, Fields: []*resolve.Field{ { Name: []byte("name"), @@ -1798,9 +1799,11 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Value: &resolve.Object{ Path: []string{"shippingInfo"}, Nullable: true, + HasAliases: true, Fields: []*resolve.Field{ { Name: []byte("z"), + OriginalName: []byte("zip"), Value: &resolve.Scalar{ Path: []string{"z"}, }, @@ -1869,6 +1872,16 @@ func TestGraphQLDataSourceFederation(t *testing.T) { TTL: time.Second * 30, IncludeSubgraphHeaderPrefix: true, UseL1Cache: false, // Set to false by postprocessor (no L1 benefit for this fetch) + KeyFields: []resolve.KeyField{ + {Name: "id"}, + { + Name: "info", + Children: []resolve.KeyField{ + {Name: "a"}, + {Name: "b"}, + }, + }, + }, CacheKeyTemplate: &resolve.EntityQueryCacheKeyTemplate{ Keys: resolve.NewResolvableObjectVariable(&resolve.Object{ Nullable: true, @@ -1941,6 +1954,11 @@ func TestGraphQLDataSourceFederation(t *testing.T) { }, TypeName: "User", SourceName: "user.service", + CacheAnalytics: &resolve.ObjectCacheAnalytics{ + KeyFields: []resolve.KeyField{ + {Name: "id"}, + }, + }, Fields: []*resolve.Field{ { Name: []byte("account"), @@ -1953,6 +1971,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Names: []string{"user.service"}, }, ExactParentTypeName: "User", + CacheAnalyticsHash: true, }, Value: &resolve.Object{ Path: []string{"account"}, @@ -1962,6 +1981,14 @@ func TestGraphQLDataSourceFederation(t *testing.T) { }, TypeName: "Account", SourceName: "user.service", + CacheAnalytics: &resolve.ObjectCacheAnalytics{ + KeyFields: []resolve.KeyField{ + {Name: "id"}, + {Name: "info"}, + {Name: "{a"}, + {Name: "b}"}, + }, + }, Fields: []*resolve.Field{ { Name: []byte("__typename"), @@ -1974,6 +2001,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Names: []string{"user.service"}, }, ExactParentTypeName: "Account", + CacheAnalyticsHash: true, }, Value: &resolve.String{ Path: []string{"__typename"}, @@ -1991,6 +2019,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Names: []string{"account.service"}, }, ExactParentTypeName: "Account", + CacheAnalyticsHash: true, }, Value: &resolve.String{ Path: []string{"name"}, @@ -2008,6 +2037,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { }, ExactParentTypeName: "Account", HasAuthorizationRule: true, + CacheAnalyticsHash: true, }, Value: &resolve.Object{ Path: []string{"shippingInfo"}, @@ -3908,6 +3938,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Info: &resolve.FieldInfo{ Name: "account", ExactParentTypeName: "User", + CacheAnalyticsHash: true, ParentTypeNames: []string{"User"}, NamedType: "Account", Source: resolve.TypeFieldSource{ @@ -3929,6 +3960,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Info: &resolve.FieldInfo{ Name: "address", ExactParentTypeName: "Account", + CacheAnalyticsHash: true, ParentTypeNames: []string{"Account"}, NamedType: "Address", Source: resolve.TypeFieldSource{ @@ -3953,6 +3985,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Info: &resolve.FieldInfo{ Name: "fullAddress", ExactParentTypeName: "Address", + CacheAnalyticsHash: true, ParentTypeNames: []string{"Address"}, NamedType: "String", Source: resolve.TypeFieldSource{ diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go index ea4fb52cd9..434b668098 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go @@ -464,12 +464,14 @@ func TestGraphQLDataSource(t *testing.T) { ProvidesData: &resolve.Object{ Nullable: false, Path: []string{}, + HasAliases: true, Fields: []*resolve.Field{ { Name: []byte("droid"), Value: &resolve.Object{ Nullable: true, Path: []string{"droid"}, + HasAliases: true, Fields: []*resolve.Field{ { Name: []byte("name"), @@ -479,7 +481,8 @@ func TestGraphQLDataSource(t *testing.T) { }, }, { - Name: []byte("aliased"), + Name: []byte("aliased"), + OriginalName: []byte("name"), Value: &resolve.Scalar{ Path: []string{"aliased"}, Nullable: false, diff --git a/v2/pkg/engine/datasourcetesting/datasourcetesting.go b/v2/pkg/engine/datasourcetesting/datasourcetesting.go index 994a40d933..a6e294cd40 100644 --- a/v2/pkg/engine/datasourcetesting/datasourcetesting.go +++ b/v2/pkg/engine/datasourcetesting/datasourcetesting.go @@ -260,6 +260,12 @@ func RunTestWithVariables(definition, operation, operationName, variables string clearCacheKeyTemplates(actualPlan) } + // Clear CacheAnalytics from response Object nodes by default since most tests + // don't need to verify cache analytics. Tests using WithEntityCaching() opt in. + if !opts.withEntityCaching { + clearCacheAnalytics(actualPlan) + } + if opts.withPrintPlan { t.Log("\n", actualPlan.(*plan.SynchronousResponsePlan).Response.Fetches.QueryPlan().PrettyPrint()) } @@ -361,3 +367,35 @@ func clearCacheKeyTemplateFromFetch(f resolve.Fetch) { fetch.FetchConfiguration.Caching.UseL1Cache = false } } + +// clearCacheAnalytics recursively clears CacheAnalytics from all Object nodes in the plan. +// This is called by default so tests don't need to account for cache analytics. +// Use WithEntityCaching() to opt in to including cache analytics in tests. +func clearCacheAnalytics(p plan.Plan) { + switch pl := p.(type) { + case *plan.SynchronousResponsePlan: + if pl.Response != nil && pl.Response.Data != nil { + clearCacheAnalyticsFromNode(pl.Response.Data) + } + case *plan.SubscriptionResponsePlan: + if pl.Response != nil && pl.Response.Response != nil && pl.Response.Response.Data != nil { + clearCacheAnalyticsFromNode(pl.Response.Response.Data) + } + } +} + +func clearCacheAnalyticsFromNode(node resolve.Node) { + switch n := node.(type) { + case *resolve.Object: + n.CacheAnalytics = nil + for _, field := range n.Fields { + if field.Value != nil { + clearCacheAnalyticsFromNode(field.Value) + } + } + case *resolve.Array: + if n.Item != nil { + clearCacheAnalyticsFromNode(n.Item) + } + } +} From a24f2a2f43ef6e2967633e6aca07795cdd4248d4 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Wed, 4 Mar 2026 09:07:48 +0100 Subject: [PATCH 2/3] fix: resolve gci formatting lint errors Co-Authored-By: Claude Haiku 4.5 --- .../graphql_datasource_federation_test.go | 10 +++++----- .../graphql_datasource/graphql_datasource_test.go | 8 ++++---- v2/pkg/engine/resolve/resolve_test.go | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go index c7ab652885..c1cb77130d 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go @@ -1797,12 +1797,12 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Name: []byte("shippingInfo"), OnTypeNames: [][]byte{[]byte("Account")}, Value: &resolve.Object{ - Path: []string{"shippingInfo"}, - Nullable: true, - HasAliases: true, + Path: []string{"shippingInfo"}, + Nullable: true, + HasAliases: true, Fields: []*resolve.Field{ { - Name: []byte("z"), + Name: []byte("z"), OriginalName: []byte("zip"), Value: &resolve.Scalar{ Path: []string{"z"}, @@ -2037,7 +2037,7 @@ func TestGraphQLDataSourceFederation(t *testing.T) { }, ExactParentTypeName: "Account", HasAuthorizationRule: true, - CacheAnalyticsHash: true, + CacheAnalyticsHash: true, }, Value: &resolve.Object{ Path: []string{"shippingInfo"}, diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go index 434b668098..98d1f13708 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go @@ -462,15 +462,15 @@ func TestGraphQLDataSource(t *testing.T) { }, }, ProvidesData: &resolve.Object{ - Nullable: false, - Path: []string{}, + Nullable: false, + Path: []string{}, HasAliases: true, Fields: []*resolve.Field{ { Name: []byte("droid"), Value: &resolve.Object{ - Nullable: true, - Path: []string{"droid"}, + Nullable: true, + Path: []string{"droid"}, HasAliases: true, Fields: []*resolve.Field{ { diff --git a/v2/pkg/engine/resolve/resolve_test.go b/v2/pkg/engine/resolve/resolve_test.go index 72e29fecf6..82a8e1e635 100644 --- a/v2/pkg/engine/resolve/resolve_test.go +++ b/v2/pkg/engine/resolve/resolve_test.go @@ -183,7 +183,6 @@ func waitForFollowerCount(t *testing.T, r *Resolver, count int32) { } } - type TestErrorWriter struct { } From 963fa199f851b5027f388854be831474dd580760 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Wed, 4 Mar 2026 09:14:26 +0100 Subject: [PATCH 3/3] fix: lowercase error string to satisfy staticcheck ST1005 Co-Authored-By: Claude Haiku 4.5 --- v2/pkg/engine/datasource/service_datasource/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/pkg/engine/datasource/service_datasource/schema.go b/v2/pkg/engine/datasource/service_datasource/schema.go index 86b1d5f74d..6dcaf1ddd0 100644 --- a/v2/pkg/engine/datasource/service_datasource/schema.go +++ b/v2/pkg/engine/datasource/service_datasource/schema.go @@ -56,7 +56,7 @@ func ExtendSchemaWithServiceTypes(schema *ast.Document) error { // 1. Find Query type first to fail fast queryNode, found := findQueryType(schema) if !found { - return fmt.Errorf("Query type not found in schema") + return fmt.Errorf("query type not found in schema") } // 2. Add _Capability type (must be added before _Service since _Service references it)