From c89ff828a1af8b29a6bd32daa4d08bcd2f8c8b93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:41:19 +0000 Subject: [PATCH 1/2] Initial plan From 4983e70d4ef8aa7d661f51f93e83bc5efc2da6cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:05:04 +0000 Subject: [PATCH 2/2] Fix GraphQL replay for wildcard and bare leaf field selections - Handle wildcard paths (e.g. "legs.*") in mergeObservedSelection so the schema observer sees the full field data - Add resolveSelectionsAgainstSchema to expand wildcards and bare leaf selections on object-typed fields in the generated GraphQL query - Remove disable: ["graphql"] from both sparse fieldset test scenarios Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --- packages/bridge/test/shared-parity.test.ts | 2 - packages/bridge/test/utils/regression.ts | 66 +++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/bridge/test/shared-parity.test.ts b/packages/bridge/test/shared-parity.test.ts index 13397214..31f03a01 100644 --- a/packages/bridge/test/shared-parity.test.ts +++ b/packages/bridge/test/shared-parity.test.ts @@ -1718,7 +1718,6 @@ regressionTest("parity: sparse fieldsets — wildcard and chains", { }, fields: ["id", "legs.*"], assertData: { id: 42, legs: { duration: "2h", distance: 150 } }, - disable: ["graphql"], assertTraces: 1, }, "requesting price returns price": { @@ -1883,7 +1882,6 @@ regressionTest("parity: sparse fieldsets — nested and array paths", { { id: 1, legs: [{ name: "L1" }] }, { id: 2, legs: [{ name: "L2" }] }, ], - disable: ["graphql"], assertTraces: 1, }, "all fields returned when no requestedFields": { diff --git a/packages/bridge/test/utils/regression.ts b/packages/bridge/test/utils/regression.ts index 13dd4365..9b598b68 100644 --- a/packages/bridge/test/utils/regression.ts +++ b/packages/bridge/test/utils/regression.ts @@ -217,6 +217,50 @@ function buildSelectionTreeForValue( return tree; } +/** + * Expand wildcard (`*`) entries and bare leaf selections on object-typed + * fields by looking up sub-fields from the GraphQL schema type. + * + * - `{ legs: { "*": {} } }` → `{ legs: { duration: {}, distance: {} } }` + * - `{ legs: {} }` where `legs` is an object type → `{ legs: { name: {} } }` + */ +function resolveSelectionsAgainstSchema( + tree: SelectionTree, + type: GraphQLOutputType, +): SelectionTree { + const named = getNamedType(type); + if (!isObjectType(named)) return tree; + + const result: SelectionTree = {}; + for (const [key, child] of Object.entries(tree)) { + const fieldDef = named.getFields()[key]; + if (!fieldDef) { + result[key] = child; + continue; + } + + if ("*" in child) { + // Expand wildcard to all immediate sub-fields from the schema type + const expanded = buildSelectionTreeFromType(fieldDef.type); + result[key] = expanded ?? {}; + } else if (Object.keys(child).length === 0) { + // Bare leaf — if the field's type has sub-fields, expand them so + // the resulting GraphQL query is valid. + const fieldNamedType = getNamedType(fieldDef.type); + if (isObjectType(fieldNamedType)) { + const expanded = buildSelectionTreeFromType(fieldDef.type); + result[key] = expanded ?? {}; + } else { + result[key] = child; + } + } else { + result[key] = resolveSelectionsAgainstSchema(child, fieldDef.type); + } + } + + return result; +} + function renderSelectionTree(tree: SelectionTree | null): string { if (!tree || Object.keys(tree).length === 0) { return ""; @@ -372,6 +416,17 @@ function mergeObservedSelection( for (const selectedPath of selectedPaths) { const path = selectedPath.split(".").filter(Boolean); + + // Handle wildcard: "legs.*" means "select all children of legs" + if (path.length > 1 && path[path.length - 1] === "*") { + const parentPath = path.slice(0, -1); + const parentValue = getObservationPath(value, parentPath); + if (parentValue !== undefined) { + setObservationPath(merged, parentPath, parentValue); + } + continue; + } + const selectedValue = getObservationPath(value, path); if (selectedValue !== undefined) { setObservationPath(merged, path, selectedValue); @@ -428,12 +483,21 @@ function buildGraphQLOperationSource( ? `(${field.args.map((arg) => `${arg.name}: $${arg.name}`).join(", ")})` : ""; - const selectionTree = orderSelectionTree( + let selectionTree = orderSelectionTree( requestedFields?.length ? buildSelectionTreeFromPaths(requestedFields) : buildSelectionTreeForValue(expectedData, field.type), preferredFieldOrder, ); + + // When explicit requestedFields are provided, resolve wildcards + // (e.g. "legs.*") and bare leaf selections on object-typed fields + // (e.g. "legs" where legs has sub-fields) against the schema type so + // the resulting GraphQL query includes the required sub-field selections. + if (selectionTree && requestedFields?.length) { + selectionTree = resolveSelectionsAgainstSchema(selectionTree, field.type); + } + const selection = renderSelectionTree(selectionTree); const operationKeyword = rootTypeName === "Mutation" ? "mutation" : "query";