refactor(api): converge the Remote failure vocabulary and client surface

Single RemoteError with a merge-extensible, domain-prefixed code map;
owners throw at the failure point; streams surface marked failures;
clients consume ctx.remote directly with isRemoteFailure as the only
discrimination point and construct no failure instances.
This commit is contained in:
imccyu
2026-08-28 22:37:36 +08:00
parent 12d7b4ed0c
commit 804b1ffbfc
252 changed files with 3182 additions and 3832 deletions
@@ -15,11 +15,10 @@ import type {
WorkspaceOrderValue,
WorkspaceRenameRequest,
WorkspaceValue,
WorkspaceError,
WorkspaceId,
WorkspaceView,
} from '../src/types.ts'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import { RemoteError, type RemoteFailure, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
const sid = (id: string): SessionId => id as SessionId
@@ -44,7 +43,7 @@ function remoteOk<T>(value: T): RemoteResult<T> {
return { ok: true, value }
}
function workspaceError(error: WorkspaceError): RemoteResult<never> {
function workspaceError(error: RemoteFailure): RemoteResult<never> {
return { ok: false, error }
}
@@ -158,17 +157,17 @@ describe('ClientWorkspaceModel', () => {
model.handleCarrierFailure()
expect(model.getSnapshot()).toMatchObject({ phase: 'ready', state: 'loading', error: null })
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['visible'])
model.handleStreamFailure(new Error('wire down'))
model.handleStreamFailure(new RemoteError('gateway/internal', 'wire down', {}))
expect(model.getSnapshot()).toMatchObject({
phase: 'ready', state: 'error', error: { code: 'internal', message: 'wire down' },
phase: 'ready', state: 'error', error: { code: 'gateway/internal', message: 'wire down' },
})
model.handleStreamFailure('plain failure')
expect(model.getSnapshot().error?.message).toBe('plain failure')
// An unmarked value never crosses the stream boundary: it is a local fault.
expect(() => { model.handleStreamFailure('plain failure') }).toThrow()
baseline(model, [workspace('restored')])
expect(model.getSnapshot()).toMatchObject({ phase: 'ready', state: 'idle', error: null })
})
it('creates by path, prepends the returned row, and folds rejected calls', async () => {
it('creates by path and prepends the returned row', async () => {
const remote = new FakeWorkspaceRemote()
const model = modelFor(remote)
remote.onCreate = request => Promise.resolve(remoteOk({
@@ -178,11 +177,6 @@ describe('ClientWorkspaceModel', () => {
await expect(model.create({ path: '/w/created' })).resolves.toMatchObject({ ok: true })
expect(remote.calls).toContainEqual({ method: 'create', request: { path: '/w/created' } })
expect(model.getSnapshot().items[0]?.workspaceId).toBe('created')
remote.onCreate = () => Promise.reject(new Error('create transport'))
await expect(model.create({ path: '/w/existing' })).resolves.toMatchObject({
ok: false, error: { code: 'internal', message: 'create transport' },
})
})
it('lets newer stream order outrank unary echoes and rolls failures back', async () => {
@@ -199,22 +193,16 @@ describe('ClientWorkspaceModel', () => {
await pending
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['one', 'three', 'two'])
remote.onInsertBefore = () => Promise.resolve(workspaceError({
code: 'workspace-not-found', message: 'gone', details: { workspaceId: wid('three') },
}))
remote.onInsertBefore = () => Promise.resolve(workspaceError(
new RemoteError('workspace/not-found', 'gone', { workspaceId: wid('three') }),
))
const rejected = model.insertBefore(wid('three'))
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['one', 'two', 'three'])
await expect(rejected).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['one', 'three', 'two'])
remote.onInsertBefore = () => Promise.reject(new Error('transport down'))
const disconnected = model.insertBefore(wid('three'), wid('one'))
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['three', 'one', 'two'])
await expect(disconnected).rejects.toThrow('transport down')
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['one', 'three', 'two'])
})
it('keeps a newer optimistic reorder when an older transport call rejects', async () => {
it('keeps a newer optimistic reorder when an older refused call settles', async () => {
const remote = new FakeWorkspaceRemote()
const model = modelFor(remote)
baseline(model, [workspace('one'), workspace('two'), workspace('three')])
@@ -225,8 +213,10 @@ describe('ClientWorkspaceModel', () => {
const first = model.insertBefore(wid('three'), wid('one'))
const second = model.insertBefore(wid('two'), wid('three'))
firstGate.reject(new Error('first transport failed'))
await expect(first).rejects.toThrow('first transport failed')
firstGate.resolve(workspaceError(
new RemoteError('workspace/not-found', 'first refused', { workspaceId: wid('three') }),
))
await expect(first).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['two', 'three', 'one'])
secondGate.resolve(remoteOk({ workspaceIds: [wid('two'), wid('three'), wid('one')] }))
await expect(second).resolves.toMatchObject({ ok: true })
@@ -244,14 +234,10 @@ describe('ClientWorkspaceModel', () => {
const first = model.insertBefore(wid('three'), wid('one'))
const second = model.insertBefore(wid('two'), wid('three'))
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['two', 'three', 'one'])
firstGate.resolve(workspaceError({
code: 'workspace-not-found', message: 'first rejected', details: { workspaceId: wid('three') },
}))
firstGate.resolve(workspaceError(new RemoteError('workspace/not-found', 'first rejected', { workspaceId: wid('three') })))
await expect(first).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['two', 'three', 'one'])
secondGate.resolve(workspaceError({
code: 'workspace-not-found', message: 'second rejected', details: { workspaceId: wid('two') },
}))
secondGate.resolve(workspaceError(new RemoteError('workspace/not-found', 'second rejected', { workspaceId: wid('two') })))
await expect(second).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['one', 'two', 'three'])
})
@@ -284,15 +270,11 @@ describe('ClientWorkspaceModel', () => {
const model = modelFor(remote)
baseline(model, [workspace('one', [sid('first'), sid('second')])], [sid('archived')])
remote.onRename = () => Promise.resolve(workspaceError({
code: 'workspace-not-found', message: 'gone', details: { workspaceId: wid('one') },
}))
remote.onRename = () => Promise.resolve(workspaceError(new RemoteError('workspace/not-found', 'gone', { workspaceId: wid('one') })))
await expect(model.rename(wid('one'), 'ignored')).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items[0]?.title).toBe('one')
remote.onDelete = () => Promise.resolve(workspaceError({
code: 'workspace-not-found', message: 'gone', details: { workspaceId: wid('one') },
}))
remote.onDelete = () => Promise.resolve(workspaceError(new RemoteError('workspace/not-found', 'gone', { workspaceId: wid('one') })))
await expect(model.delete(wid('one'))).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().items).toHaveLength(1)
@@ -306,11 +288,9 @@ describe('ClientWorkspaceModel', () => {
request: { workspaceId: 'one', sessionId: 'second', beforeSessionId: 'first' },
})
remote.onInsertSessionBefore = () => Promise.resolve(workspaceError({
code: 'workspace-move-invalid',
message: 'invalid move',
details: { workspaceId: wid('one'), sessionId: sid('second') },
}))
remote.onInsertSessionBefore = () => Promise.resolve(workspaceError(
new RemoteError('workspace/move-invalid', 'invalid move', { workspaceId: wid('one'), sessionId: sid('second') }),
))
await expect(model.insertSessionBefore(wid('one'), sid('second')))
.resolves.toMatchObject({ ok: false })
expect(remote.calls).toContainEqual({
@@ -318,9 +298,9 @@ describe('ClientWorkspaceModel', () => {
request: { workspaceId: 'one', sessionId: 'second' },
})
remote.onArchiveSession = () => Promise.resolve(workspaceError({
code: 'session-not-found', message: 'missing', details: { sessionId: sid('missing') },
}))
remote.onArchiveSession = () => Promise.resolve(workspaceError(
new RemoteError('session/not-found', 'missing', { sessionId: sid('missing') }),
))
await expect(model.archiveSession(sid('missing'))).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot().archivedSessionIds).toEqual(['archived'])
remote.onArchiveSession = request => Promise.resolve(remoteOk({ archivedSessionIds: [request.sessionId] }))