From b227c17df5fd90f51ad01f034f23bfca8c2912a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:50:17 +0000 Subject: [PATCH] Cover abortSignalFromRequestBehaviour request cancellation `abortSignalFromRequestBehaviour` decides whether an HTTP request can be cancelled, and is used by both shopifyFetch and the GraphQL client, but had no direct tests: its sibling `requestMode` was covered while all three of its branches were only exercised indirectly through shopifyFetch. Cover the timeout, disabled, factory-function and supplied-signal cases, and assert that each call returns a fresh signal so a retried request is not born aborted. `AbortSignal.timeout` is backed by libuv rather than a JS timer, so the timeout cases use real timers and await the abort event instead of sleeping on a fixed delay. Co-Authored-By: Claude Opus 4.8 --- packages/cli-kit/src/public/node/http.test.ts | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/cli-kit/src/public/node/http.test.ts b/packages/cli-kit/src/public/node/http.test.ts index f848c4e76a6..333892046e1 100644 --- a/packages/cli-kit/src/public/node/http.test.ts +++ b/packages/cli-kit/src/public/node/http.test.ts @@ -1,4 +1,4 @@ -import {downloadFile, shopifyFetch, formData, requestMode, fetch} from './http.js' +import {downloadFile, shopifyFetch, formData, requestMode, fetch, abortSignalFromRequestBehaviour} from './http.js' import {mockAndCaptureOutput} from './testing/output.js' import {fileExists, inTemporaryDirectory, readFile} from './fs.js' import {joinPath} from './path.js' @@ -374,3 +374,71 @@ describe('requestMode', () => { }) }) }) + +describe('abortSignalFromRequestBehaviour', () => { + // `AbortSignal.timeout` is backed by libuv rather than a JS timer, so faked timers never + // fire it. These tests use real timers and wait for the abort event instead of sleeping. + const timeoutBehaviour = { + useNetworkLevelRetry: false, + useAbortSignal: true, + timeoutMs: 10, + } as const + + const abortOf = async (signal: ReturnType) => + new Promise((resolve) => signal?.addEventListener('abort', () => resolve())) + + test('cancels the request once the configured timeout elapses', async () => { + vi.useRealTimers() + + const signal = abortSignalFromRequestBehaviour(timeoutBehaviour) + expect(signal?.aborted).toBe(false) + + await abortOf(signal) + + expect(signal?.aborted).toBe(true) + }) + + test('leaves the request uncancelled when no abort signal is wanted', () => { + expect( + abortSignalFromRequestBehaviour({ + useNetworkLevelRetry: false, + useAbortSignal: false, + }), + ).toBeUndefined() + }) + + test('uses the signal a factory function produces', () => { + const provided = AbortSignal.timeout(DURATION_UNTIL_ABORT_IS_SEEN) + + const signal = abortSignalFromRequestBehaviour({ + useNetworkLevelRetry: false, + useAbortSignal: () => provided, + }) + + expect(signal).toBe(provided) + }) + + test('uses a signal supplied directly', () => { + const provided = AbortSignal.timeout(DURATION_UNTIL_ABORT_IS_SEEN) + + const signal = abortSignalFromRequestBehaviour({ + useNetworkLevelRetry: false, + useAbortSignal: provided, + }) + + expect(signal).toBe(provided) + }) + + test('returns a fresh signal on each call so a retried request is not born aborted', async () => { + vi.useRealTimers() + + const first = abortSignalFromRequestBehaviour(timeoutBehaviour) + await abortOf(first) + expect(first?.aborted).toBe(true) + + const second = abortSignalFromRequestBehaviour(timeoutBehaviour) + + expect(second).not.toBe(first) + expect(second?.aborted).toBe(false) + }) +})