Skip to content

fix(tutor): propagate chatStream bridge-not-ready as a rejection (stranded spinner)#173

Closed
heznpc wants to merge 2 commits into
mainfrom
fix/chatstream-stranded-spinner
Closed

fix(tutor): propagate chatStream bridge-not-ready as a rejection (stranded spinner)#173
heznpc wants to merge 2 commits into
mainfrom
fix/chatstream-stranded-spinner

Conversation

@heznpc

@heznpc heznpc commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Confirmed bug

chatStream's try/catch (translator.js) caught the synchronous throw new Error('Bridge not ready') — raised when the Puter bridge handshake hasn't finished (reachable on cold start, slow/blocked CDN, or a non-trusted host) — and resolved to a localized error string.

But the sole production caller (sidebar-chat.js:381) discards chatStream's return value and relies on a thrown error to render the error bubble + retry button (catch at sidebar-chat.js:408). So on bridge-not-ready:

  • the caller's await resolved normally → its catch never ran → no error bubble, no retry;
  • onChunk never fired → started stayed false → the bubble was never cleared;
  • result: the "thinking…" spinner is stranded forever, no error, no retry.

The E2E helpers (tests/e2e/helpers/extension.js:412, tutor-chat.spec.js:58) even document the intended behavior as "chatStream throws 'Bridge not ready'" — the production code diverged from it.

Fix

Re-throw from the catch instead of returning a string, so the synchronous setup failure propagates as a promise rejection and the caller's existing error+retry path handles it. Promise-path failures (timeout / abort / success:false) already reject and are unaffected.

Test

Added a regression test: chatStream rejects (not resolves) when isReady is false. 493 tests · eslint · prettier green.

🤖 Generated with Claude Code

heznpc added 2 commits June 4, 2026 19:47
…K prose

A service-quality audit confirmed (reproduced from shipped data) that
restoreProtectedTerms — an unanchored replaceAll with no CJK word boundary —
corrupts CORRECT translations whenever a _protected "wrong form" is itself a
common standalone word:
  클라우드(Cloud)  -> Claude          "클라우드 컴퓨팅" -> "Claude 컴퓨팅"
  인류(humanity)   -> Anthropic       "인류의 미래"     -> "Anthropic의 미래"
  기업(enterprise) -> Enterprise      "대기업 환경"     -> "대Enterprise 환경"
  협업(collaborate)-> Cowork, etc.
This hit all four CJK locales (ko/ja/zh-CN/zh-TW) live — worse than untranslated
text, since it ships confident, brand-correct-looking but semantically wrong
output. The code comment already documented the substring class; the data was
triggering it broadly.

Fix: removed the dangerous wrong-forms — common words whose meaning differs from
the brand — from the _protected sections of all 4 CJK dictionaries, keeping the
brand transliterations and same-concept forms (클로드, 플러그인, 技能, サブエージェント).
Intended restorations still work (클로드->Claude, 서브에이전트->subagent verified);
brand-English fidelity that's lost (e.g. Enterprise occasionally shown as 기업) is
the Gemini-verify pass's job and is far preferable to corrupting real prose.
Regenerated the companion plugin's bundled data accordingly.

Tests: added a regression guard in protected-terms.test.js that runs the REAL
src/data/*.json (not a fixture) and asserts ordinary CJK prose passes through
untouched — it fails if a dangerous wrong-form is ever re-introduced. 492 tests,
eslint, check:plugin, check:locales green.

Note: the src/data diff is large because ja/zh _protected arrays were normalized
from multi-line to single-line; the only semantic change is the removed forms.
… a string

Confirmed by a service-quality audit. chatStream's try/catch caught the
synchronous `throw new Error('Bridge not ready')` (Puter bridge handshake not
finished — reachable on cold start / slow or blocked CDN / non-trusted host) and
RESOLVED to a localized error string. But the sole caller (sidebar-chat.js:381)
discards chatStream's return value and relies on a thrown error to render the
error bubble + retry button. So on bridge-not-ready: the caller's await resolved
normally, its catch never ran, onChunk never fired — leaving the "thinking…"
spinner stranded forever with no error and no retry. The E2E helpers even
document the intended behavior as "chatStream throws 'Bridge not ready'".

Fix: re-throw from the catch instead of returning a string, so the synchronous
setup failure propagates as a promise rejection and the caller's existing
error+retry path handles it. Promise-path failures (timeout, abort,
success:false) already reject and are unaffected.

Added a regression test asserting chatStream rejects (not resolves) when
isReady is false. 493 tests, eslint, prettier green.
@heznpc heznpc enabled auto-merge (squash) June 4, 2026 11:04
@heznpc

heznpc commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Superseded. This branch was accidentally created on top of #172's protected-terms changes (the background 'checkout main' hadn't completed), so it bundled both fixes and failed the same protected-terms E2E. #172 carries the protected-terms fix + its E2E update; the chatStream fix (commit fc32537) will be re-opened cleanly from main as its own PR.

@heznpc heznpc closed this Jun 4, 2026
auto-merge was automatically disabled June 4, 2026 11:27

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant