mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
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:
@@ -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] }))
|
||||
|
||||
Reference in New Issue
Block a user