From c0c1f7f7c35f2fae868a72c642b68346125c1195 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 18:19:01 +0000 Subject: [PATCH 1/3] Initial plan From d5f069abc1e3e3bc5b2c2a8c36b2233251b7a409 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 18:23:50 +0000 Subject: [PATCH 2/3] fix: harden noswhere response parsing Agent-Logs-Url: https://github.com/damus-io/api/sessions/30936094-ca55-465a-9ffa-c7f7bd2d4a58 Co-authored-by: danieldaquino <24692108+danieldaquino@users.noreply.github.com> --- src/translate/noswhere.js | 30 ++++++++++--- test/noswhere.test.js | 91 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 test/noswhere.test.js diff --git a/src/translate/noswhere.js b/src/translate/noswhere.js index 9c26517..f92f5aa 100644 --- a/src/translate/noswhere.js +++ b/src/translate/noswhere.js @@ -7,7 +7,27 @@ module.exports = class NoswhereTranslator { constructor() { if (!this.#noswhereKey) throw new Error("expected NOSWHERE_KEY env var") - this.#loadTranslationLangs() + this.#loadTranslationLangs().catch((err) => { + console.error("error loading noswhere translation langs: %o", err) + }) + } + #getRequestId(resp) { + return resp.headers?.get?.("x-noswhere-request") || "unknown" + } + #getBodySnippet(body) { + if (typeof body !== "string" || body.length === 0) return "" + if (body.length <= 500) return body + return body.slice(0, 500) + "...(truncated)" + } + async #parseResponse(resp, action) { + const request_id = this.#getRequestId(resp) + const body = await resp.text() + try { + return JSON.parse(body) + } catch (err) { + console.error("noswhere %s response parse error: status=%s ok=%s request=%s body=%o", action, resp.status, resp.ok, request_id, this.#getBodySnippet(body)) + throw new Error(`error ${action}: invalid JSON response from Noswhere (request: ${request_id})`) + } } async #loadTranslationLangs() { let resp = await fetch(this.#noswhereURL + "/langs", { @@ -18,9 +38,9 @@ module.exports = class NoswhereTranslator { 'Content-Type': 'application/json' } }) - let data = await resp.json() + let data = await this.#parseResponse(resp, "getting translation langs") if (!resp.ok) { - throw new Error(`error getting translation langs: API failed with ${resp.status} ${data.error} (request: ${resp.headers.get("x-noswhere-request")})`) + throw new Error(`error getting translation langs: API failed with ${resp.status} ${data.error} (request: ${this.#getRequestId(resp)})`) } if (!data[this.#type]) { throw new Error(`type ${this.#type} not supported for translation`) @@ -46,9 +66,9 @@ module.exports = class NoswhereTranslator { }) }) - let data = await resp.json() + let data = await this.#parseResponse(resp, "translating") if (!resp.ok) { - throw new Error(`error translating: API failed with ${resp.status} ${data.error} (request: ${resp.headers.get("x-noswhere-request")})`) + throw new Error(`error translating: API failed with ${resp.status} ${data.error} (request: ${this.#getRequestId(resp)})`) } if (data.result) { diff --git a/test/noswhere.test.js b/test/noswhere.test.js new file mode 100644 index 0000000..39d781a --- /dev/null +++ b/test/noswhere.test.js @@ -0,0 +1,91 @@ +const tap = require('tap'); +const sinon = require('sinon'); + +const NOSWHERE_TRANSLATOR_PATH = require.resolve('../src/translate/noswhere.js'); + +function loadNoswhereTranslator() { + delete require.cache[NOSWHERE_TRANSLATOR_PATH]; + return require(NOSWHERE_TRANSLATOR_PATH); +} + +function mockHeaders(request_id) { + return { + get: (name) => name === 'x-noswhere-request' ? request_id : null, + }; +} + +tap.test('NoswhereTranslator constructor logs malformed langs responses without crashing', async (t) => { + process.env.NOSWHERE_KEY = 'test-key'; + + const fetchStub = sinon.stub(global, 'fetch').resolves({ + ok: true, + status: 200, + headers: mockHeaders('langs-req-1'), + text: async () => '', + }); + const errorStub = sinon.stub(console, 'error'); + + t.teardown(() => { + fetchStub.restore(); + errorStub.restore(); + delete process.env.NOSWHERE_KEY; + delete require.cache[NOSWHERE_TRANSLATOR_PATH]; + }); + + const NoswhereTranslator = loadNoswhereTranslator(); + const translator = new NoswhereTranslator(); + + await new Promise((resolve) => setImmediate(resolve)); + + t.equal(translator.canTranslate('en', 'ja'), true, 'translator stays usable when langs cannot be loaded'); + t.ok(errorStub.calledTwice, 'malformed response is logged without escaping the constructor'); + t.match(errorStub.firstCall.args[0], /noswhere %s response parse error/, 'parse failure is logged'); + t.match(errorStub.firstCall.args.slice(1).join(' '), /langs-req-1/, 'request id is included in log output'); + t.match(errorStub.firstCall.args.slice(1).join(' '), //, 'response body snippet is included in log output'); + t.match(errorStub.secondCall.args[0], /error loading noswhere translation langs/, 'constructor catch logs the failure summary'); +}); + +tap.test('NoswhereTranslator translate rejects malformed JSON responses with request details', async (t) => { + process.env.NOSWHERE_KEY = 'test-key'; + + const fetchStub = sinon.stub(global, 'fetch'); + fetchStub.onFirstCall().resolves({ + ok: true, + status: 200, + headers: mockHeaders('langs-req-2'), + text: async () => JSON.stringify({ + default: { + from: ['en'], + to: ['ja'], + }, + }), + }); + fetchStub.onSecondCall().resolves({ + ok: true, + status: 200, + headers: mockHeaders('translate-req-1'), + text: async () => '{"result"', + }); + const errorStub = sinon.stub(console, 'error'); + + t.teardown(() => { + fetchStub.restore(); + errorStub.restore(); + delete process.env.NOSWHERE_KEY; + delete require.cache[NOSWHERE_TRANSLATOR_PATH]; + }); + + const NoswhereTranslator = loadNoswhereTranslator(); + const translator = new NoswhereTranslator(); + + await new Promise((resolve) => setImmediate(resolve)); + + await t.rejects( + translator.translate('en', 'ja', 'hello'), + /error translating: invalid JSON response from Noswhere \(request: translate-req-1\)/, + 'translate surfaces a helpful parse error' + ); + t.equal(errorStub.calledOnce, true, 'translate parse failures are logged'); + t.match(errorStub.firstCall.args.slice(1).join(' '), /translate-req-1/, 'translate log includes request id'); + t.match(errorStub.firstCall.args.slice(1).join(' '), /{"result"/, 'translate log includes body snippet'); +}); From 7182be66103afb9df783f774d010637cc7797d98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 18:25:22 +0000 Subject: [PATCH 3/3] chore: align noswhere naming conventions Agent-Logs-Url: https://github.com/damus-io/api/sessions/30936094-ca55-465a-9ffa-c7f7bd2d4a58 Co-authored-by: danieldaquino <24692108+danieldaquino@users.noreply.github.com> --- src/translate/noswhere.js | 6 +++--- test/noswhere.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/translate/noswhere.js b/src/translate/noswhere.js index f92f5aa..e3dfa7c 100644 --- a/src/translate/noswhere.js +++ b/src/translate/noswhere.js @@ -20,13 +20,13 @@ module.exports = class NoswhereTranslator { return body.slice(0, 500) + "...(truncated)" } async #parseResponse(resp, action) { - const request_id = this.#getRequestId(resp) + const requestId = this.#getRequestId(resp) const body = await resp.text() try { return JSON.parse(body) } catch (err) { - console.error("noswhere %s response parse error: status=%s ok=%s request=%s body=%o", action, resp.status, resp.ok, request_id, this.#getBodySnippet(body)) - throw new Error(`error ${action}: invalid JSON response from Noswhere (request: ${request_id})`) + console.error("noswhere %s response parse error: status=%s ok=%s request=%s body=%o", action, resp.status, resp.ok, requestId, this.#getBodySnippet(body)) + throw new Error(`error ${action}: invalid JSON response from Noswhere (request: ${requestId})`) } } async #loadTranslationLangs() { diff --git a/test/noswhere.test.js b/test/noswhere.test.js index 39d781a..0b12cbd 100644 --- a/test/noswhere.test.js +++ b/test/noswhere.test.js @@ -8,9 +8,9 @@ function loadNoswhereTranslator() { return require(NOSWHERE_TRANSLATOR_PATH); } -function mockHeaders(request_id) { +function mockHeaders(requestId) { return { - get: (name) => name === 'x-noswhere-request' ? request_id : null, + get: (name) => name === 'x-noswhere-request' ? requestId : null, }; }