feat(channel): surface 410 Gone with re-onboard hint instead of HTTP-410 (#2429)
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 <id> was deleted on the platform at <ts>. <hint>` 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 <ts>' clause when removed_at is missing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
68d8ae981f
commit
53e4ac329d
49
server.test.ts
Normal file
49
server.test.ts
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
27
server.ts
27
server.ts
@ -691,12 +691,39 @@ const GetWorkspaceInfoArgsSchema = z.object({
|
|||||||
).optional(),
|
).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<typeof GetWorkspaceInfoArgsSchema>): Promise<unknown> {
|
async function getWorkspaceInfo(args: z.infer<typeof GetWorkspaceInfoArgsSchema>): Promise<unknown> {
|
||||||
const { workspaceId, token } = resolveWatching(args._as_workspace)
|
const { workspaceId, token } = resolveWatching(args._as_workspace)
|
||||||
const resp = await fetch(`${PLATFORM_URL}/workspaces/${workspaceId}`, {
|
const resp = await fetch(`${PLATFORM_URL}/workspaces/${workspaceId}`, {
|
||||||
headers: platformHeaders(token),
|
headers: platformHeaders(token),
|
||||||
signal: AbortSignal.timeout(15_000),
|
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) {
|
if (!resp.ok) {
|
||||||
const errText = await resp.text().catch(() => '')
|
const errText = await resp.text().catch(() => '')
|
||||||
throw new Error(`get_workspace_info failed: HTTP ${resp.status} — ${errText.slice(0, 200)}`)
|
throw new Error(`get_workspace_info failed: HTTP ${resp.status} — ${errText.slice(0, 200)}`)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user