|
|
@@ -2,8 +2,6 @@ import { homedir } from 'node:os'
|
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
|
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
|
|
-import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
|
|
|
-import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker'
|
|
|
import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
|
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
|
import { createApiProxy } from '../src/api-proxy.ts'
|
|
|
@@ -26,7 +24,6 @@ function expectOk<T>(response: { readonly result: { readonly ok: true; readonly
|
|
|
}
|
|
|
|
|
|
async function harness(
|
|
|
- picker: DirectoryPickerCapability = { kind: 'native', pick: async () => null },
|
|
|
extras: {
|
|
|
openPath?: (path: string, signal: AbortSignal) => Promise<void>
|
|
|
canOpenPath?: () => boolean
|
|
|
@@ -35,7 +32,6 @@ async function harness(
|
|
|
const ctx = new Context()
|
|
|
contexts.push(ctx)
|
|
|
await ctx.plugin(AgentRegistry)
|
|
|
- ctx.provide('directoryPicker', { capability: () => picker } as never)
|
|
|
const api = createApiProxy(ctx, {
|
|
|
defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }),
|
|
|
cwd: '/tmp/dsh-apiproxy-host',
|
|
|
@@ -45,135 +41,10 @@ async function harness(
|
|
|
return { api }
|
|
|
}
|
|
|
|
|
|
-describe('host.pickDirectory', () => {
|
|
|
- it('returns a selected path or explicit cancellation from the native capability', async () => {
|
|
|
- const selected = await harness({ kind: 'native', pick: async () => '/tmp/project' })
|
|
|
- expect((await selected.api.host.pickDirectory(request({}), new AbortController().signal)).result)
|
|
|
- .toEqual({ ok: true, value: { path: '/tmp/project' } })
|
|
|
-
|
|
|
- const cancelled = await harness({ kind: 'native', pick: async () => null })
|
|
|
- expect((await cancelled.api.host.pickDirectory(request({}), new AbortController().signal)).result)
|
|
|
- .toEqual({ ok: true, value: { path: null } })
|
|
|
- })
|
|
|
-
|
|
|
- it('propagates abort into the native capability as a cancelled RPC error', async () => {
|
|
|
- const { api } = await harness({
|
|
|
- kind: 'native',
|
|
|
- pick: signal => new Promise((_resolve, reject) => {
|
|
|
- signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
|
|
|
- }),
|
|
|
- })
|
|
|
- const abort = new AbortController()
|
|
|
- const pending = api.host.pickDirectory(request({}), abort.signal)
|
|
|
- abort.abort()
|
|
|
- expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
|
|
|
- })
|
|
|
-
|
|
|
- it('folds a non-abort native-chooser failure into an internal error', async () => {
|
|
|
- const { api } = await harness({
|
|
|
- kind: 'native',
|
|
|
- pick: async () => { throw new Error('no chooser installed') },
|
|
|
- })
|
|
|
- const response = await api.host.pickDirectory(request({}), new AbortController().signal)
|
|
|
- expect(response.result).toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
- })
|
|
|
-
|
|
|
- it('refuses the native RPC under a browse composition', async () => {
|
|
|
- const { api } = await harness(BROWSE_STUB)
|
|
|
- const response = await api.host.pickDirectory(request({}), new AbortController().signal)
|
|
|
- expect(response.result).toMatchObject({
|
|
|
- ok: false,
|
|
|
- error: { code: 'directory-picker-unavailable', details: { capability: 'browse' } },
|
|
|
- })
|
|
|
- })
|
|
|
-})
|
|
|
-
|
|
|
-const BROWSE_STUB: DirectoryPickerCapability = {
|
|
|
- kind: 'browse',
|
|
|
- list: async (path) => {
|
|
|
- if (path === '/denied') {
|
|
|
- throw new DirectoryPickerError('directory-unreadable', '/denied', 'cannot list /denied')
|
|
|
- }
|
|
|
- const target = path ?? '/home/user'
|
|
|
- return {
|
|
|
- path: target,
|
|
|
- home: '/home/user',
|
|
|
- crumbs: [{ name: '/', path: '/', hidden: false }],
|
|
|
- entries: [{ name: 'projects', path: `${target}/projects`, hidden: false }],
|
|
|
- truncated: false,
|
|
|
- }
|
|
|
- },
|
|
|
- createDirectory: async (path, name) => {
|
|
|
- if (name === 'taken') {
|
|
|
- throw new DirectoryPickerError('directory-exists', `${path}/${name}`, 'already exists')
|
|
|
- }
|
|
|
- if (name === 'unwritable') throw new Error('disk detached')
|
|
|
- return `${path}/${name}`
|
|
|
- },
|
|
|
-}
|
|
|
-
|
|
|
-describe('host.listDirectory / host.createDirectory', () => {
|
|
|
- it('serves listings and creation through the browse capability, defaulting to home', async () => {
|
|
|
- const { api } = await harness(BROWSE_STUB)
|
|
|
- const home = await api.host.listDirectory(request({}), new AbortController().signal)
|
|
|
- expect(home.result).toMatchObject({ ok: true, value: { path: '/home/user', home: '/home/user' } })
|
|
|
- const listed = await api.host.listDirectory(
|
|
|
- request({ path: '/home/user/projects' }),
|
|
|
- new AbortController().signal,
|
|
|
- )
|
|
|
- expect(listed.result).toMatchObject({ ok: true, value: { path: '/home/user/projects' } })
|
|
|
- const created = await api.host.createDirectory(request({ path: '/home/user', name: 'fresh' }))
|
|
|
- expect(created.result).toEqual({ ok: true, value: { path: '/home/user/fresh' } })
|
|
|
- })
|
|
|
-
|
|
|
- it('maps typed picker failures onto wire errors and folds unknown throws to internal', async () => {
|
|
|
- const { api } = await harness(BROWSE_STUB)
|
|
|
- expect((await api.host.listDirectory(
|
|
|
- request({ path: '/denied' }),
|
|
|
- new AbortController().signal,
|
|
|
- )).result).toMatchObject({
|
|
|
- ok: false,
|
|
|
- error: { code: 'directory-unreadable', details: { path: '/denied' } },
|
|
|
- })
|
|
|
- expect((await api.host.createDirectory(request({ path: '/home/user', name: 'taken' }))).result)
|
|
|
- .toMatchObject({ ok: false, error: { code: 'directory-exists' } })
|
|
|
- expect((await api.host.createDirectory(request({ path: '/home/user', name: 'unwritable' }))).result)
|
|
|
- .toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
- })
|
|
|
-
|
|
|
- it('reports an aborted listing as cancelled', async () => {
|
|
|
- const { api } = await harness({
|
|
|
- kind: 'browse',
|
|
|
- list: (_path, signal) => new Promise((_resolve, reject) => {
|
|
|
- signal?.addEventListener('abort', () => { reject(new Error('scan aborted')) }, { once: true })
|
|
|
- }),
|
|
|
- createDirectory: async () => '/never',
|
|
|
- })
|
|
|
- const abort = new AbortController()
|
|
|
- const pending = api.host.listDirectory(request({}), abort.signal)
|
|
|
- abort.abort()
|
|
|
- expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
|
|
|
- })
|
|
|
-
|
|
|
- it('refuses the browse RPCs under a native composition', async () => {
|
|
|
- const { api } = await harness()
|
|
|
- expect((await api.host.listDirectory(request({}), new AbortController().signal)).result)
|
|
|
- .toMatchObject({
|
|
|
- ok: false,
|
|
|
- error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
|
|
|
- })
|
|
|
- expect((await api.host.createDirectory(request({ path: '/x', name: 'y' }))).result)
|
|
|
- .toMatchObject({
|
|
|
- ok: false,
|
|
|
- error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
|
|
|
- })
|
|
|
- })
|
|
|
-})
|
|
|
-
|
|
|
describe('host.openPath', () => {
|
|
|
it('describes whether the deployment can reach a native desktop', async () => {
|
|
|
- const visible = await harness(undefined, { canOpenPath: () => true })
|
|
|
- const headless = await harness(undefined, { canOpenPath: () => false })
|
|
|
+ const visible = await harness({ canOpenPath: () => true })
|
|
|
+ const headless = await harness({ canOpenPath: () => false })
|
|
|
expect(expectOk(await visible.api.host.describe(request({}))).canOpenPath).toBe(true)
|
|
|
expect(expectOk(await headless.api.host.describe(request({}))).canOpenPath).toBe(false)
|
|
|
expect(expectOk(await visible.api.host.describe(request({}))).home).toBe(homedir())
|
|
|
@@ -181,7 +52,7 @@ describe('host.openPath', () => {
|
|
|
|
|
|
it('opens through the injected native boundary', async () => {
|
|
|
const opened: string[] = []
|
|
|
- const { api } = await harness(undefined, {
|
|
|
+ const { api } = await harness({
|
|
|
openPath: async (path) => { opened.push(path) },
|
|
|
})
|
|
|
expect((await api.host.openPath(
|
|
|
@@ -192,7 +63,7 @@ describe('host.openPath', () => {
|
|
|
})
|
|
|
|
|
|
it('propagates abort into the native boundary as a cancelled RPC error', async () => {
|
|
|
- const { api } = await harness(undefined, {
|
|
|
+ const { api } = await harness({
|
|
|
openPath: (_path, signal) => new Promise((_resolve, reject) => {
|
|
|
signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
|
|
|
}),
|