| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- /** Test-owned workspaces face: the renderer standard-kit observable plus recorded actions. */
- import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
- import type {
- DirectoryListing, IWorkspaces, SessionId, SnapshotStore, WorkspaceId, WorkspaceListState, WorkspaceView,
- } from '@deepseek-ai/dsh-client-runtime/client'
- import { workspaceListState } from './fixtures.ts'
- import type { Stabilizer } from './fixtures.ts'
- /**
- * Workspaces test double. Implements the same IWorkspaces face features
- * receive as `ctx.workspaces`, so a production face change breaks this
- * double at compile time. Every action records into {@link
- * TestWorkspaces.calls}; defaults are inert echoes — feature tests needing
- * richer behavior replace them via {@link TestWorkspaces.stub}.
- */
- export class TestWorkspaces implements IWorkspaces {
- /** The useWorkspaces standard feed. */
- readonly list: SnapshotStore<WorkspaceListState>
- /** Calls observed on the action face, newest last. */
- readonly calls: { method: string; args: unknown[] }[] = []
- /** Replaceable action seat: feature tests may stub richer behavior. */
- private readonly stubs = new Map<string, (...args: unknown[]) => unknown>()
- /**
- * @param stabilize - the owning runtime's act wrapper.
- */
- constructor(private readonly stabilize: Stabilizer) {
- this.list = createSnapshotStore<WorkspaceListState>(workspaceListState())
- }
- /**
- * Update the workspace list state through an immer draft.
- * @param mutate - draft mutator.
- */
- async update(mutate: (draft: WorkspaceListState) => void): Promise<void> {
- await this.stabilize(() => { this.list.update(mutate) })
- }
- /**
- * Replace an action's behavior (the recorded call is still appended first).
- * @param method - action name (e.g. 'connectWorkspace').
- * @param impl - replacement behavior.
- */
- stub(method: string, impl: (...args: unknown[]) => unknown): void {
- this.stubs.set(method, impl)
- }
- /**
- * Connect a workspace to its reusable/new blank session (recorded). The
- * default resolves the workspace id back as the session id; stub for
- * cross-session flows.
- * @param workspaceId - target workspace.
- * @returns the connected session id.
- */
- async connectWorkspace(workspaceId: WorkspaceId): Promise<SessionId> {
- this.calls.push({ method: 'connectWorkspace', args: [workspaceId] })
- const stub = this.stubs.get('connectWorkspace')
- if (stub !== undefined) return await (stub(workspaceId) as Promise<SessionId>)
- return `session-of-${workspaceId}` as SessionId
- }
- /**
- * New-session flow (recorded; stubbed behavior runs when installed).
- * @param workspaceId - optional explicit workspace target.
- */
- startSession(workspaceId?: WorkspaceId): void {
- this.calls.push({ method: 'startSession', args: [workspaceId] })
- this.stubs.get('startSession')?.(workspaceId)
- }
- /**
- * Create a Workspace (recorded). The default echoes a view derived from
- * the input; stub for failure or list-coupled flows.
- * @param input - exactly one Host create spelling.
- * @returns the created Workspace view.
- */
- async create(input: { name: string } | { path: string }): Promise<WorkspaceView> {
- this.calls.push({ method: 'create', args: [input] })
- const stub = this.stubs.get('create')
- if (stub !== undefined) return await (stub(input) as Promise<WorkspaceView>)
- const title = 'name' in input ? input.name : input.path
- return {
- workspaceId: `ws-${title}` as WorkspaceId,
- title,
- path: 'path' in input ? input.path : `/${input.name}`,
- sessionIds: [],
- } as unknown as WorkspaceView
- }
- /**
- * Open a path with the host OS default application (recorded; default no-op).
- * @param path - host-resolvable path.
- */
- async openPath(path: string): Promise<void> {
- this.calls.push({ method: 'openPath', args: [path] })
- await (this.stubs.get('openPath')?.(path) as Promise<void> | undefined)
- }
- /**
- * Directory picker (recorded). The default cancels (null); stub to select.
- * @returns the picked path, or null.
- */
- async pickDirectory(): Promise<string | null> {
- this.calls.push({ method: 'pickDirectory', args: [] })
- const stub = this.stubs.get('pickDirectory')
- if (stub !== undefined) return await (stub() as Promise<string | null>)
- return null
- }
- /**
- * Browse listing (recorded). The default serves an empty home level; stub
- * to shape a tree.
- * @param path - absolute directory to list; absent lists the home level.
- * @returns the level's listing.
- */
- async listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing> {
- // The signal is recorded and forwarded like the production face passes
- // it to the wire, so cancellation integration tests can observe or
- // reject on a superseded scan.
- this.calls.push({ method: 'listDirectory', args: [path, signal] })
- const stub = this.stubs.get('listDirectory')
- if (stub !== undefined) return await (stub(path, signal) as Promise<DirectoryListing>)
- // The chain runs root-to-target inclusive, per the DirectoryListing
- // contract — a bare root crumb would mislabel the level in browsers
- // driven by this double.
- return {
- path: '/home/test',
- home: '/home/test',
- crumbs: [
- { name: '/', path: '/', hidden: false },
- { name: 'home', path: '/home', hidden: false },
- { name: 'test', path: '/home/test', hidden: false },
- ],
- entries: [],
- truncated: false,
- }
- }
- /**
- * Browse child creation (recorded). The default joins parent and name.
- * @param path - absolute existing parent directory.
- * @param name - single path segment.
- * @returns the created directory's absolute path.
- */
- async createDirectory(path: string, name: string): Promise<string> {
- this.calls.push({ method: 'createDirectory', args: [path, name] })
- const stub = this.stubs.get('createDirectory')
- if (stub !== undefined) return await (stub(path, name) as Promise<string>)
- return `${path}/${name}`
- }
- /**
- * Rename a Workspace (recorded). The default echoes a minimal view.
- * @param workspaceId - target workspace.
- * @param title - new title.
- * @returns the updated view.
- */
- async rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView> {
- this.calls.push({ method: 'rename', args: [workspaceId, title] })
- const stub = this.stubs.get('rename')
- if (stub !== undefined) return await (stub(workspaceId, title) as Promise<WorkspaceView>)
- return { workspaceId, title, path: `/${title}`, sessionIds: [] } as unknown as WorkspaceView
- }
- /**
- * Delete a Workspace (recorded; default no-op).
- * @param workspaceId - target workspace.
- */
- async delete(workspaceId: WorkspaceId): Promise<void> {
- this.calls.push({ method: 'delete', args: [workspaceId] })
- await (this.stubs.get('delete')?.(workspaceId) as Promise<void> | undefined)
- }
- /**
- * Move an accounted session (recorded). The default echoes a minimal view.
- * @param workspaceId - target workspace.
- * @param sessionId - session to move.
- * @param beforeSessionId - anchor; omitted appends.
- * @returns the updated view.
- */
- async insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise<WorkspaceView> {
- this.calls.push({ method: 'insertSessionBefore', args: [workspaceId, sessionId, beforeSessionId] })
- const stub = this.stubs.get('insertSessionBefore')
- if (stub !== undefined) return await (stub(workspaceId, sessionId, beforeSessionId) as Promise<WorkspaceView>)
- return { workspaceId, title: '', path: '', sessionIds: [sessionId] } as unknown as WorkspaceView
- }
- }
|