Repository navigation
test: cover the Linear GraphQL layer end to end - #50
Closed
seraph-pixelperfect wants to merge 1 commit into
Closed
seraph-pixelperfect wants to merge 1 commit into
seraph-pixelperfect wants to merge 1 commit into
Conversation
…ayer Gap-fill for #29 on top of the command-level suites: direct transport-level tests for fetchViewer, fetchTeams, deleteIssue, and resolveStateIdByName (query TeamStates) which had zero document assertions; createIssue's labelNames->labelIds branch, team-not-found error, and the minimal omit-null document; updateIssue's minimal and full omit-null combinations; all six mutations' success:false payload branches; fetchIssues' anti-spin pagination guards; and the mapLinearError matrix end to end through linearRequest (HTTP 400/403/404/502 + GraphQL not-found/auth/validation, message text formatting, extensions hint, non-JSON body fallback, LINEAR_AXI_DEBUG dump, and the POST request-shape contract).
This was referenced Aug 20, 2026
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #29
Depends on #49
What this does
A coverage audit + gap-fill for issue #29 ("src/linear.ts — every GraphQL operation — has zero coverage"). The issue's premise was partially stale: thirteen stacked PRs (#37–#49) already added fetch-stubbed, command-level coverage asserting most GraphQL document shapes transitively (225 tests / 19 files at the base of this branch). This PR audits every export in
src/linear.ts, cites where each operation's document is already asserted, and adds two new transport-level test files (functions insrc/linear.tscalled directly against a stubbedfetch) only for the real gaps:test/graphql-layer.test.ts(24 tests) — operationstest/graphql-layer-errors.test.ts(16 tests) —mapLinearErrorend to end throughlinearRequestNet: 225 → 265 tests (21 files), +40 tests, zero source changes.
Gaps filled (all had zero coverage before)
fetchViewer— exact documentquery { viewer { id name email } }(only a loose transitivetoContain('viewer')existed via--assignee me).fetchTeams— thequery Teams { teams { nodes { id key name } } }document had no assertion anywhere (theteamscommand has no test file).deleteIssue—mutation DeleteIssueand itssuccess:falsebranch had no assertion anywhere.resolveStateIdByName(viaupdateIssuestateName) —query TeamStates, case-insensitive matching, theAvailable states: ...not-found listing, and theCould not resolve team for issuebranch were all untested.createIssueomit-null builder — the minimal document (teamId+titleonly, asserting literal omission ofdescription/labelIds/projectId/cycleId/parentId), thelabelNames → resolveLabelIds → labelIdsbranch (case-insensitive, unknown names skipped, server order), and the team-not-foundVALIDATION_ERROR.updateIssueomit-null builder — minimal ($id+$titleonly, everything else literally omitted) and maximal ($title+$stateId+$assigneeId+$labelIdsdeclared in builder order, unsetpriority/description/projectId/cycleIdomitted).success:falsepayload branches — every existing suite stubbedsuccess: trueonly (grep 'success: false' test/was empty).fetchIssuesanti-spin pagination guards —hasNextPage:truewith a nullendCursor, a non-advancing (repeated) cursor, and an empty page that still claimshasNextPage. (Cursor batching in 50s + remaining-slotfirstwas already covered directly inissues-pagination.test.ts; the empty-page guard forfetchLabelsinlabels.test.ts.)mapLinearErrore2e throughlinearRequest— the remaining matrix: HTTP 400 (incl. the non-JSON-bodyjson()-catch fallback), 403 both sub-branches (message-less →FORBIDDEN; forbidden-message →AUTH_REQUIREDoverride), 404 plain →NOT_FOUND, 502 →NETWORK_ERROR; GraphQL 200errors[]: not-found →NOT_FOUND, unauthorized →AUTH_REQUIRED, validation →VALIDATION_ERROR, message text with(at path)+[extensions.code], theextensions: {...}hint for unclassified errors, no-hintUNKNOWN; a non-arrayerrorsvalue ignored; and theLINEAR_AXI_DEBUG=1stderr dump. Plus the POST request-shape contract (URL, headers,{query, variables}body, defaultvariables: {}). (401/429/500/generic-GQL/network-throw were already covered inrate-limit-retry.test.ts; the direct unit inerrors.test.tsstands.)Coverage audit — every
src/linear.tsoperation → where its document shape is assertedlinearRequest(transport, 429 retry, 401/429/500, network throw, generic GraphQL error)rate-limit-retry.test.ts; + NEWgraphql-layer-errors.test.ts(request contract + full status/GraphQL matrix + DEBUG dump)fetchViewerissues-search.test.ts(--assignee me); + NEW direct, exact documentfetchTeamsgraphql-layer.test.ts(zero before)fetchProjects(+ pagination)projects.test.ts(direct: doc, fields, cursor batching,hasMore)fetchCycles(root + per-team docs, team resolution, not-found, pagination)cycles.test.ts(direct)fetchIssues(filters,searchIssues, pagination batching)issues-search.test.ts,issues-pagination.test.ts(direct batching),projects.test.ts(project filter); + NEW anti-spin guardsfetchIssue(detail,--fieldsextras,--fullchildren, null→undefined)fields-wiring.test.ts(extras),sub-issues.test.ts(children + null→undefined),comment-list/comment-update-delete/description-file/project-cycle-assignment(ref pre-fetch); + NEW direct minimal/maximal selectionfetchComments(+ pagination)comment-list.test.ts(direct: doc + pagination),comment-update-delete.test.tsfetchCommentcomment-update-delete.test.tsfetchLabels(+ pagination, empty-page guard,hasMore)labels.test.ts(direct)createIssue(omit-null: description / projectId / cycleId / parentId)description-file.test.ts,project-cycle-assignment.test.ts,sub-issues.test.ts; + NEW minimal doc,labelNamesbranch, team-not-found,success:falseupdateIssue(omit-null: assigneeId / labelIds /[]/ projectId / cycleId / description)issue-update-assignee-labels.test.ts,project-cycle-assignment.test.ts,description-file.test.ts; + NEW title-only, full combo,success:falsedeleteIssuegraphql-layer.test.ts(zero before)createCommentcomment-list.test.ts,comment-update-delete.test.ts; + NEWsuccess:falseupdateCommentcomment-update-delete.test.ts; + NEWsuccess:falsedeleteCommentcomment-update-delete.test.ts; + NEWsuccess:falseresolveTeamId(query TeamByKey)cycles.test.ts(incl. filter doc),description-file/labels/project-cycle-assignment/sub-issues.test.tsresolveProjectId(query ProjectByName)project-cycle-assignment.test.tsrequireProjectId(loud not-found)project-cycle-assignment.test.tsfetchActiveCycles(both docs)project-cycle-assignment.test.tsresolveCycleIdByNumber(+ not-found)project-cycle-assignment.test.tsresolveLabelIds--labelinissue-update-assignee-labels.test.ts; + NEW e2e throughcreateIssueresolveUserId(no-match, multi-match)issue-update-assignee-labels.test.tsresolveStateIdByName(query TeamStates)graphql-layer.test.ts(zero before)withExtraFields+ field fragmentsfields-wiring.test.ts(documents with--fields)mapLinearErrorerrors.test.ts; e2e:rate-limit-retry.test.ts; + NEW full matrix ingraphql-layer-errors.test.tsLocal gates (all run at HEAD of this branch)
Latent issues found during the audit (documented, NOT fixed — no source changes in this PR)
linearRequest200-with-non-JSON success path resolvesundefined: if the server answers HTTP 200 with a non-JSON body,bodybecomesnull(thejson()catch) and(body as { data: T }).dataisundefined, so callers fail later with a rawTypeError(e.g. reading.viewer) instead of a structuredAxiError. Only reachable with a misbehaving server/proxy; left as-is since fixing changes behavior and exceeds this PR's no-product-change scope.createIssuedeclares$title: String!whileupdateIssuedeclares$title: String— consistent with Linear's create-vs-update input nullability; the new tests pin both.Stacked-PR note
Stacked on #49 (stack: #37→#38→#39→#40→#41→#42→#43→#44→#45→#46→#47→#48→#49). No CI checks appear here —
ci.ymlonly triggers on PRs targetingmain. CI runs when retargeted tomainafter the stack merges; local gates above are the evidence. Not retargeting.CI caveat: GitHub Actions billing/spending-limit rejections render like check failures. If a check shows red with no log output, check
gh run view <run-id>/ the jobs API for a billing annotation or an emptystepslist with a ~3-4s duration — that is a quota block, not a test failure; in that case local output above is the evidence and the run should not be retried.Identity note
Posted by an automated agent dispatch authenticated as
seraph-pixelperfect, for human review — not self-approved. Merge is a human decision.