Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7fc494f
add cache for variables normalization and remapping
devsergiy Nov 5, 2025
ac99a67
ensure we hit variables normalization cache: remove polluting of keyg…
devsergiy Nov 5, 2025
5096314
ensure that we hit variables remap cache
devsergiy Nov 5, 2025
e14181e
Merge branch 'main' into fix/normalization-cache
ysmolski Nov 13, 2025
4319eab
Merge branch 'main' of github.com:wundergraph/cosmo into fix/normaliz…
ysmolski Nov 13, 2025
a2bf4a4
add proper cache hits and more tests
ysmolski Nov 14, 2025
dcbf441
remove debug comments
ysmolski Nov 14, 2025
11e6116
edit comments
ysmolski Nov 19, 2025
ca894b4
fix comments in tests
ysmolski Nov 19, 2025
47893d8
remove unneeded keygen reset
ysmolski Nov 19, 2025
2be06a9
add new caches to metricInfos
ysmolski Nov 19, 2025
faacc11
Merge branch 'main' into fix/normalization-cache
ysmolski Nov 19, 2025
9e0a7ac
fix telemetry tests
ysmolski Nov 19, 2025
9121581
fix small bug
ysmolski Nov 20, 2025
db0e4a4
clarify tests and comments
ysmolski Nov 26, 2025
aee6117
clarify comments
ysmolski Nov 26, 2025
815e29e
expand comments for skip/include variables
ysmolski Nov 26, 2025
a7db1a8
Merge branch 'main' into fix/normalization-cache
ysmolski Nov 26, 2025
50cd276
fix error messages
ysmolski Nov 26, 2025
9301cf9
include disabled into remap vars cache key
ysmolski Nov 26, 2025
c39ddf0
Merge branch 'main' into fix/normalization-cache
ysmolski Nov 26, 2025
b6cd9bb
simplify options for cache response headers
ysmolski Nov 26, 2025
8d8b263
Merge branch 'main' into fix/normalization-cache
ysmolski Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions router-tests/normalization_cache_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,161 @@
package integration

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/wundergraph/cosmo/router-tests/testenv"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/cosmo/router/pkg/config"
)

// cacheHit represents the expected cache hit/miss status for all three normalization stages.
// True value means the cache was hit.
type cacheHit struct {
normalization bool
variables bool
remapping bool
}

// assertCacheHeaders checks all three normalization cache headers
func assertCacheHeaders(t *testing.T, res *testenv.TestResponse, expected cacheHit) {
t.Helper()
s := func(hit bool) string {
if hit {
return "HIT"
}
return "MISS"
}

require.Equal(t, s(expected.normalization), res.Response.Header.Get(core.NormalizationCacheHeader),
"Normalization cache hit mismatch")
require.Equal(t, s(expected.variables), res.Response.Header.Get(core.VariablesNormalizationCacheHeader),
"Variables normalization cache hit mismatch")
require.Equal(t, s(expected.remapping), res.Response.Header.Get(core.VariablesRemappingCacheHeader),
"Variables remapping cache hit mismatch")
}

func TestVarsNormalizationRemappingCaches(t *testing.T) {
t.Parallel()

t.Run("Basic normalization cache with skip/include", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
f := func(expected cacheHit, skipMouse bool) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
OperationName: []byte(`"Employee"`),
Query: `query Employee( $id: Int! = 4 $withAligators: Boolean! $withCats: Boolean! $skipDogs:Boolean! $skipMouses:Boolean! ) { employee(id: $id) { details { pets { name __typename ...AlligatorFields @include(if: $withAligators) ...CatFields @include(if: $withCats) ...DogFields @skip(if: $skipDogs) ...MouseFields @skip(if: $skipMouses) ...PonyFields @include(if: false) } } } } fragment AlligatorFields on Alligator { __typename class dangerous gender name } fragment CatFields on Cat { __typename class gender name type } fragment DogFields on Dog { __typename breed class gender name } fragment MouseFields on Mouse { __typename class gender name } fragment PonyFields on Pony { __typename class gender name }`,
Variables: []byte(fmt.Sprintf(`{"withAligators": true,"withCats": true,"skipDogs": false,"skipMouses": %t}`, skipMouse)),
})
assertCacheHeaders(t, res, expected)
require.Equal(t, `{"data":{"employee":{"details":{"pets":[{"name":"Abby","__typename":"Dog","breed":"GOLDEN_RETRIEVER","class":"MAMMAL","gender":"FEMALE"},{"name":"Survivor","__typename":"Pony"}]}}}}`, res.Body)
}

f(cacheHit{false, false, false}, true)
f(cacheHit{true, true, true}, true)
f(cacheHit{true, true, true}, true)
f(cacheHit{false, false, false}, false)
f(cacheHit{true, true, true}, true)
})
})

t.Run("Variables normalization cache - inline value extraction", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
// Inline value gets extracted to variable
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { employee(id: 1) { id details { forename } } }`,
})
assertCacheHeaders(t, res, cacheHit{false, false, false})

// Same query
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { employee(id: 1) { id details { forename } } }`,
})
assertCacheHeaders(t, res, cacheHit{true, true, true})

// Different inline value
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { employee(id: 2) { id details { forename } } }`,
})
assertCacheHeaders(t, res, cacheHit{false, false, true})
})
})

t.Run("Variables normalization cache - query changes, but variables stay the same", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query MyQuery($id: Int!) { employee(id: $id) { id } }`,
Variables: []byte(`{"id": 1}`),
})
require.Equal(t, `{"data":{"employee":{"id":1}}}`, res.Body)
assertCacheHeaders(t, res, cacheHit{false, false, false})

// Different query with the same variable value.
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query MyQuery($id: Int!) { employee(id: $id) { id details { forename }} }`,
Variables: []byte(`{"id": 1}`),
})
require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens"}}}}`, res.Body)
assertCacheHeaders(t, res, cacheHit{false, false, false})
})
})

t.Run("Cache key isolation - different operations don't collide", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
// Query A
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query A($id: Int!) { employee(id: $id) { id } }`,
Variables: []byte(`{"id": 1}`),
})
assertCacheHeaders(t, res, cacheHit{false, false, false})

// Query B with different structure should miss
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query B($id: Int!) { employee(id: $id) { id details { forename } } }`,
Variables: []byte(`{"id": 1}`),
})
assertCacheHeaders(t, res, cacheHit{false, false, false})

// Query A again should hit its own cache
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query A($id: Int!) { employee(id: $id) { id } }`,
Variables: []byte(`{"id": 1}`),
})
assertCacheHeaders(t, res, cacheHit{true, true, true})
})
})

t.Run("List coercion with variables normalization cache", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
// Test that list coercion works correctly with caching
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query MyQuery($arg: [String!]!) { rootFieldWithListArg(arg: $arg) }`,
Variables: []byte(`{"arg": "single"}`),
})
require.Equal(t, `{"data":{"rootFieldWithListArg":["single"]}}`, res.Body)
assertCacheHeaders(t, res, cacheHit{false, false, false})

// Same structure should hit cache even with different value
res = xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query MyQuery($arg: [String!]!) { rootFieldWithListArg(arg: $arg) }`,
Variables: []byte(`{"arg": "different"}`),
})
require.Equal(t, `{"data":{"rootFieldWithListArg":["different"]}}`, res.Body)
// Normalization hits because the query structure is unchanged,
// variables misses because the value differs,
// and remapping hits because the structure remains the same.
assertCacheHeaders(t, res, cacheHit{true, false, true})
})
})

}

func TestNormalizationCache(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading