From 0ea9a456c00b24442a8328b9e43e008e766df85a Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:15:49 +0800 Subject: [PATCH] refactor(workspace): move Client ownership into Workspace Controller --- .../api/workspace-controller/package.json | 6 +- .../workspace-controller/src/client/index.ts | 42 +++- .../workspace-controller/src/client/model.ts | 13 +- .../workspace-controller/src/client}/path.ts | 8 +- .../src/client/service.ts | 131 ++++++++++ .../tests/path.client.spec.ts | 2 +- .../tests/transport.client.spec.ts | 229 +++++++++++++++++- .../workspace-controller/tsconfig.client.json | 4 + 8 files changed, 408 insertions(+), 27 deletions(-) rename packages/{client/runtime/src/client/workspaces => api/workspace-controller/src/client}/path.ts (86%) create mode 100644 packages/api/workspace-controller/src/client/service.ts rename packages/{client/runtime => api/workspace-controller}/tests/path.client.spec.ts (98%) diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index eef8a4c64d..29245fef86 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -47,7 +47,8 @@ "@deepseek-ai/dsh-api-gateway/client" ], "inject": [ - "@deepseek-ai/dsh-api-gateway" + "@deepseek-ai/dsh-api-gateway", + "@deepseek-ai/dsh-client-connection" ], "platform": "web" } @@ -74,6 +75,7 @@ "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-api-gateway": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", @@ -83,6 +85,8 @@ "devDependencies": { "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-api-gateway": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", + "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", diff --git a/packages/api/workspace-controller/src/client/index.ts b/packages/api/workspace-controller/src/client/index.ts index 8562f9cfaa..0ec758391e 100644 --- a/packages/api/workspace-controller/src/client/index.ts +++ b/packages/api/workspace-controller/src/client/index.ts @@ -1,5 +1,6 @@ /** Workspace-specific adapter for the Gateway-owned snapshot stream lifecycle. */ +import type { Context } from '@deepseek-ai/cordis' import { RemoteSnapshotStream, RemoteStreamCarrierError, @@ -7,14 +8,20 @@ import { } from '@deepseek-ai/dsh-api-gateway/client' import type { WorkspaceFollowFrame, WorkspaceFollowIncrement } from '../types.ts' import type { WorkspaceFollowSink, WorkspaceRemote } from './model.ts' +import { ClientWorkspaceModel } from './model.ts' +import { WorkspaceController } from './service.ts' export { ClientWorkspaceModel } from './model.ts' export type { - WorkspaceFollowSink, WorkspaceListPhase, WorkspaceListSnapshot, WorkspaceRemote, + WorkspaceFollowSink, WorkspaceListPhase, WorkspaceRemote, WorkspaceSnapshot, } from './model.ts' +export { abbreviateHomePath, resolveWorkspacePath } from './path.ts' +export { WorkspaceController, WorkspaceCreateError } from './service.ts' +export type { IWorkspaces, WorkspaceSource } from './service.ts' +export type { WorkspaceId, WorkspaceView } from '../types.ts' type WorkspaceStreamRemote = Pick & { - readonly workspace: Pick + readonly workspace: WorkspaceRemote } type WorkspaceBaselineFrame = Extract @@ -25,8 +32,35 @@ export type WorkspaceStateStream = RemoteSnapshotStream< WorkspaceFollowIncrement > -/** Workspace Controller's Client row exports library values and installs no Cordis service. */ -export function apply(): void {} +declare module '@deepseek-ai/cordis' { + interface Context { + /** React-free Client Workspace state and commands. */ + workspaces: import('./service.ts').IWorkspaces + } +} + +/** Required Client Remote services. */ +export const inject = ['remote', 'remote.workspace'] + +/** + * Install Client Workspace state, commands, and reconnecting follow control. + * @param ctx - Client root Context. + */ +export function apply(ctx: Context): void { + const remote = ctx.remote as WorkspaceStreamRemote + const model = new ClientWorkspaceModel(remote.workspace) + new WorkspaceController(ctx, model) + const control = createWorkspaceStateStream(remote, { + accept: model, + carrierFailed: () => { model.handleCarrierFailure() }, + failed: (error) => { model.handleStreamFailure(error) }, + }) + control.start() + ctx.effect( + () => async () => { await control.dispose() }, + 'workspace-controller.client.control', + ) +} /** Domain sinks used by the Workspace state stream. */ export interface WorkspaceStateStreamOptions { diff --git a/packages/api/workspace-controller/src/client/model.ts b/packages/api/workspace-controller/src/client/model.ts index 14854d9244..2ec365e8b2 100644 --- a/packages/api/workspace-controller/src/client/model.ts +++ b/packages/api/workspace-controller/src/client/model.ts @@ -1,5 +1,6 @@ /** Client-side Workspace state model shared by Remote transport and UI projection. */ +import { notifySubscribers } from '@deepseek-ai/dsh-client-store' import type {} from '@deepseek-ai/dsh-api-workspace-controller/remote' import type { RemoteFailure, RemoteResult, TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol' import type { @@ -23,7 +24,7 @@ export type WorkspaceRemote = TypertClientRemote['workspace'] export type WorkspaceListPhase = 'pending' | 'ready' /** Immutable Client Workspace state. */ -export interface WorkspaceListSnapshot { +export interface WorkspaceSnapshot { readonly items: readonly WorkspaceView[] /** Complete registry-global archive set in Host order. */ readonly archivedSessionIds: WorkspaceArchiveValue['archivedSessionIds'] @@ -52,7 +53,7 @@ export interface WorkspaceFollowSink { export class ClientWorkspaceModel implements WorkspaceFollowSink { private items: readonly WorkspaceView[] = [] private archivedSessionIds: WorkspaceArchiveValue['archivedSessionIds'] = [] - private state: WorkspaceListSnapshot['state'] = 'loading' + private state: WorkspaceSnapshot['state'] = 'loading' private phase: WorkspaceListPhase = 'pending' private error: RemoteFailure | null = null /** Latest local reorder request; only its unary echo may install order. */ @@ -64,7 +65,7 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink { /** Host Workspace ids are never reused, so delayed data cannot resurrect a removed row. */ private readonly removedIds = new Set() private readonly listeners = new Set<() => void>() - private snapshotCache: WorkspaceListSnapshot + private snapshotCache: WorkspaceSnapshot private snapshotDirty = false private notificationPending = false private notificationScheduled = false @@ -251,12 +252,12 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink { * Read the cached state, rebuilding it first when necessary. * @returns the current stable Workspace list snapshot. */ - getSnapshot(): WorkspaceListSnapshot { + getSnapshot(): WorkspaceSnapshot { this.refreshSnapshot() return this.snapshotCache } - private buildSnapshot(): WorkspaceListSnapshot { + private buildSnapshot(): WorkspaceSnapshot { return { items: this.items, archivedSessionIds: this.archivedSessionIds, @@ -346,7 +347,7 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink { if (!this.notificationPending || this.listeners.size === 0) return this.notificationPending = false this.refreshSnapshot() - for (const listener of this.listeners) listener() + notifySubscribers(this.listeners, '[workspace-controller]') } private refreshSnapshot(): void { diff --git a/packages/client/runtime/src/client/workspaces/path.ts b/packages/api/workspace-controller/src/client/path.ts similarity index 86% rename from packages/client/runtime/src/client/workspaces/path.ts rename to packages/api/workspace-controller/src/client/path.ts index 8bb3aa6645..334c56b007 100644 --- a/packages/client/runtime/src/client/workspaces/path.ts +++ b/packages/api/workspace-controller/src/client/path.ts @@ -1,8 +1,8 @@ /** * Resolve a workspace-relative path into the Host-facing spelling used by openPath. - * @param cwd - session workspace root, when known. - * @param path - absolute or workspace-relative path. - * @returns an absolute path when a workspace root is available, otherwise the original path. + * @param cwd - Session Workspace root, when known. + * @param path - absolute or Workspace-relative path. + * @returns an absolute path when a Workspace root is available, otherwise the original path. */ export function resolveWorkspacePath(cwd: string | undefined, path: string): string { if (path.startsWith('/') || isWindowsStylePath(path)) return path @@ -22,7 +22,7 @@ function isWindowsStylePath(value: string): boolean { * verbatim, including when `home` itself is a Windows path. A missing, empty, * or filesystem-root `home` leaves `path` unchanged so `/` cannot become `~`. * @param path - absolute or already-short display path. - * @param home - host account home from `host.describe`; absent skips abbreviation. + * @param home - Host account home from `host.describe`; absent skips abbreviation. * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`. */ export function abbreviateHomePath(path: string, home?: string): string { diff --git a/packages/api/workspace-controller/src/client/service.ts b/packages/api/workspace-controller/src/client/service.ts new file mode 100644 index 0000000000..993ac55ca6 --- /dev/null +++ b/packages/api/workspace-controller/src/client/service.ts @@ -0,0 +1,131 @@ +/** React-free Client Workspace service and command facade. */ + +import { Service, type Context } from '@deepseek-ai/cordis' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol' +import type { WorkspaceId, WorkspaceView } from '../types.ts' +import type { ClientWorkspaceModel, WorkspaceSnapshot } from './model.ts' + +/** Structured create failure for callers that distinguish Host business errors. */ +export class WorkspaceCreateError extends Error { + override readonly name = 'WorkspaceCreateError' + + /** @param rpcError - Host business or folded transport failure. */ + constructor(readonly rpcError: RemoteFailure) { + super(`workspace create failed: ${rpcError.code}: ${rpcError.message}`) + } +} + +/** Bare observable source for the Workspace Controller snapshot. */ +export interface WorkspaceSource { + /** Read the identity-stable current snapshot. */ + getSnapshot(): WorkspaceSnapshot + /** + * Subscribe to snapshot changes. + * @param listener - invalidation callback. + * @returns unsubscribe function. + */ + subscribe(listener: () => void): () => void +} + +/** Workspace Controller's Client service face. */ +export interface IWorkspaces { + /** Host-authoritative Workspace rows, order, archive set, and follow lifecycle. */ + readonly list: WorkspaceSource + /** + * Register an existing path as a Workspace. + * @param input - Host create payload. + * @returns the created or idempotently resolved Workspace. + */ + create(input: { path: string }): Promise + /** + * Rename a Workspace. + * @param workspaceId - target Workspace. + * @param title - new display title. + * @returns the renamed Workspace. + */ + rename(workspaceId: WorkspaceId, title: string): Promise + /** + * Delete a Workspace registration without deleting Sessions or files. + * @param workspaceId - target Workspace. + */ + delete(workspaceId: WorkspaceId): Promise + /** + * Move a Workspace within the Host registry order. + * @param workspaceId - Workspace to move. + * @param beforeWorkspaceId - anchor Workspace; omitted appends. + */ + insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise + /** + * Archive a Session from Workspace grouping surfaces. + * @param sessionId - Session to archive. + */ + archiveSession(sessionId: SessionId): Promise + /** + * Move a Session within one Workspace account. + * @param workspaceId - owning Workspace. + * @param sessionId - Session to move. + * @param beforeSessionId - anchor Session; omitted appends. + * @returns the changed Workspace. + */ + insertSessionBefore( + workspaceId: WorkspaceId, + sessionId: SessionId, + beforeSessionId?: SessionId, + ): Promise +} + +/** Owns the bare Workspace snapshot and Workspace-only commands. */ +export class WorkspaceController extends Service implements IWorkspaces { + readonly list: WorkspaceSource + + /** + * @param ctx - Client root Context. + * @param model - Remote-backed Workspace state model. + */ + constructor(ctx: Context, private readonly model: ClientWorkspaceModel) { + super(ctx, 'workspaces') + this.list = model + } + + async create(input: { path: string }): Promise { + const result = await this.model.create(input) + if (!result.ok) throw new WorkspaceCreateError(result.error) + return result.value.workspace + } + + async rename(workspaceId: WorkspaceId, title: string): Promise { + const result = await this.model.rename(workspaceId, title) + if (!result.ok) throw commandError('rename', result.error) + return result.value.workspace + } + + async delete(workspaceId: WorkspaceId): Promise { + const result = await this.model.delete(workspaceId) + if (!result.ok) throw commandError('delete', result.error) + } + + async insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise { + const result = await this.model.insertBefore(workspaceId, beforeWorkspaceId) + if (!result.ok) throw commandError('reorder', result.error) + } + + async archiveSession(sessionId: SessionId): Promise { + const result = await this.model.archiveSession(sessionId) + if (!result.ok) throw commandError('session archive', result.error) + } + + async insertSessionBefore( + workspaceId: WorkspaceId, + sessionId: SessionId, + beforeSessionId?: SessionId, + ): Promise { + const result = await this.model.insertSessionBefore(workspaceId, sessionId, beforeSessionId) + if (!result.ok) throw commandError('move', result.error) + return result.value.workspace + } +} + +function commandError(operation: string, failure: RemoteFailure): Error { + return new Error(`workspace ${operation} failed: ${failure.code}: ${failure.message}`) +} diff --git a/packages/client/runtime/tests/path.client.spec.ts b/packages/api/workspace-controller/tests/path.client.spec.ts similarity index 98% rename from packages/client/runtime/tests/path.client.spec.ts rename to packages/api/workspace-controller/tests/path.client.spec.ts index 455df0d112..48a7d63b85 100644 --- a/packages/client/runtime/tests/path.client.spec.ts +++ b/packages/api/workspace-controller/tests/path.client.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { abbreviateHomePath, resolveWorkspacePath } from '../src/client/workspaces/path.ts' +import { abbreviateHomePath, resolveWorkspacePath } from '../src/client/path.ts' describe('abbreviateHomePath', () => { it('collapses a POSIX home and its descendants', () => { diff --git a/packages/api/workspace-controller/tests/transport.client.spec.ts b/packages/api/workspace-controller/tests/transport.client.spec.ts index dd4517600c..d71cfba44e 100644 --- a/packages/api/workspace-controller/tests/transport.client.spec.ts +++ b/packages/api/workspace-controller/tests/transport.client.spec.ts @@ -1,14 +1,19 @@ +import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { RemoteStream, RemoteStreamCarrierError, type RemoteStreamOptions, } from '@deepseek-ai/dsh-api-gateway/client' -import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/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 * as WorkspaceClientPlugin from '../src/client/index.ts' import { - apply, + ClientWorkspaceModel, createWorkspaceStateStream, + WorkspaceController, + WorkspaceCreateError, type WorkspaceFollowSink, type WorkspaceRemote, } from '../src/client/index.ts' @@ -24,15 +29,12 @@ import type { WorkspaceInsertSessionBeforeRequest, WorkspaceOrderValue, WorkspaceRenameRequest, + WorkspaceError, + WorkspaceId, WorkspaceValue, + WorkspaceView, } from '../src/types.ts' -interface Generation { - readonly frames: readonly WorkspaceFollowFrame[] - readonly error?: unknown - readonly hold?: boolean -} - const AVAILABLE_CONNECTION = { hostDescription: { getSnapshot: () => ({ @@ -52,6 +54,14 @@ function workspaceClient( } } +interface Generation { + readonly frames: readonly WorkspaceFollowFrame[] + readonly error?: unknown + readonly hold?: boolean + readonly afterAbort?: () => void + readonly afterAbortError?: unknown +} + const baseline = (id?: string): Extract => ({ type: 'baseline', value: { @@ -67,6 +77,29 @@ const baseline = (id?: string): Extract id as WorkspaceId +const sid = (id: string): SessionId => SessionId(id) + +function workspace(id: string, overrides: Partial = {}): WorkspaceView { + return { + workspaceId: wid(id), + path: `/work/${id}`, + title: id, + sessionIds: [], + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + ...overrides, + } +} + +function remoteOk(value: T): RemoteResult { + return { ok: true, value } +} + +function remoteFailure(error: WorkspaceError): RemoteResult { + return { ok: false, error } +} + function accepts(overrides: Partial = {}): WorkspaceFollowSink { const ignore = (): void => {} return { @@ -119,13 +152,128 @@ class ScriptedWorkspaceRemote implements WorkspaceRemote { await new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + generation.afterAbort?.() + if (generation.afterAbortError !== undefined) throw generation.afterAbortError } } } -describe('Workspace Client snapshot adapter', () => { - it('installs no Client service and maps the baseline plus every increment', async () => { - apply() +class CommandWorkspaceRemote implements WorkspaceRemote { + readonly create = vi.fn(request => Promise.resolve(remoteOk({ + workspace: workspace('created', { path: request.path }), + created: true, + }))) + + readonly rename = vi.fn(request => Promise.resolve(remoteOk({ + workspace: workspace(String(request.workspaceId), { title: request.title }), + }))) + + readonly delete = vi.fn(() => Promise.resolve(remoteOk({ deleted: true }))) + + readonly insertBefore = vi.fn(request => Promise.resolve(remoteOk({ + workspaceIds: [request.workspaceId], + }))) + + readonly insertSessionBefore = vi.fn(request => Promise.resolve(remoteOk({ + workspace: workspace(String(request.workspaceId), { sessionIds: [request.sessionId] }), + }))) + + readonly archiveSession = vi.fn(request => Promise.resolve(remoteOk({ + archivedSessionIds: [request.sessionId], + }))) + + async *follow(_signal?: AbortSignal): AsyncIterable {} +} + +async function waitFor(check: () => void): Promise { + for (let attempt = 0; attempt < 40; attempt++) { + try { + check() + return + } catch { + await Promise.resolve() + } + } + check() +} + +function provideClientServices(ctx: Context, remote: WorkspaceRemote): void { + const connection: ConnectionHandle = { + api: {} as ConnectionHandle['api'], + isLoopback: true, + hostDescription: { + getSnapshot: () => ({ + version: 'fixture', + cwd: '/fixture', + attachedSessions: 0, + home: '/home/fixture', + canOpenPath: true, + }), + subscribe: () => () => {}, + }, + rpc: { + call: () => Promise.reject(new Error('unexpected generic RPC call')), + }, + registerGenerationSource: () => () => {}, + start: () => ({ stop: () => {} }), + } + ctx.reflect.provide('connection', connection) + ctx.reflect.provide('remote', workspaceClient(remote, connection)) + ctx.reflect.provide('remote.workspace', remote) +} + +describe('Workspace Controller Client apply', () => { + it('provides the Workspace service and stops its follow generation with the plugin fiber', async () => { + const ctx = new Context() + const remote = new ScriptedWorkspaceRemote([{ frames: [baseline('mounted')], hold: true }]) + provideClientServices(ctx, remote) + const fiber = ctx.plugin(WorkspaceClientPlugin) + await fiber + await waitFor(() => { + expect(ctx.workspaces.list.getSnapshot()).toMatchObject({ + phase: 'ready', + state: 'idle', + items: [{ workspaceId: 'mounted' }], + }) + }) + + await fiber.dispose() + + expect(remote.signals[0]?.aborted).toBe(true) + expect(ctx.get('workspaces')).toBeUndefined() + }) + + it('marks carrier loss while retrying and publishes a later protocol failure', async () => { + const ctx = new Context() + const remote = new ScriptedWorkspaceRemote([ + { + frames: [baseline('old')], + error: new RemoteStreamCarrierError('generation lost'), + }, + { frames: [baseline('fresh'), baseline('duplicate')] }, + ]) + provideClientServices(ctx, remote) + const carrierFailure = vi.spyOn(ClientWorkspaceModel.prototype, 'handleCarrierFailure') + const streamFailure = vi.spyOn(ClientWorkspaceModel.prototype, 'handleStreamFailure') + const fiber = ctx.plugin(WorkspaceClientPlugin) + await fiber + await waitFor(() => { + expect(ctx.workspaces.list.getSnapshot()).toMatchObject({ + phase: 'ready', + state: 'error', + items: [{ workspaceId: 'fresh' }], + error: { code: 'internal', message: 'Workspace state stream emitted more than one opening snapshot' }, + }) + }) + + expect(carrierFailure).toHaveBeenCalledOnce() + expect(streamFailure).toHaveBeenCalledOnce() + await fiber.dispose() + }) +}) + +describe('Workspace state stream', () => { + it('delivers one baseline followed by increments', async () => { const opening = baseline('one') const workspace = opening.value.items[0]! const remote = new ScriptedWorkspaceRemote([{ @@ -287,3 +435,62 @@ describe('Workspace Client snapshot adapter', () => { await stream.dispose() }) }) + +describe('WorkspaceController', () => { + it('publishes the model source and exposes successful Workspace commands', async () => { + const remote = new CommandWorkspaceRemote() + const model = new ClientWorkspaceModel(remote) + model.replaceBaseline({ items: [workspace('one')], archivedSessionIds: [] }) + const controller = new WorkspaceController(new Context(), model) + + expect(controller.list).toBe(model) + await expect(controller.create({ path: '/work/created' })).resolves.toMatchObject({ workspaceId: 'created' }) + await expect(controller.rename(wid('one'), 'renamed')).resolves.toMatchObject({ title: 'renamed' }) + await expect(controller.insertBefore(wid('one'))).resolves.toBeUndefined() + await expect(controller.insertSessionBefore(wid('one'), sid('session'))).resolves.toMatchObject({ + sessionIds: ['session'], + }) + await expect(controller.archiveSession(sid('session'))).resolves.toBeUndefined() + await expect(controller.delete(wid('one'))).resolves.toBeUndefined() + }) + + 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') }, + } + + remote.create.mockResolvedValueOnce(remoteFailure({ + code: 'workspace-invalid-path', + message: 'missing path', + details: { path: '/missing' }, + })) + const create = controller.create({ path: '/missing' }) + await expect(create).rejects.toBeInstanceOf(WorkspaceCreateError) + 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') + remote.delete.mockResolvedValueOnce(remoteFailure(missingWorkspace)) + 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') + 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.insertSessionBefore(wid('missing'), sid('session'))) + .rejects.toThrow('workspace move failed: workspace-move-invalid: invalid move') + }) +}) diff --git a/packages/api/workspace-controller/tsconfig.client.json b/packages/api/workspace-controller/tsconfig.client.json index ada2e4adf7..e2f1eacd1c 100644 --- a/packages/api/workspace-controller/tsconfig.client.json +++ b/packages/api/workspace-controller/tsconfig.client.json @@ -8,11 +8,15 @@ "files": [ "src/client/index.ts", "src/client/model.ts", + "src/client/path.ts", + "src/client/service.ts", "src/types.ts" ], "references": [ { "path": "../../../vendor/cordis" }, { "path": "../gateway/tsconfig.client.json" }, + { "path": "../../client/connection/tsconfig.client.json" }, + { "path": "../../client/store" }, { "path": "../../core/session" }, { "path": "../../typert/protocol" }, { "path": "../../workspace/workspace" }