From c30eebe6c1117362aa3579205bf000550bba81e7 Mon Sep 17 00:00:00 2001 From: quality Date: Wed, 7 Oct 2026 17:50:02 -0400 Subject: [PATCH 1/2] test(bundle): drive /meow's parseCatImage refusals and dropped-connection retry through dist/index.js #244 drove the foreign-host row of parseCatImage only. Add the other five refusals (empty list, record without url, excessively long url, unparsable url, http:/credentialed url) as a table, and teach the cat api stub to drop the connection so the TypeError retry arm runs: one drop then an image is retried, three drops degrade to the note. Closes #243 Signed-off-by: quality --- __tests__/bundle/meow.test.ts | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/__tests__/bundle/meow.test.ts b/__tests__/bundle/meow.test.ts index b70e0280..12e58a14 100644 --- a/__tests__/bundle/meow.test.ts +++ b/__tests__/bundle/meow.test.ts @@ -23,6 +23,8 @@ interface CatResponse { status: number body?: unknown headers?: Record + // drop the connection without answering: the child's fetch rejects with a TypeError + destroy?: boolean } // a stand-in for api.thecatapi.com: answers one queued response per request, repeating the last @@ -32,6 +34,10 @@ async function startCatApi() { const server = http.createServer((req, res) => { requests.push({ path: req.url ?? '', headers: req.headers }) const next = queue.length > 1 ? queue.shift()! : queue[0] ?? { status: 500 } + if (next.destroy) { + req.socket.destroy() + return + } res.writeHead(next.status, { 'content-type': 'application/json', ...next.headers }) res.end(next.body === undefined ? '' : JSON.stringify(next.body)) }) @@ -174,6 +180,52 @@ describe('dist/index.js issue_comment /meow', () => { expect(postedComments()).toEqual([fallback]) }) + // the other parseCatImage refusals: each is a 2xx the bundle must not render, so none is retried + it.each([ + ['an empty list', [], 'cat api returned no images'], + ['a record without a url', [{ id: 'MTY3ODIyMQ' }], 'cat api returned an invalid image record'], + ['an excessively long url', [{ url: `https://cdn2.thecatapi.com/images/${'a'.repeat(4096)}.jpg` }], 'cat api returned an excessively long image url'], + ['an unparsable url', [{ url: 'not a url' }], 'cat api returned an invalid image url'], + ['an http: url', [{ url: 'http://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg' }], 'cat api returned an unusable image url'], + ['a url carrying credentials', [{ url: 'https://user:pass@cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg' }], 'cat api returned an unusable image url'], + ])('falls back to the note without retrying when the cat api returns %s', async (_, body, message) => { + cat.answer({ status: 200, body }) + gh.route('POST', `${repo}/issues/1/comments`, { status: 201, body: {} }) + + const result = await meow('/meow') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(cat.requests).toHaveLength(1) + expect(result.stdout).toContain(`::warning::Could not fetch a cat image: Error: ${message}`) + expect(postedComments()).toEqual([fallback]) + }) + + it('retries a dropped connection to the cat api and posts the image once it answers', async () => { + cat.answer({ status: 0, destroy: true }, { status: 200, body: [{ url: image }] }) + gh.route('POST', `${repo}/issues/1/comments`, { status: 201, body: {} }) + + const result = await meow('/meow') + + expect(result.status, result.stdout).toBe(0) + expect(result.stdout).not.toMatch(/::warning::/) + expect(cat.requests).toHaveLength(2) + expect(postedComments()).toEqual([`![cat](<${image}>)`]) + }) + + it('gives up after three dropped connections and posts the unavailable note; the run still succeeds', async () => { + cat.answer({ status: 0, destroy: true }) + gh.route('POST', `${repo}/issues/1/comments`, { status: 201, body: {} }) + + const result = await meow('/meow') + + expect(result.status, result.stdout).toBe(0) + expect(result.errors).toEqual([]) + expect(cat.requests).toHaveLength(3) + expect(result.stdout).toMatch(/::warning::Could not fetch a cat image: TypeError: fetch failed/) + expect(postedComments()).toEqual([fallback]) + }) + it('ignores /meow inside a fenced code block without calling either api', async () => { const result = await meow('```\n/meow\n```') From 4889a8c8db65bc4465233667f4c7010680e3f5db Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 04:05:06 -0400 Subject: [PATCH 2/2] test(bundle): keep one unusable-url row and drop the three-drops give-up case Review on #372: the http: and credentials rows both reach the same throw at meow.ts:141, so only the http: row stays; the three-dropped-connections case adds no line the single-retry and 5xx give-up cases do not already reach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- __tests__/bundle/meow.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/__tests__/bundle/meow.test.ts b/__tests__/bundle/meow.test.ts index 12e58a14..01b2ae4a 100644 --- a/__tests__/bundle/meow.test.ts +++ b/__tests__/bundle/meow.test.ts @@ -187,7 +187,6 @@ describe('dist/index.js issue_comment /meow', () => { ['an excessively long url', [{ url: `https://cdn2.thecatapi.com/images/${'a'.repeat(4096)}.jpg` }], 'cat api returned an excessively long image url'], ['an unparsable url', [{ url: 'not a url' }], 'cat api returned an invalid image url'], ['an http: url', [{ url: 'http://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg' }], 'cat api returned an unusable image url'], - ['a url carrying credentials', [{ url: 'https://user:pass@cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg' }], 'cat api returned an unusable image url'], ])('falls back to the note without retrying when the cat api returns %s', async (_, body, message) => { cat.answer({ status: 200, body }) gh.route('POST', `${repo}/issues/1/comments`, { status: 201, body: {} }) @@ -213,19 +212,6 @@ describe('dist/index.js issue_comment /meow', () => { expect(postedComments()).toEqual([`![cat](<${image}>)`]) }) - it('gives up after three dropped connections and posts the unavailable note; the run still succeeds', async () => { - cat.answer({ status: 0, destroy: true }) - gh.route('POST', `${repo}/issues/1/comments`, { status: 201, body: {} }) - - const result = await meow('/meow') - - expect(result.status, result.stdout).toBe(0) - expect(result.errors).toEqual([]) - expect(cat.requests).toHaveLength(3) - expect(result.stdout).toMatch(/::warning::Could not fetch a cat image: TypeError: fetch failed/) - expect(postedComments()).toEqual([fallback]) - }) - it('ignores /meow inside a fenced code block without calling either api', async () => { const result = await meow('```\n/meow\n```')