From aa2bda2e329cc2cd657acc6e3f53258e57ebf46e Mon Sep 17 00:00:00 2001 From: Harsh Date: Fri, 6 Mar 2026 15:08:54 +0530 Subject: [PATCH] fix: harden Linear integration helper against transient non-JSON errors --- .../src/tracker-linear.integration.test.ts | 116 +++++++++++------- 1 file changed, 70 insertions(+), 46 deletions(-) diff --git a/packages/integration-tests/src/tracker-linear.integration.test.ts b/packages/integration-tests/src/tracker-linear.integration.test.ts index 4aaa6c8b4..2354c9b0e 100644 --- a/packages/integration-tests/src/tracker-linear.integration.test.ts +++ b/packages/integration-tests/src/tracker-linear.integration.test.ts @@ -48,50 +48,72 @@ function linearGraphQL(query: string, variables: Record): Pr } const body = JSON.stringify({ query, variables }); - return new Promise((resolve, reject) => { - const url = new URL("https://api.linear.app/graphql"); - const req = request( - { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: LINEAR_API_KEY, - "Content-Length": Buffer.byteLength(body), - }, - }, - (res) => { - const chunks: Buffer[] = []; - res.on("data", (chunk: Buffer) => chunks.push(chunk)); - res.on("end", () => { - try { - const text = Buffer.concat(chunks).toString("utf-8"); - const json = JSON.parse(text) as { - data?: T; - errors?: Array<{ message: string }>; - }; - if (json.errors?.length) { - reject(new Error(`Linear API error: ${json.errors[0].message}`)); - return; - } - resolve(json.data as T); - } catch (err) { - reject(err); - } + async function executeWithRetry(attempt = 1): Promise { + try { + return await new Promise((resolve, reject) => { + const url = new URL("https://api.linear.app/graphql"); + const req = request( + { + hostname: url.hostname, + path: url.pathname, + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: LINEAR_API_KEY, + "Content-Length": Buffer.byteLength(body), + }, + }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => chunks.push(chunk)); + res.on("end", () => { + const text = Buffer.concat(chunks).toString("utf-8"); + const statusCode = res.statusCode ?? 0; + + if (statusCode >= 500) { + reject(new Error(`Linear API ${statusCode}: ${text.slice(0, 200)}`)); + return; + } + + let json: { data?: T; errors?: Array<{ message: string }> }; + try { + json = JSON.parse(text) as { data?: T; errors?: Array<{ message: string }> }; + } catch { + reject( + new Error(`Linear API returned non-JSON (${statusCode}): ${text.slice(0, 200)}`), + ); + return; + } + + if (json.errors?.length) { + reject(new Error(`Linear API error: ${json.errors[0].message}`)); + return; + } + + resolve(json.data as T); + }); + }, + ); + + req.setTimeout(30_000, () => { + req.destroy(); + reject(new Error("Linear API request timed out")); }); - }, - ); - req.setTimeout(30_000, () => { - req.destroy(); - reject(new Error("Linear API request timed out")); - }); + req.on("error", (err) => reject(err)); + req.write(body); + req.end(); + }); + } catch (err) { + if (attempt < 3) { + await new Promise((resolve) => setTimeout(resolve, 1_000)); + return executeWithRetry(attempt + 1); + } + throw err; + } + } - req.on("error", (err) => reject(err)); - req.write(body); - req.end(); - }); + return executeWithRetry(); } // --------------------------------------------------------------------------- @@ -135,11 +157,13 @@ describe.skipIf(!canRun)("tracker-linear (integration)", () => { // Resolve the UUID for cleanup — only possible with direct API key if (LINEAR_API_KEY) { - const data = await linearGraphQL<{ issue: { id: string } }>( - `query($id: String!) { issue(id: $id) { id } }`, - { id: issueIdentifier }, - ); - issueUuid = data.issue.id; + try { + const data = await linearGraphQL<{ issue: { id: string } }>( + `query($id: String!) { issue(id: $id) { id } }`, + { id: issueIdentifier }, + ); + issueUuid = data.issue.id; + } catch {} } }, 30_000);