From 53e4ac329d69d93829e4109470dae4d3947d3e5d Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 22:10:00 -0700 Subject: [PATCH] feat(channel): surface 410 Gone with re-onboard hint instead of HTTP-410 (#2429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to molecule-core#2449 (which taught the platform to return 410 Gone for status='removed'). Without this branch the operator sees `get_workspace_info failed: HTTP 410 — workspace removed` and has to guess what to do — exactly the 2026-04-30 silent-fail UX hit on the hongmingwang tenant. The new code path: 1. Detect resp.status === 410 explicitly 2. Best-effort parse the body for id / removed_at / hint 3. Throw `Workspace was deleted on the platform at . ` The 410-message-formatting is extracted into a pure `formatRemovedWorkspaceError` helper so it can be unit-tested without mocking fetch + resolveWatching. Four new bun:test cases: - prefers platform-supplied id, removed_at, hint - falls back to local workspaceId + default hint when body is empty - tolerates null/undefined body (unparseable response) - omits ' at ' clause when removed_at is missing Co-Authored-By: Claude Opus 4.7 (1M context) --- server.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ server.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 server.test.ts diff --git a/server.test.ts b/server.test.ts new file mode 100644 index 0000000..b22dcbf --- /dev/null +++ b/server.test.ts @@ -0,0 +1,49 @@ +// Regression tests for getWorkspaceInfo's 410-handling — pinned via +// the formatRemovedWorkspaceError pure helper so the test doesn't +// need to mock fetch + resolveWatching just to read one string. +// +// molecule-core#2429 — without these tests, the "your workspace was +// deleted, re-onboard" message is a 4-line code path that an +// inattentive refactor could collapse back into the generic +// "HTTP 410" error we used to surface. + +import { describe, expect, it } from 'bun:test' +import { formatRemovedWorkspaceError } from './server.ts' + +describe('formatRemovedWorkspaceError — 410 Gone handling (#2429)', () => { + it('prefers the platform-supplied id, removed_at, and hint when present', () => { + const msg = formatRemovedWorkspaceError('local-fallback-id', { + id: 'real-uuid', + removed_at: '2026-04-30T12:00:00Z', + hint: 'Custom hint from the platform.', + }) + expect(msg).toBe( + 'Workspace real-uuid was deleted on the platform at 2026-04-30T12:00:00Z. Custom hint from the platform.', + ) + }) + + it('falls back to the local workspaceId + default hint when body is empty', () => { + const msg = formatRemovedWorkspaceError('fallback-uuid', {}) + expect(msg).toBe( + 'Workspace fallback-uuid was deleted on the platform. Regenerate workspace + token from the canvas → Tokens tab.', + ) + }) + + it('tolerates a null/undefined body (unparseable response)', () => { + expect(formatRemovedWorkspaceError('uuid', null)).toContain( + 'Workspace uuid was deleted', + ) + expect(formatRemovedWorkspaceError('uuid', undefined)).toContain( + 'Regenerate workspace + token', + ) + }) + + it('omits the timestamp clause when removed_at is missing', () => { + const msg = formatRemovedWorkspaceError('uuid', { + id: 'uuid', + hint: 'h', + }) + expect(msg).not.toContain(' at ') + expect(msg).toBe('Workspace uuid was deleted on the platform. h') + }) +}) diff --git a/server.ts b/server.ts index 6216bff..259cdd9 100644 --- a/server.ts +++ b/server.ts @@ -691,12 +691,39 @@ const GetWorkspaceInfoArgsSchema = z.object({ ).optional(), }) +// Pure formatter — kept exportable so server.test.ts can pin the +// message shape without mocking fetch + resolveWatching just to read +// one string. molecule-core#2429. +export function formatRemovedWorkspaceError( + workspaceId: string, + body: { id?: string; removed_at?: string; hint?: string } | null | undefined, +): string { + const safeBody = body ?? {} + const id = safeBody.id ?? workspaceId + const hint = safeBody.hint ?? 'Regenerate workspace + token from the canvas → Tokens tab.' + const removed = safeBody.removed_at ? ` at ${safeBody.removed_at}` : '' + return `Workspace ${id} was deleted on the platform${removed}. ${hint}` +} + async function getWorkspaceInfo(args: z.infer): Promise { const { workspaceId, token } = resolveWatching(args._as_workspace) const resp = await fetch(`${PLATFORM_URL}/workspaces/${workspaceId}`, { headers: platformHeaders(token), signal: AbortSignal.timeout(15_000), }) + if (resp.status === 410) { + // molecule-core#2429: platform returns 410 Gone when status='removed'. + // Surface a clear "your workspace was deleted, re-onboard" error + // instead of a generic HTTP error — without this branch the operator + // sees `get_workspace_info failed: HTTP 410` and has to guess why. + let body: { id?: string; removed_at?: string; hint?: string } = {} + try { + body = await resp.json() as typeof body + } catch { + // best-effort body parse; the error message stands alone + } + throw new Error(formatRemovedWorkspaceError(workspaceId, body)) + } if (!resp.ok) { const errText = await resp.text().catch(() => '') throw new Error(`get_workspace_info failed: HTTP ${resp.status} — ${errText.slice(0, 200)}`)