mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +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:
@@ -7,7 +7,7 @@ import {
|
||||
type ClientRemote,
|
||||
} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { WorkspaceFollowFrame, WorkspaceFollowIncrement } from '../types.ts'
|
||||
import type { WorkspaceFollowSink, WorkspaceRemote } from './model.ts'
|
||||
import type { WorkspaceFollowSink } from './model.ts'
|
||||
import { ClientWorkspaceModel } from './model.ts'
|
||||
import { WorkspaceController } from './service.ts'
|
||||
|
||||
@@ -19,10 +19,6 @@ export { WorkspaceController, WorkspaceCreateError } from './service.ts'
|
||||
export type { IWorkspaces, WorkspaceSource } from './service.ts'
|
||||
export type { WorkspaceId, WorkspaceView } from '../types.ts'
|
||||
|
||||
type WorkspaceStreamRemote = Pick<ClientRemote, '$stream'> & {
|
||||
readonly workspace: WorkspaceRemote
|
||||
}
|
||||
|
||||
type WorkspaceBaselineFrame = Extract<WorkspaceFollowFrame, { type: 'baseline' }>
|
||||
|
||||
/** Gateway-owned snapshot stream configured for Workspace state. */
|
||||
@@ -46,10 +42,9 @@ export const inject = ['remote', 'remote.workspace']
|
||||
* @param ctx - Client root Context.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
const remote = ctx.remote as WorkspaceStreamRemote
|
||||
const model = new ClientWorkspaceModel(remote.workspace)
|
||||
const model = new ClientWorkspaceModel(ctx.remote.workspace)
|
||||
new WorkspaceController(ctx, model)
|
||||
const control = createWorkspaceStateStream(remote, {
|
||||
const control = createWorkspaceStateStream(ctx.remote, {
|
||||
accept: model,
|
||||
carrierFailed: () => { model.handleCarrierFailure() },
|
||||
failed: (error) => { model.handleStreamFailure(error) },
|
||||
@@ -73,12 +68,12 @@ export interface WorkspaceStateStreamOptions {
|
||||
|
||||
/**
|
||||
* Create the reconnecting Workspace state stream.
|
||||
* @param remote - generated Workspace namespace and Gateway stream factory.
|
||||
* @param remote - Client Remote face carrying the Workspace namespace and the stream factory.
|
||||
* @param options - Workspace state destinations.
|
||||
* @returns an unstarted stream owned by the Client Workspace runtime.
|
||||
*/
|
||||
export function createWorkspaceStateStream(
|
||||
remote: WorkspaceStreamRemote,
|
||||
remote: ClientRemote,
|
||||
options: WorkspaceStateStreamOptions,
|
||||
): WorkspaceStateStream {
|
||||
const stream = remote.$stream<WorkspaceFollowFrame>({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { notifySubscribers } from '@deepseek-ai/dsh-client-store'
|
||||
import type {} from '@deepseek-ai/dsh-api-workspace-controller/remote'
|
||||
import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { RemoteFailure, RemoteResult, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type {
|
||||
WorkspaceArchiveSessionRequest,
|
||||
@@ -82,12 +83,7 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink {
|
||||
* @returns generated Remote result.
|
||||
*/
|
||||
async create(input: WorkspaceCreateRequest): Promise<RemoteResult<WorkspaceCreateValue>> {
|
||||
let result: RemoteResult<WorkspaceCreateValue>
|
||||
try {
|
||||
result = await this.remote.create(input)
|
||||
} catch (error) {
|
||||
result = failureResult(error)
|
||||
}
|
||||
const result = await this.remote.create(input)
|
||||
if (result.ok) this.upsert(result.value.workspace)
|
||||
return result
|
||||
}
|
||||
@@ -129,19 +125,10 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink {
|
||||
const frameGeneration = this.orderFrameGeneration
|
||||
const localOrder = this.items.map(workspace => workspace.workspaceId)
|
||||
this.installOrder(insertIdBefore(localOrder, workspaceId, beforeWorkspaceId))
|
||||
let result: RemoteResult<WorkspaceOrderValue>
|
||||
try {
|
||||
result = await this.remote.insertBefore({
|
||||
workspaceId,
|
||||
...beforeWorkspaceId === undefined ? {} : { beforeWorkspaceId },
|
||||
})
|
||||
} catch (error) {
|
||||
if (requestGeneration === this.orderRequestGeneration
|
||||
&& frameGeneration === this.orderFrameGeneration) {
|
||||
this.installOrder(this.committedOrder)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const result = await this.remote.insertBefore({
|
||||
workspaceId,
|
||||
...beforeWorkspaceId === undefined ? {} : { beforeWorkspaceId },
|
||||
})
|
||||
if (requestGeneration === this.orderRequestGeneration
|
||||
&& frameGeneration === this.orderFrameGeneration) {
|
||||
this.installOrder(result.ok ? result.value.workspaceIds : this.committedOrder, result.ok)
|
||||
@@ -233,8 +220,9 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink {
|
||||
* @param error - terminal stream failure.
|
||||
*/
|
||||
handleStreamFailure(error: unknown): void {
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
this.state = 'error'
|
||||
this.error = failureOf(error)
|
||||
this.error = error
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
@@ -369,15 +357,3 @@ function insertIdBefore(
|
||||
const at = beforeId === undefined ? without.length : without.indexOf(beforeId)
|
||||
return [...without.slice(0, at), id, ...without.slice(at)]
|
||||
}
|
||||
|
||||
function failureResult<T>(error: unknown): RemoteResult<T> {
|
||||
return { ok: false, error: failureOf(error) }
|
||||
}
|
||||
|
||||
function failureOf(error: unknown): RemoteFailure {
|
||||
return {
|
||||
code: 'internal',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ClientWorkspaceModel, WorkspaceSnapshot } from './model.ts'
|
||||
export class WorkspaceCreateError extends Error {
|
||||
override readonly name = 'WorkspaceCreateError'
|
||||
|
||||
/** @param rpcError - Host business or folded transport failure. */
|
||||
/** @param rpcError - Host business or folded carrier failure. */
|
||||
constructor(readonly rpcError: RemoteFailure) {
|
||||
super(`workspace create failed: ${rpcError.code}: ${rpcError.message}`)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
WorkspaceOrderInvalidError,
|
||||
WorkspaceUnknownSessionError,
|
||||
} from '@deepseek-ai/dsh-workspace'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { workspaceView } from './feed.ts'
|
||||
import type {
|
||||
WorkspaceArchiveSessionRequest,
|
||||
@@ -46,11 +46,12 @@ export class WorkspaceCommands {
|
||||
const workspace = await this.ctx.workspaceRegistry.create(request.path)
|
||||
return { workspace: workspaceView(workspace), created: true }
|
||||
} catch (error) {
|
||||
if (error instanceof TypertRemoteFailure) throw error
|
||||
throw failure(
|
||||
'workspace-invalid-path',
|
||||
if (remoteErrorOf(error) !== undefined) throw error
|
||||
throw new RemoteError(
|
||||
'workspace/invalid-path',
|
||||
`cannot create a Workspace at "${request.path}": ${errorMessage(error)}`,
|
||||
{ path: request.path },
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -64,19 +65,15 @@ export class WorkspaceCommands {
|
||||
rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue> {
|
||||
const title = request.title.trim()
|
||||
if (title === '') {
|
||||
return Promise.reject(failure(
|
||||
'bad-request',
|
||||
'Workspace rename requires a non-blank title',
|
||||
{},
|
||||
))
|
||||
return Promise.reject(new RemoteError('gateway/bad-request', 'Workspace rename requires a non-blank title', {}))
|
||||
}
|
||||
return this.enqueue(async () => {
|
||||
const workspace = this.requireWorkspace(request.workspaceId)
|
||||
if (title !== workspace.title) {
|
||||
if (this.ctx.workspaceRegistry.list().some(candidate =>
|
||||
candidate.id !== workspace.id && candidate.title === title)) {
|
||||
throw failure(
|
||||
'workspace-name-conflict',
|
||||
throw new RemoteError(
|
||||
'workspace/name-conflict',
|
||||
`Workspace name '${title}' is already in use`,
|
||||
{ name: title },
|
||||
)
|
||||
@@ -132,8 +129,8 @@ export class WorkspaceCommands {
|
||||
await workspace.insertSessionBefore(request.sessionId, request.beforeSessionId)
|
||||
} catch (error) {
|
||||
if (!(error instanceof WorkspaceMoveInvalidError)) throw error
|
||||
throw failure(
|
||||
'workspace-move-invalid',
|
||||
throw new RemoteError(
|
||||
'workspace/move-invalid',
|
||||
error.message,
|
||||
{
|
||||
workspaceId: request.workspaceId,
|
||||
@@ -142,6 +139,7 @@ export class WorkspaceCommands {
|
||||
? {}
|
||||
: { beforeSessionId: request.beforeSessionId },
|
||||
},
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
return { workspace: workspaceView(workspace) }
|
||||
@@ -157,7 +155,7 @@ export class WorkspaceCommands {
|
||||
await this.ctx.workspaceRegistry.archiveSession(request.sessionId)
|
||||
} catch (error) {
|
||||
if (!(error instanceof WorkspaceUnknownSessionError)) throw error
|
||||
throw failure('session-not-found', error.message, { sessionId: request.sessionId })
|
||||
throw new RemoteError('session/not-found', error.message, { sessionId: request.sessionId }, { cause: error })
|
||||
}
|
||||
return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] }
|
||||
}
|
||||
@@ -175,22 +173,14 @@ export class WorkspaceCommands {
|
||||
}
|
||||
}
|
||||
|
||||
function workspaceNotFound(workspaceId: WorkspaceId): TypertRemoteFailure {
|
||||
return failure(
|
||||
'workspace-not-found',
|
||||
function workspaceNotFound(workspaceId: WorkspaceId): RemoteError<'workspace/not-found'> {
|
||||
return new RemoteError(
|
||||
'workspace/not-found',
|
||||
`Workspace "${workspaceId}" not found`,
|
||||
{ workspaceId },
|
||||
)
|
||||
}
|
||||
|
||||
function failure(
|
||||
code: string,
|
||||
message: string,
|
||||
details: object,
|
||||
): TypertRemoteFailure {
|
||||
return new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { z } from 'zod'
|
||||
import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
|
||||
import type { DirectoryPickerCapabilities } from '@deepseek-ai/dsh-host-directory-picker'
|
||||
import type {
|
||||
DirectoryPickerCapabilities, DirectoryPickerErrorCode,
|
||||
} from '@deepseek-ai/dsh-host-directory-picker'
|
||||
// The seam owns the listing declaration; the generator requires the reference
|
||||
// site to name that package rather than this package's re-export of it.
|
||||
import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { DirectoryPickerErrorDetailsMap } from './types.ts'
|
||||
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { RemoteErrorCode } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
const createDirectoryRequestSchema = z.object({
|
||||
path: z.string(),
|
||||
@@ -86,8 +88,8 @@ export class DirectoryPickerController extends TypertRemoteService {
|
||||
async createDirectory(path: string, name: string): Promise<string> {
|
||||
const request = createDirectoryRequestSchema.safeParse({ path, name })
|
||||
if (!request.success) {
|
||||
throw pickerFailureOf(
|
||||
'bad-request',
|
||||
throw new RemoteError(
|
||||
'gateway/bad-request',
|
||||
'invalid payload for host.createDirectory',
|
||||
{ issues: request.error.issues },
|
||||
)
|
||||
@@ -107,8 +109,8 @@ export class DirectoryPickerController extends TypertRemoteService {
|
||||
): DirectoryPickerCapabilities[Kind] {
|
||||
const capability = this.ctx.directoryPicker.capability()
|
||||
if (capability.kind !== kind) {
|
||||
throw pickerFailureOf(
|
||||
'directory-picker-unavailable',
|
||||
throw new RemoteError(
|
||||
'directory-picker/unavailable',
|
||||
`directoryPicker.${method} needs the ${kind} capability; the composed picker serves "${capability.kind}"`,
|
||||
{ capability: capability.kind },
|
||||
)
|
||||
@@ -118,19 +120,15 @@ export class DirectoryPickerController extends TypertRemoteService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise one entry of the picking wire failure vocabulary.
|
||||
* @param code - the failure code a caller discriminates on.
|
||||
* @param message - operator-facing description.
|
||||
* @param details - the payload this code carries.
|
||||
* @returns the failure to throw across the Remote boundary.
|
||||
* Wire code answered for each seam browse failure. The seam's closed codes are
|
||||
* its own local vocabulary, so this controller owns the projection onto the
|
||||
* `directory-picker/*` codes a Remote caller discriminates on.
|
||||
*/
|
||||
function pickerFailureOf<Code extends keyof DirectoryPickerErrorDetailsMap>(
|
||||
code: Code,
|
||||
message: string,
|
||||
details: DirectoryPickerErrorDetailsMap[Code],
|
||||
): TypertRemoteFailure {
|
||||
return new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
const BROWSE_FAILURE_CODES = {
|
||||
'directory-unreadable': 'directory-picker/unreadable',
|
||||
'directory-exists': 'directory-picker/exists',
|
||||
'directory-create-failed': 'directory-picker/create-failed',
|
||||
} as const satisfies Record<DirectoryPickerErrorCode, RemoteErrorCode>
|
||||
|
||||
/**
|
||||
* Classify a browse-primitive rejection: the seam's own closed codes carry the
|
||||
@@ -138,16 +136,21 @@ function pickerFailureOf<Code extends keyof DirectoryPickerErrorDetailsMap>(
|
||||
* @param error - the primitive's rejection.
|
||||
* @returns the failure to throw across the Remote boundary.
|
||||
*/
|
||||
function browseFailure(error: unknown): TypertRemoteFailure {
|
||||
function browseFailure(error: unknown): RemoteError {
|
||||
if (error instanceof DirectoryPickerError) {
|
||||
return pickerFailureOf(error.code, error.message, { path: error.path })
|
||||
return new RemoteError(
|
||||
BROWSE_FAILURE_CODES[error.code],
|
||||
error.message,
|
||||
{ path: error.path },
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
return pickerFailureOf('internal', errorMessage(error), {})
|
||||
return new RemoteError('gateway/internal', errorMessage(error), {}, { cause: error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a cancellable primitive's rejection. An abort is the caller's own
|
||||
* timeout or disconnect, not a backend failure, so it answers `cancelled`
|
||||
* timeout or disconnect, not a backend failure, so it answers `gateway/cancelled`
|
||||
* before the business classification runs.
|
||||
* @param error - the primitive's rejection.
|
||||
* @param signal - the caller lifetime the primitive ran under.
|
||||
@@ -160,10 +163,10 @@ function cancellableFailure(
|
||||
signal: AbortSignal,
|
||||
cancelled: string,
|
||||
failed?: string,
|
||||
): TypertRemoteFailure {
|
||||
if (signal.aborted) return pickerFailureOf('cancelled', cancelled, {})
|
||||
): RemoteError {
|
||||
if (signal.aborted) return new RemoteError('gateway/cancelled', cancelled, {}, { cause: error })
|
||||
if (failed === undefined) return browseFailure(error)
|
||||
return pickerFailureOf('internal', `${failed}: ${errorMessage(error)}`, {})
|
||||
return new RemoteError('gateway/internal', `${failed}: ${errorMessage(error)}`, {}, { cause: error })
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import type { z as zCore } from 'zod'
|
||||
|
||||
type ZodIssue = zCore.core.$ZodIssue
|
||||
|
||||
export type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
export type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
|
||||
@@ -29,45 +26,27 @@ export interface WorkspaceView {
|
||||
readonly updatedAt: string
|
||||
}
|
||||
|
||||
/** Stable Workspace failure details returned by unary methods. */
|
||||
export interface WorkspaceErrorDetailsMap {
|
||||
'bad-request': Record<never, never>
|
||||
'workspace-invalid-path': { readonly path: string }
|
||||
'workspace-not-found': { readonly workspaceId: WorkspaceId }
|
||||
'workspace-name-conflict': { readonly name: string }
|
||||
'workspace-move-invalid': {
|
||||
readonly workspaceId: WorkspaceId
|
||||
readonly sessionId: SessionId
|
||||
readonly beforeSessionId?: SessionId
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
/** The requested directory cannot back a Workspace. */
|
||||
'workspace/invalid-path': { readonly path: string }
|
||||
/** Another Workspace already uses the requested name. */
|
||||
'workspace/name-conflict': { readonly name: string }
|
||||
/** The Session or its anchor is not in the Workspace's manual order. */
|
||||
'workspace/move-invalid': {
|
||||
readonly workspaceId: WorkspaceId
|
||||
readonly sessionId: SessionId
|
||||
readonly beforeSessionId?: SessionId
|
||||
}
|
||||
/** The verb needs an interaction the composed backend does not serve. */
|
||||
'directory-picker/unavailable': { readonly capability: string }
|
||||
/** The target is not fully qualified, or the backend cannot list it. */
|
||||
'directory-picker/unreadable': { readonly path: string }
|
||||
/** A child of that name is already there. */
|
||||
'directory-picker/exists': { readonly path: string }
|
||||
/** The parent is not fully qualified, the name is not one segment, or creation failed. */
|
||||
'directory-picker/create-failed': { readonly path: string }
|
||||
}
|
||||
'session-not-found': { readonly sessionId: SessionId }
|
||||
}
|
||||
|
||||
/** Workspace business failure returned without throwing a carrier error. */
|
||||
export type WorkspaceError = {
|
||||
[Code in keyof WorkspaceErrorDetailsMap]: {
|
||||
readonly code: Code
|
||||
readonly message: string
|
||||
readonly details: WorkspaceErrorDetailsMap[Code]
|
||||
}
|
||||
}[keyof WorkspaceErrorDetailsMap]
|
||||
|
||||
/** Stable directory-picking failure details returned by the picking wire verbs. */
|
||||
export interface DirectoryPickerErrorDetailsMap {
|
||||
/** The directory creation request violates its semantic input constraints. */
|
||||
'bad-request': { readonly issues: ZodIssue[] }
|
||||
/** The verb needs an interaction the composed backend does not serve. */
|
||||
'directory-picker-unavailable': { readonly capability: string }
|
||||
/** The target is not fully qualified, or the backend cannot list it. */
|
||||
'directory-unreadable': { readonly path: string }
|
||||
/** A child of that name is already there. */
|
||||
'directory-exists': { readonly path: string }
|
||||
/** The parent is not fully qualified, the name is not one segment, or creation failed. */
|
||||
'directory-create-failed': { readonly path: string }
|
||||
/** The caller's own timeout or disconnect ended the chooser or the scan. */
|
||||
cancelled: Record<never, never>
|
||||
/** A backend failure with no seam code of its own. */
|
||||
internal: Record<never, never>
|
||||
}
|
||||
|
||||
/** Existing directory requested for Workspace adoption. */
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { DirectoryPicker, DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
|
||||
import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { DirectoryPickerController } from '../src/directory-picker.ts'
|
||||
|
||||
const roots: Context[] = []
|
||||
@@ -60,8 +60,9 @@ async function refused(call: Promise<unknown>): Promise<{ code: string; message:
|
||||
try {
|
||||
await call
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof TypertRemoteFailure)) throw error
|
||||
return { ...error.failure }
|
||||
const failure = remoteErrorOf(error)
|
||||
if (failure === undefined) throw error
|
||||
return { code: failure.code, message: failure.message, details: failure.details }
|
||||
}
|
||||
throw new Error('the call was expected to be refused')
|
||||
}
|
||||
@@ -85,18 +86,18 @@ describe('directoryPicker pick Remote', () => {
|
||||
const abort = new AbortController()
|
||||
const pending = refused(picker.pick(abort.signal))
|
||||
abort.abort()
|
||||
expect((await pending).code).toBe('cancelled')
|
||||
expect((await pending).code).toBe('gateway/cancelled')
|
||||
|
||||
const broken = await harness({ kind: 'native', pick: async () => { throw new Error('no chooser installed') } })
|
||||
const failure = await refused(broken.pick(new AbortController().signal))
|
||||
expect(failure.code).toBe('internal')
|
||||
expect(failure.code).toBe('gateway/internal')
|
||||
expect(failure.message).toContain('no chooser installed')
|
||||
})
|
||||
|
||||
it('refuses the native verb under a browse composition', async () => {
|
||||
const picker = await harness(BROWSE_STUB)
|
||||
const failure = await refused(picker.pick(new AbortController().signal))
|
||||
expect(failure.code).toBe('directory-picker-unavailable')
|
||||
expect(failure.code).toBe('directory-picker/unavailable')
|
||||
expect(failure.message).toContain('needs the native capability')
|
||||
expect(failure.details).toEqual({ capability: 'browse' })
|
||||
})
|
||||
@@ -115,12 +116,12 @@ describe('directoryPicker browse Remotes', () => {
|
||||
it('maps the seam\'s typed failures and folds unknown throws to internal', async () => {
|
||||
const picker = await harness(BROWSE_STUB)
|
||||
expect(await refused(picker.list('/denied', new AbortController().signal)))
|
||||
.toMatchObject({ code: 'directory-unreadable', details: { path: '/denied' } })
|
||||
expect((await refused(picker.createDirectory('/home/user', 'taken'))).code).toBe('directory-exists')
|
||||
expect((await refused(picker.createDirectory('/home/user', 'unwritable'))).code).toBe('internal')
|
||||
.toMatchObject({ code: 'directory-picker/unreadable', details: { path: '/denied' } })
|
||||
expect((await refused(picker.createDirectory('/home/user', 'taken'))).code).toBe('directory-picker/exists')
|
||||
expect((await refused(picker.createDirectory('/home/user', 'unwritable'))).code).toBe('gateway/internal')
|
||||
|
||||
const thrown = await refused(picker.createDirectory('/home/user', 'gone'))
|
||||
expect(thrown).toMatchObject({ code: 'internal', message: 'the volume vanished' })
|
||||
expect(thrown).toMatchObject({ code: 'gateway/internal', message: 'the volume vanished' })
|
||||
})
|
||||
|
||||
it('rejects invalid child names before capability dispatch', async () => {
|
||||
@@ -134,7 +135,7 @@ describe('directoryPicker browse Remotes', () => {
|
||||
for (const name of ['', ' ', '.', '..', 'a/b', 'a\\b']) {
|
||||
const failure = await refused(picker.createDirectory('/home/user', name))
|
||||
expect(failure).toMatchObject({
|
||||
code: 'bad-request',
|
||||
code: 'gateway/bad-request',
|
||||
message: 'invalid payload for host.createDirectory',
|
||||
})
|
||||
expect(Array.isArray(Reflect.get(failure.details, 'issues'))).toBe(true)
|
||||
@@ -153,14 +154,14 @@ describe('directoryPicker browse Remotes', () => {
|
||||
const abort = new AbortController()
|
||||
const pending = refused(picker.list(undefined, abort.signal))
|
||||
abort.abort()
|
||||
expect((await pending).code).toBe('cancelled')
|
||||
expect((await pending).code).toBe('gateway/cancelled')
|
||||
})
|
||||
|
||||
it('refuses the browse verbs under a native composition', async () => {
|
||||
const picker = await harness()
|
||||
expect(await refused(picker.list(undefined, new AbortController().signal)))
|
||||
.toMatchObject({ code: 'directory-picker-unavailable', details: { capability: 'native' } })
|
||||
.toMatchObject({ code: 'directory-picker/unavailable', details: { capability: 'native' } })
|
||||
expect(await refused(picker.createDirectory('/x', 'y')))
|
||||
.toMatchObject({ code: 'directory-picker-unavailable', details: { capability: 'native' } })
|
||||
.toMatchObject({ code: 'directory-picker/unavailable', details: { capability: 'native' } })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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] }))
|
||||
|
||||
@@ -3,11 +3,12 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
RemoteStream,
|
||||
RemoteStreamCarrierError,
|
||||
type ClientRemote,
|
||||
type RemoteStreamOptions,
|
||||
} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError, type RemoteFailure, type RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import * as WorkspaceClientPlugin from '../src/client/index.ts'
|
||||
import {
|
||||
ClientWorkspaceModel,
|
||||
@@ -29,7 +30,6 @@ import type {
|
||||
WorkspaceInsertSessionBeforeRequest,
|
||||
WorkspaceOrderValue,
|
||||
WorkspaceRenameRequest,
|
||||
WorkspaceError,
|
||||
WorkspaceId,
|
||||
WorkspaceValue,
|
||||
WorkspaceView,
|
||||
@@ -45,11 +45,11 @@ const AVAILABLE_CONNECTION = {
|
||||
function workspaceClient(
|
||||
remote: WorkspaceRemote,
|
||||
connection: Pick<ConnectionHandle, 'generation'> = AVAILABLE_CONNECTION,
|
||||
) {
|
||||
): ClientRemote {
|
||||
return {
|
||||
workspace: remote,
|
||||
$stream: <Item>(options: RemoteStreamOptions<Item>) => new RemoteStream(connection, options),
|
||||
}
|
||||
} as unknown as ClientRemote
|
||||
}
|
||||
|
||||
interface Generation {
|
||||
@@ -94,7 +94,7 @@ function remoteOk<T>(value: T): RemoteResult<T> {
|
||||
return { ok: true, value }
|
||||
}
|
||||
|
||||
function remoteFailure(error: WorkspaceError): RemoteResult<never> {
|
||||
function remoteFailure(error: RemoteFailure): RemoteResult<never> {
|
||||
return { ok: false, error }
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ describe('Workspace Controller Client apply', () => {
|
||||
phase: 'ready',
|
||||
state: 'error',
|
||||
items: [{ workspaceId: 'fresh' }],
|
||||
error: { code: 'internal', message: 'Workspace state stream emitted more than one opening snapshot' },
|
||||
error: { code: 'gateway/internal', message: 'Workspace state stream emitted more than one opening snapshot' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -445,40 +445,27 @@ describe('WorkspaceController', () => {
|
||||
it('maps generated business failures to the command facade errors', async () => {
|
||||
const remote = new CommandWorkspaceRemote()
|
||||
const controller = new WorkspaceController(new Context(), new ClientWorkspaceModel(remote))
|
||||
const missingWorkspace: WorkspaceError = {
|
||||
code: 'workspace-not-found',
|
||||
message: 'gone',
|
||||
details: { workspaceId: wid('missing') },
|
||||
}
|
||||
const missingSession: WorkspaceError = {
|
||||
code: 'session-not-found',
|
||||
message: 'missing session',
|
||||
details: { sessionId: sid('session') },
|
||||
}
|
||||
const missingWorkspace = new RemoteError('workspace/not-found', 'gone', { workspaceId: wid('missing') })
|
||||
const missingSession = new RemoteError('session/not-found', 'missing session', { sessionId: sid('session') })
|
||||
|
||||
remote.create.mockResolvedValueOnce(remoteFailure({
|
||||
code: 'workspace-invalid-path',
|
||||
message: 'missing path',
|
||||
details: { path: '/missing' },
|
||||
}))
|
||||
remote.create.mockResolvedValueOnce(remoteFailure(new RemoteError('workspace/invalid-path', 'missing path', { path: '/missing' })))
|
||||
const create = controller.create({ path: '/missing' })
|
||||
await expect(create).rejects.toBeInstanceOf(WorkspaceCreateError)
|
||||
await expect(create).rejects.toThrow('workspace-invalid-path: missing path')
|
||||
await expect(create).rejects.toThrow('workspace/invalid-path: missing path')
|
||||
|
||||
remote.rename.mockResolvedValueOnce(remoteFailure(missingWorkspace))
|
||||
await expect(controller.rename(wid('missing'), 'name')).rejects.toThrow('workspace rename failed: workspace-not-found: gone')
|
||||
await expect(controller.rename(wid('missing'), 'name')).rejects.toThrow('workspace rename failed: workspace/not-found: gone')
|
||||
remote.delete.mockResolvedValueOnce(remoteFailure(missingWorkspace))
|
||||
await expect(controller.delete(wid('missing'))).rejects.toThrow('workspace delete failed: workspace-not-found: gone')
|
||||
await expect(controller.delete(wid('missing'))).rejects.toThrow('workspace delete failed: workspace/not-found: gone')
|
||||
remote.insertBefore.mockResolvedValueOnce(remoteFailure(missingWorkspace))
|
||||
await expect(controller.insertBefore(wid('missing'))).rejects.toThrow('workspace reorder failed: workspace-not-found: gone')
|
||||
await expect(controller.insertBefore(wid('missing'))).rejects.toThrow('workspace reorder failed: workspace/not-found: gone')
|
||||
remote.archiveSession.mockResolvedValueOnce(remoteFailure(missingSession))
|
||||
await expect(controller.archiveSession(sid('session'))).rejects.toThrow('workspace session archive failed: session-not-found: missing session')
|
||||
remote.insertSessionBefore.mockResolvedValueOnce(remoteFailure({
|
||||
code: 'workspace-move-invalid',
|
||||
message: 'invalid move',
|
||||
details: { workspaceId: wid('missing'), sessionId: sid('session') },
|
||||
}))
|
||||
await expect(controller.archiveSession(sid('session')))
|
||||
.rejects.toThrow('workspace session archive failed: session/not-found: missing session')
|
||||
remote.insertSessionBefore.mockResolvedValueOnce(remoteFailure(new RemoteError(
|
||||
'workspace/move-invalid', 'invalid move', { workspaceId: wid('missing'), sessionId: sid('session') },
|
||||
)))
|
||||
await expect(controller.insertSessionBefore(wid('missing'), sid('session')))
|
||||
.rejects.toThrow('workspace move failed: workspace-move-invalid: invalid move')
|
||||
.rejects.toThrow('workspace move failed: workspace/move-invalid: invalid move')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import Storage from '@deepseek-ai/dsh-storage'
|
||||
import { DomainFacility } from '@deepseek-ai/dsh-storage-domain'
|
||||
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import WorkspaceRegistry from '@deepseek-ai/dsh-workspace'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import WorkspaceController from '../src/index.ts'
|
||||
@@ -14,6 +14,12 @@ import { WorkspaceFeed } from '../src/feed.ts'
|
||||
import type { WorkspaceFollowFrame } from '../src/types.ts'
|
||||
import { MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'fixture/failure': {}
|
||||
}
|
||||
}
|
||||
|
||||
const roots: Context[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -94,34 +100,29 @@ describe('WorkspaceController commands', () => {
|
||||
const second = await controller.create({ path: stageDir(root, 'second') })
|
||||
|
||||
await expect(controller.create({ path: join(root, 'missing') })).rejects.toMatchObject({
|
||||
failure: { code: 'workspace-invalid-path', details: { path: join(root, 'missing') } },
|
||||
code: 'workspace/invalid-path',
|
||||
details: { path: join(root, 'missing') },
|
||||
})
|
||||
expect(existsSync(join(root, 'missing'))).toBe(false)
|
||||
await expect(controller.rename({ workspaceId: first.workspace.workspaceId, title: ' ' }))
|
||||
.rejects.toMatchObject({ failure: { code: 'bad-request' } })
|
||||
.rejects.toMatchObject({ code: 'gateway/bad-request' })
|
||||
await controller.rename({ workspaceId: first.workspace.workspaceId, title: 'occupied' })
|
||||
await expect(controller.rename({ workspaceId: second.workspace.workspaceId, title: ' occupied ' }))
|
||||
.rejects.toMatchObject({ failure: { code: 'workspace-name-conflict' } })
|
||||
.rejects.toMatchObject({ code: 'workspace/name-conflict' })
|
||||
await expect(controller.delete({ workspaceId: 'missing' as WorkspaceId }))
|
||||
.rejects.toMatchObject({ failure: { code: 'workspace-not-found' } })
|
||||
.rejects.toMatchObject({ code: 'workspace/not-found' })
|
||||
})
|
||||
|
||||
it('preserves Remote failures and propagates unexpected registry failures', async () => {
|
||||
const { controller, ctx, root } = await harness()
|
||||
const remoteFailure = new TypertRemoteFailure({
|
||||
code: 'fixture-failure',
|
||||
message: 'already mapped',
|
||||
details: {},
|
||||
})
|
||||
const remoteFailure = new RemoteError('fixture/failure', 'already mapped', {})
|
||||
const resolveByPath = vi.spyOn(ctx.workspaceRegistry, 'resolveByPath')
|
||||
.mockRejectedValueOnce(remoteFailure)
|
||||
.mockRejectedValueOnce('plain failure')
|
||||
await expect(controller.create({ path: stageDir(root, 'remote-failure') }))
|
||||
.rejects.toBe(remoteFailure)
|
||||
const plainFailure = controller.create({ path: stageDir(root, 'plain-failure') })
|
||||
await expect(plainFailure).rejects.toMatchObject({
|
||||
failure: { code: 'workspace-invalid-path' },
|
||||
})
|
||||
await expect(plainFailure).rejects.toMatchObject({ code: 'workspace/invalid-path' })
|
||||
await expect(plainFailure).rejects.toThrow('plain failure')
|
||||
resolveByPath.mockRestore()
|
||||
|
||||
@@ -168,7 +169,7 @@ describe('WorkspaceController commands', () => {
|
||||
gate.resolve(undefined)
|
||||
await blocker
|
||||
await expect(deletion).resolves.toEqual({ deleted: true })
|
||||
await expect(staleRename).rejects.toMatchObject({ failure: { code: 'workspace-not-found' } })
|
||||
await expect(staleRename).rejects.toMatchObject({ code: 'workspace/not-found' })
|
||||
})
|
||||
|
||||
it('reorders Workspaces and Sessions and archives only known Sessions', async () => {
|
||||
@@ -182,7 +183,7 @@ describe('WorkspaceController commands', () => {
|
||||
workspaceIds: [first.workspace.workspaceId, second.workspace.workspaceId],
|
||||
})
|
||||
await expect(controller.insertBefore({ workspaceId: 'missing' as WorkspaceId }))
|
||||
.rejects.toMatchObject({ failure: { code: 'workspace-not-found' } })
|
||||
.rejects.toMatchObject({ code: 'workspace/not-found' })
|
||||
|
||||
const session = ctx.sessions.create(SessionId('session-one'), {
|
||||
meta: { cwd: first.workspace.path },
|
||||
@@ -197,26 +198,24 @@ describe('WorkspaceController commands', () => {
|
||||
await expect(controller.insertSessionBefore({
|
||||
workspaceId: first.workspace.workspaceId,
|
||||
sessionId: SessionId('missing-session'),
|
||||
})).rejects.toMatchObject({ failure: { code: 'workspace-move-invalid' } })
|
||||
})).rejects.toMatchObject({ code: 'workspace/move-invalid' })
|
||||
await expect(controller.insertSessionBefore({
|
||||
workspaceId: first.workspace.workspaceId,
|
||||
sessionId: session.id,
|
||||
beforeSessionId: SessionId('missing-anchor'),
|
||||
})).rejects.toMatchObject({
|
||||
failure: {
|
||||
code: 'workspace-move-invalid',
|
||||
details: { beforeSessionId: 'missing-anchor' },
|
||||
},
|
||||
code: 'workspace/move-invalid',
|
||||
details: { beforeSessionId: 'missing-anchor' },
|
||||
})
|
||||
await expect(controller.insertSessionBefore({
|
||||
workspaceId: 'missing' as WorkspaceId,
|
||||
sessionId: session.id,
|
||||
})).rejects.toMatchObject({ failure: { code: 'workspace-not-found' } })
|
||||
})).rejects.toMatchObject({ code: 'workspace/not-found' })
|
||||
|
||||
await expect(controller.archiveSession({ sessionId: session.id }))
|
||||
.resolves.toEqual({ archivedSessionIds: [session.id] })
|
||||
await expect(controller.archiveSession({ sessionId: SessionId('unknown') }))
|
||||
.rejects.toMatchObject({ failure: { code: 'session-not-found' } })
|
||||
.rejects.toMatchObject({ code: 'session/not-found' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user