From 5464ddd430ff485d9cc3eb69d52fa97ae584bb9a Mon Sep 17 00:00:00 2001 From: Yudhi Armyndharis Date: Thu, 25 Jun 2026 23:00:29 +0700 Subject: [PATCH] fix(group-translate): parse res.body instead of res.json() (translations were a silent no-op) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sandboxed ctx.net.fetch returns the response body as a string with no .json() method (functions can't cross the worker structuredClone boundary), so res.json() threw on every call and the client failed open — translations never applied. Parse res.body directly. Tests now use the real runtime shape. Closes #10 --- README.md | 2 +- group-translate/CHANGELOG.md | 9 +++++++++ group-translate/README.md | 4 ++-- group-translate/libretranslate.client.test.ts | 19 +++++++++++-------- group-translate/libretranslate.client.ts | 4 +++- group-translate/manifest.json | 2 +- plugins.json | 6 +++--- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f74cde8..5228c84 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This repository provides: | [`after-hours`](./after-hours) | Auto-replies with a configurable away/closing message to messages received outside business hours. | 0.1.2 | stable | | [`chat-flow`](./chat-flow) | Interactive, stateful auto-reply: a trigger word starts a greeting + numbered menu, replies traverse a configurable menu tree, and per-chat state expires after 15 minutes. | 1.0.3 | stable | | [`faq-bot`](./faq-bot) | Auto-replies to inbound WhatsApp messages from configurable FAQ keyword/regex rules. | 0.1.4 | stable | -| [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.0.3 | stable | +| [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.0.4 | stable | | [`gsheets-logger`](./gsheets-logger) | Logs WhatsApp message events to a Google Sheet via a service account. | 0.2.2 | stable | | [`voice-transcription`](./voice-transcription) | Transcribes inbound WhatsApp voice notes to text via an OpenAI-compatible speech-to-text backend (self-hosted Speaches/faster-whisper or hosted Groq/OpenAI) and delivers a `message.transcription` event to your webhook — so bots and AI can read and reply to audio. Off the message-delivery path; disabled until enabled. | 1.0.0 | beta | diff --git a/group-translate/CHANGELOG.md b/group-translate/CHANGELOG.md index 410ad99..edcacff 100644 --- a/group-translate/CHANGELOG.md +++ b/group-translate/CHANGELOG.md @@ -8,6 +8,15 @@ The version here always matches `manifest.json`'s `version`. ## [Unreleased] +## [1.0.4] — 2026-06-25 + +### Fixed + +- Translations now actually apply. The LibreTranslate client read the response with `res.json()`, but the + sandboxed `ctx.net.fetch` returns the body as a string and provides no `.json()` method (functions can't + cross the worker boundary) — so every call threw and failed open, a silent no-op. The client now parses + `res.body` directly. + ## [1.0.3] — 2026-06-23 ### Fixed diff --git a/group-translate/README.md b/group-translate/README.md index 5945c2e..a5ba569 100644 --- a/group-translate/README.md +++ b/group-translate/README.md @@ -13,8 +13,8 @@ | Field | Value | | ----- | ----- | | **Identifier** | `group-translate` | -| **Version** | 1.0.3 | -| **Released** | 2026-06-23 | +| **Version** | 1.0.4 | +| **Released** | 2026-06-25 | | **Status** | stable | | **Author** | Yudhi Armyndharis | | **License** | MIT | diff --git a/group-translate/libretranslate.client.test.ts b/group-translate/libretranslate.client.test.ts index 0d81c77..59d02ef 100644 --- a/group-translate/libretranslate.client.test.ts +++ b/group-translate/libretranslate.client.test.ts @@ -3,15 +3,18 @@ import assert from 'node:assert/strict'; import type { PluginNetCapability, PluginNetResponse } from '../types/openwa'; import { LibreTranslateClient } from './libretranslate.client.ts'; -function res(partial: { ok?: boolean; status?: number; json?: () => Promise }): PluginNetResponse { +function res(partial: { ok?: boolean; status?: number; body?: unknown }): PluginNetResponse { return { ok: partial.ok ?? true, status: partial.status ?? 200, headers: {}, - body: '', + // The sandbox runtime returns the response body as a string and provides NO .json() (functions + // can't cross the worker structuredClone boundary). The client must JSON.parse(res.body). + body: partial.body === undefined ? '{}' : JSON.stringify(partial.body), text: async () => '', - // PluginNetResponse.json is generic (() => Promise); a concrete fake needs the cast. - json: (partial.json ?? (async () => ({}))) as PluginNetResponse['json'], + json: (async () => { + throw new Error('res.json() is not available in the sandbox runtime'); + }) as PluginNetResponse['json'], arrayBuffer: async () => new ArrayBuffer(0), }; } @@ -31,20 +34,20 @@ function fakeNet(handlers: Array<(url: string) => Promise>) { } test('translate posts and returns translatedText on success', async () => { - const { net, calls } = fakeNet([async () => res({ json: async () => ({ translatedText: 'hola' }) })]); + const { net, calls } = fakeNet([async () => res({ body: { translatedText: 'hola' } })]); const c = new LibreTranslateClient({ url: 'http://lt:7001/', timeoutMs: 4000, net }); assert.equal(await c.translate('hi', 'en', 'es'), 'hola'); assert.equal(calls[0], 'http://lt:7001/translate'); // trailing slash trimmed }); test('translate throws when the response lacks a translatedText string', async () => { - const { net } = fakeNet([async () => res({ json: async () => ({}) })]); // partial/empty body + const { net } = fakeNet([async () => res({ body: {} })]); // partial/empty body const c = new LibreTranslateClient({ url: 'http://lt:7001', timeoutMs: 4000, net }); await assert.rejects(c.translate('hi', 'en', 'es'), /translatedText/); }); test('detect returns the top result', async () => { - const { net } = fakeNet([async () => res({ json: async () => [{ language: 'en', confidence: 0.9 }] })]); + const { net } = fakeNet([async () => res({ body: [{ language: 'en', confidence: 0.9 }] })]); const c = new LibreTranslateClient({ url: 'http://lt:7001', timeoutMs: 4000, net }); assert.deepEqual(await c.detect('hello'), { lang: 'en', confidence: 0.9 }); }); @@ -69,7 +72,7 @@ test('opens the circuit after the failure threshold and short-circuits the next test('a success resets the consecutive-failure counter', async () => { const fail = async () => { throw new Error('boom'); }; - const ok = async () => res({ json: async () => ({ translatedText: 'x' }) }); + const ok = async () => res({ body: { translatedText: 'x' } }); const { net } = fakeNet([fail, ok]); const c = new LibreTranslateClient({ url: 'http://lt:7001', timeoutMs: 4000, net, failureThreshold: 3 }); await assert.rejects(c.translate('a', 'en', 'es')); diff --git a/group-translate/libretranslate.client.ts b/group-translate/libretranslate.client.ts index ded0d44..d974a96 100644 --- a/group-translate/libretranslate.client.ts +++ b/group-translate/libretranslate.client.ts @@ -83,7 +83,9 @@ export class LibreTranslateClient implements Translator { if (!res.ok) { throw new Error(`LibreTranslate ${path} -> HTTP ${res.status}`); } - const data = await res.json(); + // The sandbox runtime hands back the body as a string (no res.json() — functions can't cross the + // worker boundary), so parse it here. A malformed/empty body throws and is counted as a failure. + const data = JSON.parse(res.body); this.consecutiveFailures = 0; return data; } catch (err) { diff --git a/group-translate/manifest.json b/group-translate/manifest.json index a57ff48..18b11c5 100644 --- a/group-translate/manifest.json +++ b/group-translate/manifest.json @@ -1,7 +1,7 @@ { "id": "group-translate", "name": "Group Auto-Translation", - "version": "1.0.3", + "version": "1.0.4", "type": "extension", "main": "dist/index.js", "description": "Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled.", diff --git a/plugins.json b/plugins.json index d9ad249..4d384dd 100644 --- a/plugins.json +++ b/plugins.json @@ -540,7 +540,7 @@ { "id": "group-translate", "name": "Group Auto-Translation", - "version": "1.0.3", + "version": "1.0.4", "type": "extension", "status": "stable", "description": "Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled.", @@ -556,11 +556,11 @@ ], "minOpenWAVersion": "0.7.0", "testedOpenWAVersion": "0.7.0", - "releasedAt": "2026-06-23", + "releasedAt": "2026-06-25", "repoPath": "group-translate", "repoUrl": "https://github.com/rmyndharis/OpenWA-plugins", "homepage": "https://github.com/rmyndharis/OpenWA-plugins/tree/main/group-translate", - "download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/group-translate-v1.0.3/group-translate.zip", + "download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/group-translate-v1.0.4/group-translate.zip", "i18n": { "es": { "name": "Traducción Automática de Grupos",