Repository navigation
test(e2e): check the GraphQL mock's answers against GitHub's schema - #615
Conversation
The e2e mock refused a GraphQL request GitHub's schema refuses, but its answers went back to the product as the handler built them. A handler that had stored a null answered it in a Boolean! field, and the two rule mutations answered the whole rule node where their documents select four fields. The response guard now completes a handler's data with graphql-js under the request's own document, on the execution args the request check already prepared, so nothing is parsed twice. The product receives the completed data: the selection and nothing beside it, with null under a selected nullable key the handler left out. Data that does not complete is a violation naming the operation, the handler, the response path, and graphql-js's message. It is answered under the operation's own log entry, so a read still counts as a read. graphql-request.ts exports acceptedGraphqlRequest(), and graphqlRequestErrors() wraps it for the section suites' fake, which is unchanged. Not caught: a value graphql-js coerces (1 for a Boolean, "3" for an Int), and a stored off-schema value that no later request selects.
File size check0 over a hard cap (fails), 6 warning(s).
Split the file, wrap the line, shorten or exempt the comment, or list the path in 5 managed file(s) skipped; repo-platform owns them. |
Member shape check22 finding(s).
Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in 4 generated file(s) skipped. |
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues were identified in the GraphQL mock response-validation changes.
No open issues 🎉
What changed in this PR
This PR checks the end-to-end GraphQL mock's data answers against GitHub's schema, making defects such as the null-response issue highlighted by #599 easier to diagnose. Production code remains unchanged.
Changes:
- Reuses prepared request execution arguments to complete handler data through graphql-js.
- Returns only selected fields and reports completion failures as mock violations while preserving read/write classification.
- Adds unit, pipeline, and scenario-level tests for response shaping and invalid answers.
| File | Description |
|---|---|
test/e2e/mock/routes.ts |
Integrates response completion and violation reporting before response corruption. |
test/e2e/mock/graphql-request.ts |
Exposes prepared execution arguments while retaining the existing error-checking helper. |
test/e2e/mock/graphql-answer.ts |
Completes handler data against the request selection and formats schema errors with response paths. |
test/e2e/mock/graphql-answer.test.ts |
Tests invalid data, selection shaping, logging, and scenario failure behavior. |
Review effort: Balanced
The control that a mutant handler fails a run called the e2e runner's runScenario from a bun test. The runner removes the bundle it builds on the process exit event, which does not fire under bun test, so every test run left a bundle directory in the temp dir, and the unit-test job came to depend on a node binary. The control now runs the product's own code in-process through runSingle against the mock server. The handlers as they are give "clean" and no violation. A mutant branches.rulesQuery handler gives "failed" and the violation on the mock's list, the list the runner fails a scenario on. On a pipeline without the answer check that run fails too, on the product's own parse, so only the violation tells the two apart.
graphql-js completes some answers GitHub's schema could never give without an error. A non-object at an object position becomes an object of nulls, and a leaf is coerced: true in an Int field to 1, 5 in an ID to "5". Such an answer reached the product with no violation. After a completion with no error, the completed data must now equal the handler's data at every selected position: an object for an object, a list of the same length for a list, a leaf by ===. The one allowed difference is null under a nullable key the handler left out. Any other difference is a mock violation naming the path and both values. The file headers no longer say every scenario passed on an answer off the schema. Where the product parses the field, the run already failed, with the product's own error.
Before
The runner's line names no cause. The product's own error, in the rerun's stdout, does:
After
This branch:
PASS.The check adds: the cause on the runner's line, and the handler's name.
Silent without a re-read: the mutation's document does not select the field.
For the owner: the mock stores a
nulla rule mutation sends. What GitHub does with that null is not known here, so a run that sends one fails at the next read.For the owner: a wildcard
required_linear_history: nullon main goes red on a re-read, livetrueorfalse. No corpus scenario holds it.How
nullunder an omitted nullable key may differ.graphql'sexecuteRootSelectionSet. Nothing undersrc/changes.Proof
bun test test/e2e/mock.Technical details
git archive 346b640a^ | tar -x -C <dir>, linknode_modules, runbun run build:schema, save the first scenario below undertest/e2e/scenarios/, run the Before command in<dir>.test/e2e/mock/over<dir>/test/e2e/mock/and run the same command for the After block.test/e2e/scenarios/printsPASSand1/1 passed.fixpointline removed, step 2 printsPASSand1/1 passed.grepunder Before, run in<dir>after the Before command, reads the rerun's stdout from the artifacts that run left.branches.createRuleandbranches.updateRulehandlers answer the whole rule node, and their documents selectid pattern requiresDeployments requiredDeploymentEnvironments.bun -e 'import { completeRule, ruleWireNode } from "./test/e2e/mock/state.ts"; console.log(Object.keys(ruleWireNode(completeRule({ id: "BPR_1", pattern: "release/*" }))).length)'prints22.verifyDeploymentReadbackreadsrequiresDeploymentsandrequiredDeploymentEnvironments.===.truefor an Int completes to1,5for an ID to"5", and7at an object position to an object of nulls.data.deleteBranchProtectionRule: answered 7, the schema serves {"clientMutationId":null}.errorsanswer, which the OpenAPI validator's GraphQL response contract owns.test/sections/fragment-fake.ts, which still hands a handler's data over as built.__typename, so a union value without one is a violation.__typenameon an abstract field still needs__typenameon the answer, or the check reports a violation.__typenameon a concrete type needs it on the answer too, or the check reports a violation, and no declared document selects it there.graphql-request.tsexportsacceptedGraphqlRequest(), andgraphqlRequestErrors()wraps it for the section suites' fake.test/e2e/scenarios/on this branch, prints the block under it forrequiresLinearHistory: trueand forfalse.grep -rl "required_linear_history: null" test --include='*.yml'prints nothing.POST /graphqland counts as a write.graphql-answer.ts, andgraphql-request.tscopied onto an export of main, which keeps itsroutes.ts: 5 fail, 13 pass.control:tests, which call the new module directly or serve data that is already the selection.runSingleagainst the mock server, twice.cleanand no violation.branches.rulesQueryhandler answering the null givesfailedand the violation on the mock's list, the list the runner fails a scenario on.roundTripFailures (the snapshot round trip's verdict)intest/e2e/runner/runner.test.tspins that a mock violation fails a snapshot round trip.verdictFailures, the convergence re-run, or the apply-idempotence re-runs.exitevent, which does not fire underbun test, so a test that callsrunScenariothere leaves its bundle directory behind, and no test on main does.bun test/e2e/run.ts --sections branches,environments,repository, the three sections that declare the 14 GraphQL operations.bun test/e2e/run.ts --sections all, 394 scenarios plus the corpus witness line.FUZZ_SEED=20261111 bun test/e2e/fuzz.ts --iterations 25.test/e2e/mock/{graphql-answer.ts,graphql-answer.test.ts,graphql-request.ts,routes.ts}.git diff --stat origin/main...HEAD -- srcprints nothing.testis a hidden type: this should not be the last merge before a release.