Skip to content

test: cover the Linear GraphQL layer end to end - #50

Closed
seraph-pixelperfect wants to merge 1 commit into
feat/rate-limit-retryfrom
feat/graphql-layer-tests
Closed

seraph-pixelperfect wants to merge 1 commit into
feat/rate-limit-retryfrom
feat/graphql-layer-tests

Conversation

@seraph-pixelperfect

Copy link
Copy Markdown
Collaborator

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 in src/linear.ts called directly against a stubbed fetch) only for the real gaps:

  • test/graphql-layer.test.ts (24 tests) — operations
  • test/graphql-layer-errors.test.ts (16 tests) — mapLinearError end to end through linearRequest

Net: 225 → 265 tests (21 files), +40 tests, zero source changes.

Gaps filled (all had zero coverage before)

  1. fetchViewer — exact document query { viewer { id name email } } (only a loose transitive toContain('viewer') existed via --assignee me).
  2. fetchTeams — the query Teams { teams { nodes { id key name } } } document had no assertion anywhere (the teams command has no test file).
  3. deleteIssue — mutation DeleteIssue and its success:false branch had no assertion anywhere.
  4. resolveStateIdByName (via updateIssue stateName) — query TeamStates, case-insensitive matching, the Available states: ... not-found listing, and the Could not resolve team for issue branch were all untested.
  5. createIssue omit-null builder — the minimal document (teamId+title only, asserting literal omission of description/labelIds/projectId/cycleId/parentId), the labelNames → resolveLabelIds → labelIds branch (case-insensitive, unknown names skipped, server order), and the team-not-found VALIDATION_ERROR.
  6. updateIssue omit-null builder — minimal ($id+$title only, everything else literally omitted) and maximal ($title+$stateId+$assigneeId+$labelIds declared in builder order, unset priority/description/projectId/cycleId omitted).
  7. All six mutations' success:false payload branches — every existing suite stubbed success: true only (grep 'success: false' test/ was empty).
  8. fetchIssues anti-spin pagination guards — hasNextPage:true with a null endCursor, a non-advancing (repeated) cursor, and an empty page that still claims hasNextPage. (Cursor batching in 50s + remaining-slot first was already covered directly in issues-pagination.test.ts; the empty-page guard for fetchLabels in labels.test.ts.)
  9. mapLinearError e2e through linearRequest — the remaining matrix: HTTP 400 (incl. the non-JSON-body json()-catch fallback), 403 both sub-branches (message-less → FORBIDDEN; forbidden-message → AUTH_REQUIRED override), 404 plain → NOT_FOUND, 502 → NETWORK_ERROR; GraphQL 200 errors[]: not-found → NOT_FOUND, unauthorized → AUTH_REQUIRED, validation → VALIDATION_ERROR, message text with (at path) + [extensions.code], the extensions: {...} hint for unclassified errors, no-hint UNKNOWN; a non-array errors value ignored; and the LINEAR_AXI_DEBUG=1 stderr dump. Plus the POST request-shape contract (URL, headers, {query, variables} body, default variables: {}). (401/429/500/generic-GQL/network-throw were already covered in rate-limit-retry.test.ts; the direct unit in errors.test.ts stands.)

Coverage audit — every src/linear.ts operation → where its document shape is asserted

Operation Document asserted in
linearRequest (transport, 429 retry, 401/429/500, network throw, generic GraphQL error) rate-limit-retry.test.ts; + NEW graphql-layer-errors.test.ts (request contract + full status/GraphQL matrix + DEBUG dump)
fetchViewer transitive: issues-search.test.ts (--assignee me); + NEW direct, exact document
fetchTeams NEW graphql-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 guards
fetchIssue (detail, --fields extras, --full children, 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 selection
fetchComments (+ pagination) comment-list.test.ts (direct: doc + pagination), comment-update-delete.test.ts
fetchComment comment-update-delete.test.ts
fetchLabels (+ 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, labelNames branch, team-not-found, success:false
updateIssue (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:false
deleteIssue NEW graphql-layer.test.ts (zero before)
createComment comment-list.test.ts, comment-update-delete.test.ts; + NEW success:false
updateComment comment-update-delete.test.ts; + NEW success:false
deleteComment comment-update-delete.test.ts; + NEW success:false
resolveTeamId (query TeamByKey) cycles.test.ts (incl. filter doc), description-file/labels/project-cycle-assignment/sub-issues.test.ts
resolveProjectId (query ProjectByName) project-cycle-assignment.test.ts
requireProjectId (loud not-found) project-cycle-assignment.test.ts
fetchActiveCycles (both docs) project-cycle-assignment.test.ts
resolveCycleIdByNumber (+ not-found) project-cycle-assignment.test.ts
resolveLabelIds transitive via update --label in issue-update-assignee-labels.test.ts; + NEW e2e through createIssue
resolveUserId (no-match, multi-match) issue-update-assignee-labels.test.ts
resolveStateIdByName (query TeamStates) NEW graphql-layer.test.ts (zero before)
withExtraFields + field fragments fields-wiring.test.ts (documents with --fields)
mapLinearError unit: errors.test.ts; e2e: rate-limit-retry.test.ts; + NEW full matrix in graphql-layer-errors.test.ts

Local gates (all run at HEAD of this branch)

$ pnpm build      → tsc, clean
$ pnpm lint       → eslint ., clean
$ pnpm run build:skill -- --check
                  → skills/linear-axi/SKILL.md is up to date.
$ pnpm test
 Test Files  21 passed (21)
      Tests   265 passed (265)      ← baseline 225 in 19 files, +40
$ pnpm run format:check
                  → 18 pre-existing warnings (same file set as main; CI does not run this gate)
$ pnpm exec prettier --check test/graphql-layer.test.ts test/graphql-layer-errors.test.ts
                  → All matched files use Prettier code style!

Latent issues found during the audit (documented, NOT fixed — no source changes in this PR)

  • linearRequest 200-with-non-JSON success path resolves undefined: if the server answers HTTP 200 with a non-JSON body, body becomes null (the json() catch) and (body as { data: T }).data is undefined, so callers fail later with a raw TypeError (e.g. reading .viewer) instead of a structured AxiError. Only reachable with a misbehaving server/proxy; left as-is since fixing changes behavior and exceeds this PR's no-product-change scope.
  • Observation (not a bug): createIssue declares $title: String! while updateIssue declares $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.yml only triggers on PRs targeting main. CI runs when retargeted to main after 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 empty steps list 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.

…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).
@ghostinprod-pixelperfect

Copy link
Copy Markdown
Collaborator

Superseded by merged rc/0.2 PR #52 (commit ad6b141), which consolidated this write/transport stack with validation and review fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants