mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
test(api): complete migrated Remote coverage
This commit is contained in:
@@ -305,9 +305,9 @@ describe('Web session model selection', () => {
|
||||
model: 'private-preview',
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
})
|
||||
createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
|
||||
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
|
||||
|
||||
const catalog = await buildModelCatalog(ctx)
|
||||
const catalog = expectValue(await remote.modelCatalog())
|
||||
expect(currentSelection(ctx, sessionId)).toEqual({
|
||||
provider: 'deepseek-official',
|
||||
model: 'private-preview',
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts'
|
||||
import {
|
||||
createSessionTestController,
|
||||
createSessionTestRemote,
|
||||
testSessionPersistence,
|
||||
} from './test-remote.ts'
|
||||
|
||||
async function context(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
@@ -92,4 +96,54 @@ describe('session/openWorkspacePath', () => {
|
||||
await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal))
|
||||
.resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } })
|
||||
})
|
||||
|
||||
it('classifies inspection cancellation and non-session failures', async () => {
|
||||
const ctx = await context()
|
||||
const controller = createSessionTestController(ctx, {
|
||||
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
|
||||
cwd: '/default',
|
||||
})
|
||||
const inspect = vi.spyOn(controller, 'inspect')
|
||||
const aborted = new AbortController()
|
||||
inspect.mockImplementationOnce(async () => {
|
||||
aborted.abort(new Error('cancelled'))
|
||||
throw new Error('inspection stopped')
|
||||
})
|
||||
await expect(controller.openWorkspacePath({
|
||||
sessionId: SessionId('inspection-cancelled'), path: 'result.html',
|
||||
}, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
|
||||
inspect.mockRejectedValueOnce('storage offline')
|
||||
await expect(controller.openWorkspacePath({
|
||||
sessionId: SessionId('inspection-failed'), path: 'result.html',
|
||||
}, new AbortController().signal)).rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: expect.stringContaining('storage offline') },
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies opener cancellation and non-Error failures', async () => {
|
||||
const ctx = await context()
|
||||
const sessionId = SessionId('open-error-kinds')
|
||||
ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
|
||||
const aborted = new AbortController()
|
||||
const openPath = vi.fn()
|
||||
.mockImplementationOnce(async () => {
|
||||
aborted.abort(new Error('cancelled'))
|
||||
throw new Error('opening stopped')
|
||||
})
|
||||
.mockRejectedValueOnce('desktop unavailable')
|
||||
const controller = createSessionTestController(ctx, {
|
||||
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
|
||||
cwd: '/default',
|
||||
openPath,
|
||||
})
|
||||
|
||||
await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
await expect(controller.openWorkspacePath({
|
||||
sessionId, path: 'second.html',
|
||||
}, new AbortController().signal)).rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -188,4 +188,41 @@ describe('SessionSkillCatalog', () => {
|
||||
failure: { code: 'internal', message: expect.stringContaining('skill registry is absent') },
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects observations without projections or a project cwd', async () => {
|
||||
const ctx = await context()
|
||||
const sessionId = SessionId('incomplete-skills')
|
||||
const withoutProjections = { ...observation(sessionId, { cwd: '/project' }), projections: undefined }
|
||||
const observeSession = vi.fn()
|
||||
.mockResolvedValueOnce(withoutProjections)
|
||||
.mockResolvedValueOnce(observation(sessionId))
|
||||
ctx.provide('sessionQuery', { observeSession } as never)
|
||||
const catalog = new SessionSkillCatalog(ctx)
|
||||
|
||||
await expect(catalog.list({ sessionId }, new AbortController().signal))
|
||||
.rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: expect.stringContaining('projected Session observation') },
|
||||
})
|
||||
await expect(catalog.list({ sessionId }, new AbortController().signal))
|
||||
.rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: expect.stringContaining('has no project cwd') },
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies a provider listing failure', async () => {
|
||||
const ctx = await context()
|
||||
const sessionId = SessionId('failed-skills')
|
||||
ctx.provide('sessionQuery', {
|
||||
observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })),
|
||||
} as never)
|
||||
ctx.provide('skills', {
|
||||
list: () => Promise.reject(new Error('catalog offline')),
|
||||
} as never)
|
||||
const catalog = new SessionSkillCatalog(ctx)
|
||||
|
||||
await expect(catalog.list({ sessionId }, new AbortController().signal))
|
||||
.rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: 'skill listing failed: Error: catalog offline' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@deepseek-ai/dsh-typert-protocol'
|
||||
import SessionController from '../src/index.ts'
|
||||
import type {
|
||||
ModelCatalog,
|
||||
SessionAttachmentRequest,
|
||||
SessionAttachmentValue,
|
||||
SessionCancelRequest,
|
||||
@@ -54,6 +55,7 @@ export interface TestSessionRemote {
|
||||
search(request: SessionSearchRequest, signal?: AbortSignal): Promise<RemoteResult<SessionSearchValue>>
|
||||
create(request: SessionCreateRequest): Promise<RemoteResult<SessionCreateValue>>
|
||||
selectModel(request: SessionSelectModelRequest): Promise<RemoteResult<SessionSelectModelValue>>
|
||||
modelCatalog(): Promise<RemoteResult<ModelCatalog>>
|
||||
rename(request: SessionRenameRequest): Promise<RemoteResult<SessionRenameValue>>
|
||||
fork(request: SessionForkRequest): Promise<RemoteResult<SessionForkValue>>
|
||||
prompt(request: SessionPromptRequest, signal?: AbortSignal): Promise<RemoteResult<SessionPromptValue>>
|
||||
@@ -241,6 +243,7 @@ export function createSessionTestRemote(
|
||||
),
|
||||
create: request => remoteResult(() => direct.create(request)),
|
||||
selectModel: request => remoteResult(() => direct.selectModel(request)),
|
||||
modelCatalog: () => remoteResult(() => direct.modelCatalog()),
|
||||
rename: request => remoteResult(() => direct.rename(request)),
|
||||
fork: request => remoteResult(() => direct.fork(request)),
|
||||
prompt: (request, signal = new AbortController().signal) => remoteResult(
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import {
|
||||
InvalidPresetIdError,
|
||||
PresetExistsError,
|
||||
UnknownPresetError,
|
||||
} from '@deepseek-ai/dsh-agent-presets'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import type { SettingsDescriptor, SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
|
||||
@@ -306,6 +311,32 @@ describe('the settings Remote namespace a configuration page calls', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('classifies cancellation while preparing or opening the settings document', async () => {
|
||||
const preparing = new Context()
|
||||
await preparing.plugin(DocumentSettings)
|
||||
const prepareAbort = new AbortController()
|
||||
vi.spyOn(preparing.settings, 'prepareDocument').mockImplementation(async () => {
|
||||
prepareAbort.abort(new Error('cancelled'))
|
||||
throw new Error('preparation stopped')
|
||||
})
|
||||
const preparingController = new SettingsController(preparing)
|
||||
await expect(preparingController.openSettingsDocument(prepareAbort.signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
|
||||
const opening = new Context()
|
||||
await opening.plugin(DocumentSettings)
|
||||
vi.spyOn(opening.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml')
|
||||
const openAbort = new AbortController()
|
||||
const openingController = new SettingsController(opening, {}, {
|
||||
openTextFile: async () => {
|
||||
openAbort.abort(new Error('cancelled'))
|
||||
throw new Error('opening stopped')
|
||||
},
|
||||
})
|
||||
await expect(openingController.openSettingsDocument(openAbort.signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
})
|
||||
|
||||
it('opens a user Agent preset directory or returns its path without a native opener', async () => {
|
||||
const ctx = new Context()
|
||||
ctx.provide('agentPresets', {
|
||||
@@ -331,6 +362,21 @@ describe('the settings Remote namespace a configuration page calls', () => {
|
||||
.resolves.toEqual({ opened: false, path: '/presets/mine' })
|
||||
})
|
||||
|
||||
it('covers native-open detection defaults and explicit overrides', () => {
|
||||
const fromInjectedOpener = new SettingsController(new Context(), {}, {
|
||||
openPath: () => Promise.resolve(),
|
||||
})
|
||||
expect((fromInjectedOpener as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(true)
|
||||
|
||||
const detected = new SettingsController(new Context())
|
||||
expect(typeof (detected as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe('boolean')
|
||||
|
||||
const override = vi.fn(() => false)
|
||||
const overridden = new SettingsController(new Context(), {}, { canOpenPath: override })
|
||||
expect((overridden as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(false)
|
||||
expect(override).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('refuses a shipped Agent preset and a missing preset provider', async () => {
|
||||
const ctx = new Context()
|
||||
ctx.provide('agentPresets', {
|
||||
@@ -346,4 +392,54 @@ describe('the settings Remote namespace a configuration page calls', () => {
|
||||
await expect(missing.openAgentPresetDirectory('mine', new AbortController().signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'agent-preset-not-found' } })
|
||||
})
|
||||
|
||||
it('rejects an empty Agent preset id before resolving a provider', async () => {
|
||||
const resolve = vi.fn()
|
||||
const ctx = new Context()
|
||||
ctx.provide('agentPresets', { resolve } as never)
|
||||
const controller = new SettingsController(ctx)
|
||||
|
||||
await expect(controller.openAgentPresetDirectory('', new AbortController().signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'bad-request' } })
|
||||
expect(resolve).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[new UnknownPresetError('missing', ['standard']), 'agent-preset-not-found'],
|
||||
[new InvalidPresetIdError('../bad'), 'agent-preset-invalid'],
|
||||
[new PresetExistsError('taken'), 'agent-preset-invalid'],
|
||||
[new TypertRemoteFailure({ code: 'cancelled', message: 'cancelled', details: {} }), 'cancelled'],
|
||||
['unexpected preset failure', 'internal'],
|
||||
] as const)('maps Agent preset resolution failure %#', async (error, code) => {
|
||||
const ctx = new Context()
|
||||
ctx.provide('agentPresets', { resolve: () => Promise.reject(error) } as never)
|
||||
const controller = new SettingsController(ctx)
|
||||
|
||||
await expect(controller.openAgentPresetDirectory('mine', new AbortController().signal))
|
||||
.rejects.toMatchObject({ failure: { code } })
|
||||
})
|
||||
|
||||
it('classifies cancellation and non-Error failures from the preset opener', async () => {
|
||||
const ctx = new Context()
|
||||
ctx.provide('agentPresets', {
|
||||
resolve: (id: string) => Promise.resolve({
|
||||
id, trust: 'user', path: `/presets/${id}/agent.cordis.yml`,
|
||||
}),
|
||||
} as never)
|
||||
const abort = new AbortController()
|
||||
const openPath = vi.fn()
|
||||
.mockImplementationOnce(async () => {
|
||||
abort.abort(new Error('cancelled'))
|
||||
throw new Error('opening stopped')
|
||||
})
|
||||
.mockRejectedValueOnce('desktop unavailable')
|
||||
const controller = new SettingsController(ctx, { nativeOpen: true }, { openPath })
|
||||
|
||||
await expect(controller.openAgentPresetDirectory('first', abort.signal))
|
||||
.rejects.toMatchObject({ failure: { code: 'cancelled' } })
|
||||
await expect(controller.openAgentPresetDirectory('second', new AbortController().signal))
|
||||
.rejects.toMatchObject({
|
||||
failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -181,6 +181,21 @@ describe('ModelsSettingsStore', () => {
|
||||
expect(store.store.getSnapshot().status).toBe('ready')
|
||||
})
|
||||
|
||||
it('surfaces a configurable-provider directory failure', async () => {
|
||||
const { face, mirror } = api()
|
||||
const llm = (face as unknown as {
|
||||
llm: { listConfigurableProviders: () => Promise<RemoteAnswer<never>> }
|
||||
}).llm
|
||||
llm.listConfigurableProviders = () => Promise.resolve(remoteFail<never>('configuration directory down'))
|
||||
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
|
||||
|
||||
await store.load()
|
||||
|
||||
expect(store.store.getSnapshot()).toMatchObject({
|
||||
status: 'error', error: 'configuration directory down',
|
||||
})
|
||||
})
|
||||
|
||||
it('lets the newest load win over a stale slow response', async () => {
|
||||
let release: (() => void) | undefined
|
||||
const gate = new Promise<void>((resolve) => { release = resolve })
|
||||
|
||||
@@ -29,7 +29,7 @@ async function bench(served?: string[]) {
|
||||
ctx.provide('locale', locale)
|
||||
const describeCredentials = vi.fn(() => Promise.resolve({ ok: false, error: { code: 'internal', message: 'no provider', details: {} } }))
|
||||
const models = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'm', result: { ok: true, value: { groups: [], failures: [] } },
|
||||
ok: true as const, value: { groups: [], failures: [] },
|
||||
}))
|
||||
const describeSettings = vi.fn(() => Promise.resolve(served === undefined
|
||||
? { ok: false, error: { code: 'internal', message: 'no provider', details: {} } }
|
||||
@@ -45,11 +45,11 @@ async function bench(served?: string[]) {
|
||||
}))
|
||||
const remote = new TestRemote(ctx, {
|
||||
credentials: { describe: describeCredentials, set: vi.fn() },
|
||||
session: { modelCatalog: models },
|
||||
settings: { describe: describeSettings },
|
||||
})
|
||||
ctx.provide('connection', {
|
||||
isLoopback: true,
|
||||
api: { llm: { models } },
|
||||
} as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return {
|
||||
@@ -66,7 +66,9 @@ function declareRoot(slots: SlotRegistry): () => void {
|
||||
|
||||
describe('ui-settings-plugins apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.credentials', 'settingsScope'])
|
||||
expect(inject).toEqual([
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.session', 'settingsScope',
|
||||
])
|
||||
})
|
||||
|
||||
it('registers one Plugins section and declares the tab and card slots', async () => {
|
||||
|
||||
@@ -65,12 +65,11 @@ function modelsApi(options: {
|
||||
error?: string
|
||||
} = {}) {
|
||||
const models = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'm-1' as never,
|
||||
result: options.error === undefined
|
||||
...(options.error === undefined
|
||||
? { ok: true as const, value: { groups: options.groups ?? [], failures: options.failures ?? [] } }
|
||||
: { ok: false as const, error: { code: 'internal_error' as never, message: options.error } },
|
||||
: { ok: false as const, error: { code: 'internal' as const, message: options.error, details: {} } }),
|
||||
}))
|
||||
return { api: { llm: { models } } as never, models }
|
||||
return { api: { modelCatalog: models } as never, models }
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
@@ -662,15 +661,14 @@ describe('SubagentModelSelectionCardController', () => {
|
||||
const refreshed = deferred<never>()
|
||||
const models = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
rpcId: 'catalog-1',
|
||||
result: { ok: true, value: {
|
||||
ok: true, value: {
|
||||
groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }],
|
||||
failures: [],
|
||||
} },
|
||||
},
|
||||
})
|
||||
.mockImplementationOnce(() => refreshed.promise)
|
||||
const controller = new SubagentModelSelectionCardController(
|
||||
host.scope, { llm: { models } } as never,
|
||||
host.scope, { modelCatalog: models } as never,
|
||||
)
|
||||
const face = controller.inject()
|
||||
const state = () => face.hooks.subagentModelSelectionCard.getSnapshot()
|
||||
@@ -684,8 +682,7 @@ describe('SubagentModelSelectionCardController', () => {
|
||||
candidates: [expect.objectContaining({ key: 'alpha\0fast', selected: true })],
|
||||
})
|
||||
refreshed.resolve({
|
||||
rpcId: 'catalog-2',
|
||||
result: { ok: true, value: { groups: [], failures: [] } },
|
||||
ok: true, value: { groups: [], failures: [] },
|
||||
} as never)
|
||||
await vi.waitFor(() => { expect(state().catalogStatus).toBe('ready') })
|
||||
expect(state().candidates).toEqual([
|
||||
@@ -738,21 +735,19 @@ describe('SubagentModelSelectionCardController', () => {
|
||||
})
|
||||
const models = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
rpcId: 'catalog-1',
|
||||
result: { ok: true, value: {
|
||||
ok: true, value: {
|
||||
groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }],
|
||||
failures: [],
|
||||
} },
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
rpcId: 'catalog-2',
|
||||
result: { ok: true, value: {
|
||||
ok: true, value: {
|
||||
groups: [{ id: 'beta', name: 'Beta', models: [{ id: 'new', name: 'New' }] }],
|
||||
failures: [],
|
||||
} },
|
||||
},
|
||||
})
|
||||
const controller = new SubagentModelSelectionCardController(
|
||||
host.scope, { llm: { models } } as never,
|
||||
host.scope, { modelCatalog: models } as never,
|
||||
)
|
||||
const state = () => controller.inject().hooks.subagentModelSelectionCard.getSnapshot()
|
||||
await vi.waitFor(() => { expect(state().candidates[0]?.provider).toBe('alpha') })
|
||||
@@ -807,7 +802,7 @@ describe('SubagentModelSelectionCardController', () => {
|
||||
|
||||
const pending = deferred<never>()
|
||||
const models = vi.fn(() => pending.promise)
|
||||
const controller = new SubagentModelSelectionCardController(host.scope, { llm: { models } } as never)
|
||||
const controller = new SubagentModelSelectionCardController(host.scope, { modelCatalog: models } as never)
|
||||
const face = controller.inject()
|
||||
face.toggleEnabled()
|
||||
face.retryCatalog()
|
||||
@@ -819,14 +814,13 @@ describe('SubagentModelSelectionCardController', () => {
|
||||
const pendingResolve = deferred<never>()
|
||||
const resolving = new SubagentModelSelectionCardController(
|
||||
host.scope,
|
||||
{ llm: { models: () => pendingResolve.promise } } as never,
|
||||
{ modelCatalog: () => pendingResolve.promise } as never,
|
||||
)
|
||||
const resolvingFace = resolving.inject()
|
||||
resolvingFace.toggleEnabled()
|
||||
resolving.dispose()
|
||||
pendingResolve.resolve({
|
||||
rpcId: 'late' as never,
|
||||
result: { ok: true, value: { groups: [], failures: [] } },
|
||||
ok: true, value: { groups: [], failures: [] },
|
||||
} as never)
|
||||
await pendingResolve.promise
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user