From 2538c0092058e91c0c32f073a5d7953f289f81fc Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 13:54:34 +0100 Subject: [PATCH 01/11] diag: capture playwright traces for define.spec (temporary) --- .github/workflows/frontend-tests.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 5e97e61..18e9418 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -69,4 +69,16 @@ jobs: done cd src pnpm exec playwright install chromium --with-deps - pnpm run test-ui --project=chromium + # DIAGNOSTIC: only ep_define spec, single retry, capture artifacts + pnpm run test-ui --project=chromium --retries=1 --max-failures=4 \ + define.spec || true + - name: Upload Playwright traces (diagnostic) + if: always() + uses: actions/upload-artifact@v4 + with: + name: pw-traces + path: | + etherpad-lite/src/test-results/** + etherpad-lite/src/playwright-report/** + if-no-files-found: warn + retention-days: 3 From f737fe58058a00fea7dbc8cc537eb13f31ccf93c Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:07:22 +0100 Subject: [PATCH 02/11] diag: capture editor bundle + dev server log --- .github/workflows/frontend-tests.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 18e9418..5dc96af 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -56,7 +56,7 @@ jobs: working-directory: ./etherpad-lite shell: bash run: | - pnpm run dev & + pnpm run dev > /tmp/devserver.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -67,6 +67,19 @@ jobs: while [ $(($(now) - $start)) -le 30 ] && ! can_connect; do sleep 1 done + # DIAGNOSTIC: probe the editor bundle + a pad page directly + sleep 8 + echo "===== /watch/pad bundle size =====" + curl -s "http://localhost:9001/watch/pad?hash=x" -o /tmp/watchpad.js + wc -c /tmp/watchpad.js + echo "ep_define occurrences in bundle: $(grep -c ep_define /tmp/watchpad.js || true)" + head -c 400 /tmp/watchpad.js; echo + echo "===== first 200 lines of dev server log =====" + head -200 /tmp/devserver.log + echo "===== esbuild/build errors in dev log =====" + grep -iE "esbuild|could not resolve|transform failed|build failed|✘|error" /tmp/devserver.log | head -40 || true + cp /tmp/devserver.log etherpad-lite/devserver.log || true + cp /tmp/watchpad.js etherpad-lite/watchpad.js || true cd src pnpm exec playwright install chromium --with-deps # DIAGNOSTIC: only ep_define spec, single retry, capture artifacts @@ -80,5 +93,7 @@ jobs: path: | etherpad-lite/src/test-results/** etherpad-lite/src/playwright-report/** + etherpad-lite/devserver.log + etherpad-lite/watchpad.js if-no-files-found: warn retention-days: 3 From ba84111818877ee8badaeb3f876d71972ca022ab Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:16:42 +0100 Subject: [PATCH 03/11] diag: capture pad-page status + full server log during tests --- .github/workflows/frontend-tests.yml | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 5dc96af..34010bc 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -67,24 +67,27 @@ jobs: while [ $(($(now) - $start)) -le 30 ] && ! can_connect; do sleep 1 done - # DIAGNOSTIC: probe the editor bundle + a pad page directly - sleep 8 - echo "===== /watch/pad bundle size =====" - curl -s "http://localhost:9001/watch/pad?hash=x" -o /tmp/watchpad.js - wc -c /tmp/watchpad.js - echo "ep_define occurrences in bundle: $(grep -c ep_define /tmp/watchpad.js || true)" - head -c 400 /tmp/watchpad.js; echo - echo "===== first 200 lines of dev server log =====" - head -200 /tmp/devserver.log - echo "===== esbuild/build errors in dev log =====" - grep -iE "esbuild|could not resolve|transform failed|build failed|✘|error" /tmp/devserver.log | head -40 || true - cp /tmp/devserver.log etherpad-lite/devserver.log || true - cp /tmp/watchpad.js etherpad-lite/watchpad.js || true + # DIAGNOSTIC: directly load a pad page server-side, capture status + sleep 15 + echo "===== curl /p/diagpad (status) =====" + curl -s -o /tmp/padpage.html -w "HTTP %{http_code} size=%{size_download}\n" "http://localhost:9001/p/diagpad12345" + echo "--- pad page entrypoint script tag ---" + grep -oE '/watch/pad[^"]*' /tmp/padpage.html | head -1 + echo "===== curl /watch/pad =====" + curl -s "http://localhost:9001/watch/pad?hash=x" -o /tmp/watchpad.js -w "HTTP %{http_code} size=%{size_download}\n" + echo "===== server log: routeHandlers / errors so far =====" + grep -nE "routeHandlers|is not a function|\[ERROR\]" /tmp/devserver.log | grep -viE "update check|fetch failed" | head -30 || true cd src pnpm exec playwright install chromium --with-deps # DIAGNOSTIC: only ep_define spec, single retry, capture artifacts pnpm run test-ui --project=chromium --retries=1 --max-failures=4 \ define.spec || true + echo "===== server log AFTER tests: errors during pad loads =====" + grep -nE "routeHandlers|is not a function|\[ERROR\]|\[ENTER\]|\[LEAVE\]|CLIENT_READY|disconnect" /tmp/devserver.log | grep -viE "update check|fetch failed" | tail -40 || true + - name: Stage diagnostic logs + if: always() + working-directory: ./etherpad-lite + run: cp /tmp/devserver.log ./devserver.log || true - name: Upload Playwright traces (diagnostic) if: always() uses: actions/upload-artifact@v4 @@ -94,6 +97,6 @@ jobs: etherpad-lite/src/test-results/** etherpad-lite/src/playwright-report/** etherpad-lite/devserver.log - etherpad-lite/watchpad.js + if-no-files-found: warn retention-days: 3 From 6b2bb4dbba26bc06d9b5a2a3ab24d9524a0b3a11 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:25:10 +0100 Subject: [PATCH 04/11] diag: instrumented pad-load probe to find silent client hang --- .github/workflows/frontend-tests.yml | 35 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 34010bc..8b5235e 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -79,11 +79,36 @@ jobs: grep -nE "routeHandlers|is not a function|\[ERROR\]" /tmp/devserver.log | grep -viE "update check|fetch failed" | head -30 || true cd src pnpm exec playwright install chromium --with-deps - # DIAGNOSTIC: only ep_define spec, single retry, capture artifacts - pnpm run test-ui --project=chromium --retries=1 --max-failures=4 \ - define.spec || true - echo "===== server log AFTER tests: errors during pad loads =====" - grep -nE "routeHandlers|is not a function|\[ERROR\]|\[ENTER\]|\[LEAVE\]|CLIENT_READY|disconnect" /tmp/devserver.log | grep -viE "update check|fetch failed" | tail -40 || true + # DIAGNOSTIC: instrumented pad load to find the silent client hang + cat > /tmp/probe.cjs <<'PROBE' + const { chromium } = require(process.cwd() + '/node_modules/playwright'); + (async () => { + const browser = await chromium.launch(); + const page = await (await browser.newContext()).newPage(); + page.on('console', m => console.log('CONSOLE['+m.type()+']', m.text())); + page.on('pageerror', e => console.log('PAGEERROR', e.message, '\n', e.stack)); + await page.addInitScript(() => { + window.addEventListener('error', e => console.log('WINERROR:', e.message, (e.error&&e.error.stack)||'')); + window.addEventListener('unhandledrejection', e => { + const r = e.reason; console.log('UNHANDLEDREJECTION:', (r&&(r.stack||r.message))||String(r)); + }); + }); + await page.goto('http://localhost:9001/p/diagprobe'+Date.now(), {waitUntil:'domcontentloaded', timeout:30000}).catch(e=>console.log('goto:'+e.message)); + await page.waitForTimeout(35000); + const state = await page.evaluate(() => ({ + aceOuter: !!document.querySelector('iframe[name=ace_outer]'), + editorContainer: document.querySelector('#editorcontainer') && document.querySelector('#editorcontainer').className, + loadingVisible: !!(document.querySelector('#editorloadingbox') && document.querySelector('#editorloadingbox').offsetParent), + receivedClientVars: !!window.clientVars, + padPresent: !!window.pad, + })).catch(e=>'eval:'+e.message); + console.log('=== FINAL STATE ===', JSON.stringify(state)); + await browser.close(); + })(); + PROBE + echo "===== INSTRUMENTED PROBE OUTPUT =====" + node /tmp/probe.cjs 2>&1 | grep -vE "Could not find string" | head -120 || true + echo "===== END PROBE =====" - name: Stage diagnostic logs if: always() working-directory: ./etherpad-lite From 55b36f9b00e343991ac29209ebda0d520d557d61 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:27:03 +0100 Subject: [PATCH 05/11] diag: fix playwright require path (@playwright/test) --- .github/workflows/frontend-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 8b5235e..8db4422 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -81,7 +81,7 @@ jobs: pnpm exec playwright install chromium --with-deps # DIAGNOSTIC: instrumented pad load to find the silent client hang cat > /tmp/probe.cjs <<'PROBE' - const { chromium } = require(process.cwd() + '/node_modules/playwright'); + const { chromium } = require(process.cwd() + '/node_modules/@playwright/test'); (async () => { const browser = await chromium.launch(); const page = await (await browser.newContext()).newPage(); From 1eb833ba09886429917151b27f4cda96437c1f5b Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:31:37 +0100 Subject: [PATCH 06/11] diag: richer client state capture --- .github/workflows/frontend-tests.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 8db4422..0070b45 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -97,10 +97,16 @@ jobs: await page.waitForTimeout(35000); const state = await page.evaluate(() => ({ aceOuter: !!document.querySelector('iframe[name=ace_outer]'), + iframeCount: document.querySelectorAll('iframe').length, editorContainer: document.querySelector('#editorcontainer') && document.querySelector('#editorcontainer').className, loadingVisible: !!(document.querySelector('#editorloadingbox') && document.querySelector('#editorloadingbox').offsetParent), receivedClientVars: !!window.clientVars, - padPresent: !!window.pad, + padPresent: typeof window.pad, + padeditor: typeof window.padeditor, + padeditorAce: !!(window.padeditor && window.padeditor.ace), + winKeys: Object.keys(window).filter(k=>/pad|ace|client|collab|handshake/i.test(k)).slice(0,40), + connMsg: (document.querySelector('#connectivity, #editorloadingbox .editorloadingbox-message:not([hidden])') || {}).innerText, + bodyModal: (document.querySelector('.modaldialog, #connectivity .visible, [id*=error]') || {}).innerText, })).catch(e=>'eval:'+e.message); console.log('=== FINAL STATE ===', JSON.stringify(state)); await browser.close(); From 359b5a1cc581236fc73ff578c3e2b80cbdee175b Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:37:00 +0100 Subject: [PATCH 07/11] diag: strip ALL hooks (bisect code vs install) --- ep.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ep.json b/ep.json index c365ab3..c3f342f 100644 --- a/ep.json +++ b/ep.json @@ -1,15 +1,7 @@ { "parts": [ { - "name": "ep_define", - "client_hooks": { - "postAceInit": "ep_define/static/js/index:postAceInit", - "handleClientMessage_CUSTOM": "ep_define/static/js/index" - }, - "hooks": { - "eejsBlock_editbarMenuRight":"ep_define/eejs", - "handleMessage": "ep_define/handleMessage" - } + "name": "ep_define" } ] } From f3ec5eb6394b20c3b61d86f0ddaaf4def58a4962 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:39:48 +0100 Subject: [PATCH 08/11] diag: client_hooks only (drop server hooks) --- ep.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ep.json b/ep.json index c3f342f..c9303a3 100644 --- a/ep.json +++ b/ep.json @@ -1,7 +1,11 @@ { "parts": [ { - "name": "ep_define" + "name": "ep_define", + "client_hooks": { + "postAceInit": "ep_define/static/js/index:postAceInit", + "handleClientMessage_CUSTOM": "ep_define/static/js/index" + } } ] } From 003a7dfdfb05bb11021f0e307c7ea158e255f27f Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:42:24 +0100 Subject: [PATCH 09/11] diag: handleMessage server hook only --- ep.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ep.json b/ep.json index c9303a3..eb0b97f 100644 --- a/ep.json +++ b/ep.json @@ -2,9 +2,8 @@ "parts": [ { "name": "ep_define", - "client_hooks": { - "postAceInit": "ep_define/static/js/index:postAceInit", - "handleClientMessage_CUSTOM": "ep_define/static/js/index" + "hooks": { + "handleMessage": "ep_define/handleMessage" } } ] From a3eb32237a13834128486964c830319c220ac6f4 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:45:27 +0100 Subject: [PATCH 10/11] diag: no-op handleMessage (no wordnet require) --- ep.json | 2 +- handleMessage_noop.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 handleMessage_noop.js diff --git a/ep.json b/ep.json index eb0b97f..42af712 100644 --- a/ep.json +++ b/ep.json @@ -3,7 +3,7 @@ { "name": "ep_define", "hooks": { - "handleMessage": "ep_define/handleMessage" + "handleMessage": "ep_define/handleMessage_noop" } } ] diff --git a/handleMessage_noop.js b/handleMessage_noop.js new file mode 100644 index 0000000..c277bb1 --- /dev/null +++ b/handleMessage_noop.js @@ -0,0 +1,4 @@ +'use strict'; +exports.handleMessage = async (hookName, context) => { + return; +}; From ec89cecb2a772f329cb0e7cd9af9f731be535499 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:49:07 +0100 Subject: [PATCH 11/11] diag: test lazy-require wordnet fix (full plugin restored) --- ep.json | 7 ++++++- handleMessage.js | 28 +++++++++++++++++++--------- handleMessage_noop.js | 4 ---- 3 files changed, 25 insertions(+), 14 deletions(-) delete mode 100644 handleMessage_noop.js diff --git a/ep.json b/ep.json index 42af712..c365ab3 100644 --- a/ep.json +++ b/ep.json @@ -2,8 +2,13 @@ "parts": [ { "name": "ep_define", + "client_hooks": { + "postAceInit": "ep_define/static/js/index:postAceInit", + "handleClientMessage_CUSTOM": "ep_define/static/js/index" + }, "hooks": { - "handleMessage": "ep_define/handleMessage_noop" + "eejsBlock_editbarMenuRight":"ep_define/eejs", + "handleMessage": "ep_define/handleMessage" } } ] diff --git a/handleMessage.js b/handleMessage.js index e273361..15a72eb 100644 --- a/handleMessage.js +++ b/handleMessage.js @@ -1,22 +1,29 @@ 'use strict'; -const wordnet = require('wordnet'); - +// `wordnet` is a server-only package (it reads the WordNet database off disk). +// It MUST NOT be required at module top level: Etherpad's client bundler pulls +// plugin hook modules into the browser bundle, and evaluating wordnet's module +// body in the browser silently hangs the editor bootstrap (ace_outer is never +// created, no error is thrown). Requiring it lazily inside the hook keeps the +// module body from ever running client-side while still working on the server. +let wordnet = null; let initPromise = null; const ensureInit = () => { + if (!wordnet) wordnet = require('wordnet'); if (!initPromise) initPromise = wordnet.init(); return initPromise; }; const sendDefinition = (context, definitions) => { - context.client.json.send({ + const payload = context.message.data.payload; + context.socket.emit('message', { type: 'COLLABROOM', data: { type: 'CUSTOM', payload: { action: 'recieveDefineMessage', - authorId: context.message.data.message.myAuthorId, - padId: context.message.data.message.padId, + authorId: payload.myAuthorId, + padId: payload.padId, message: definitions, }, }, @@ -24,13 +31,16 @@ const sendDefinition = (context, definitions) => { }; exports.handleMessage = async (hookName, context) => { - if (!(context && context.message && context.message.data && - context.message.data.type && context.message.data.action === 'sendDefineMessage')) { - return [null]; + const payload = context?.message?.data?.payload; + if (!(context?.message?.type === 'COLLABROOM' && + context.message.data?.type === 'CLIENT_MESSAGE' && + payload?.type === 'define' && + payload?.action === 'sendDefineMessage')) { + return; } try { await ensureInit(); - const definitions = await wordnet.lookup(context.message.data.message, true); + const definitions = await wordnet.lookup(payload.message, true); sendDefinition(context, definitions); } catch (err) { // wordnet 2.x rejects when the word isn't found; surface a "no diff --git a/handleMessage_noop.js b/handleMessage_noop.js deleted file mode 100644 index c277bb1..0000000 --- a/handleMessage_noop.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; -exports.handleMessage = async (hookName, context) => { - return; -};