From 68afa9d6d5d40bc9fc622e246cb4c67806f920ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:40:24 +0000 Subject: [PATCH 1/3] Initial plan From 612eda9cf1562bf88a8e9df0a4c21252bdb582b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:46:01 +0000 Subject: [PATCH 2/3] Return watch abort controller before network response --- src/watch.ts | 75 ++++++++++++++++++++++++----------------------- src/watch_test.ts | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/watch.ts b/src/watch.ts index 85058117e2e..21bc5a8c277 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -44,7 +44,6 @@ export class Watch { const signal = AbortSignal.any([controller.signal, timeoutSignal]); const ctx = new RequestContext(watchURL.toString(), HttpMethod.GET); - await this.config.applySecurityAuthentication(ctx); let doneCalled: boolean = false; const doneCallOnce = (err: any) => { @@ -59,45 +58,49 @@ export class Watch { } }; - try { - const response = await fetch(watchURL, { - method: 'GET', - headers: ctx.getHeaders(), - dispatcher: ctx.getDispatcher(), - signal, - }); + (async () => { + try { + await this.config.applySecurityAuthentication(ctx); - if (response.status === 200) { - const body = Readable.fromWeb(response.body! as any); + const response = await fetch(watchURL, { + method: 'GET', + headers: ctx.getHeaders(), + dispatcher: ctx.getDispatcher(), + signal, + }); - body.on('error', doneCallOnce); - body.on('close', () => doneCallOnce(null)); - body.on('finish', () => doneCallOnce(null)); + if (response.status === 200) { + const body = Readable.fromWeb(response.body! as any); - const lines = createInterface(body); - lines.on('error', doneCallOnce); - lines.on('close', () => doneCallOnce(null)); - lines.on('finish', () => doneCallOnce(null)); - lines.on('line', (line) => { - try { - const data = JSON.parse(line.toString()); - callback(data.type, data.object, data); - } catch { - // ignore parse errors - } - }); - } else { - const statusText = - response.statusText || STATUS_CODES[response.status] || 'Internal Server Error'; - const error = new Error(statusText) as Error & { - statusCode: number | undefined; - }; - error.statusCode = response.status; - throw error; + body.on('error', doneCallOnce); + body.on('close', () => doneCallOnce(null)); + body.on('finish', () => doneCallOnce(null)); + + const lines = createInterface(body); + lines.on('error', doneCallOnce); + lines.on('close', () => doneCallOnce(null)); + lines.on('finish', () => doneCallOnce(null)); + lines.on('line', (line) => { + try { + const data = JSON.parse(line.toString()); + callback(data.type, data.object, data); + } catch { + // ignore parse errors + } + }); + } else { + const statusText = + response.statusText || STATUS_CODES[response.status] || 'Internal Server Error'; + const error = new Error(statusText) as Error & { + statusCode: number | undefined; + }; + error.statusCode = response.status; + throw error; + } + } catch (err) { + doneCallOnce(err); } - } catch (err) { - doneCallOnce(err); - } + })(); return controller; } diff --git a/src/watch_test.ts b/src/watch_test.ts index 4aad61a05d9..f86d22338b0 100644 --- a/src/watch_test.ts +++ b/src/watch_test.ts @@ -65,6 +65,10 @@ describe('Watch', () => { let doneCalled = false; let doneErr: any; + let doneResolve: () => void; + const donePromise = new Promise((resolve) => { + doneResolve = resolve; + }); await watch.watch( path, @@ -73,8 +77,10 @@ describe('Watch', () => { (err: any) => { doneCalled = true; doneErr = err; + doneResolve(); }, ); + await donePromise; strictEqual(doneCalled, true); strictEqual(doneErr.toString(), 'Error: Internal Server Error'); mockAgent.assertNoPendingInterceptors(); @@ -386,6 +392,53 @@ describe('Watch', () => { strictEqual(doneErr.name, 'TimeoutError'); }); + it('should return abort controller before receiving response data', async (t) => { + const kc = await setupMockSystem(t, (_req: any, _res: any) => { + // Keep connection open without responding immediately. + }); + const watch = new Watch(kc); + + let doneErr: any; + + let doneResolve: () => void; + const donePromise = new Promise((resolve) => { + doneResolve = resolve; + }); + + const controllerPromise = watch.watch( + '/some/path/to/object', + {}, + () => { + throw new Error('Unexpected data received'); + }, + (err: any) => { + doneErr = err; + doneResolve(); + }, + ); + + const controller = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('watch() did not return AbortController in time')); + }, 100); + + controllerPromise.then( + (value) => { + clearTimeout(timeout); + resolve(value); + }, + (err) => { + clearTimeout(timeout); + reject(err); + }, + ); + }); + + controller.abort(); + await donePromise; + strictEqual(doneErr?.name, 'AbortError'); + }); + it('should throw on empty config', async () => { const kc = new KubeConfig(); const watch = new Watch(kc); From e785fa2c98ab9788a1fd8bf793d7ab589efd0406 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:51:55 +0000 Subject: [PATCH 3/3] Make watch return abort controller immediately --- src/watch.ts | 5 +++-- src/watch_test.ts | 34 +++++++++++++--------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/watch.ts b/src/watch.ts index 21bc5a8c277..12e2df6a2c9 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -58,7 +58,7 @@ export class Watch { } }; - (async () => { + const startWatch = async (): Promise => { try { await this.config.applySecurityAuthentication(ctx); @@ -100,7 +100,8 @@ export class Watch { } catch (err) { doneCallOnce(err); } - })(); + }; + startWatch().catch(doneCallOnce); return controller; } diff --git a/src/watch_test.ts b/src/watch_test.ts index f86d22338b0..b470f0c3c24 100644 --- a/src/watch_test.ts +++ b/src/watch_test.ts @@ -65,7 +65,7 @@ describe('Watch', () => { let doneCalled = false; let doneErr: any; - let doneResolve: () => void; + let doneResolve!: () => void; const donePromise = new Promise((resolve) => { doneResolve = resolve; }); @@ -176,7 +176,7 @@ describe('Watch', () => { const watch = new Watch(kc); let doneCalled = 0; - let doneResolve: () => void; + let doneResolve!: () => void; const donePromise = new Promise((resolve) => { doneResolve = resolve; @@ -370,7 +370,7 @@ describe('Watch', () => { let doneErr: any; - let doneResolve: () => void; + let doneResolve!: () => void; const donePromise = new Promise((resolve) => { doneResolve = resolve; }); @@ -394,13 +394,13 @@ describe('Watch', () => { it('should return abort controller before receiving response data', async (t) => { const kc = await setupMockSystem(t, (_req: any, _res: any) => { - // Keep connection open without responding immediately. + // Intentionally do not write headers/body so fetch stays pending. }); const watch = new Watch(kc); let doneErr: any; - let doneResolve: () => void; + let doneResolve!: () => void; const donePromise = new Promise((resolve) => { doneResolve = resolve; }); @@ -417,22 +417,14 @@ describe('Watch', () => { }, ); - const controller = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('watch() did not return AbortController in time')); - }, 100); - - controllerPromise.then( - (value) => { - clearTimeout(timeout); - resolve(value); - }, - (err) => { - clearTimeout(timeout); - reject(err); - }, - ); - }); + const controller = await Promise.race([ + controllerPromise, + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('watch() did not return AbortController in time')); + }, 100); + }), + ]); controller.abort(); await donePromise;