mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml # .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md # .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md # packages/client/ui-models/README.i18n.yaml # packages/client/ui-models/README.md # packages/client/ui-models/README.zh.md # packages/client/ui-models/src/client/DeepSeekOnboardingDialog.module.css # packages/client/ui-models/src/client/DeepSeekOnboardingDialog.tsx # packages/client/ui-models/tests/onboarding-dialog.spec.tsx
181 lines
6.8 KiB
TypeScript
181 lines
6.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
/** First-run DeepSeek prompt behavior over the shared Models join. */
|
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import type { RpcResponse } from '@deepseek-ai/dsh-client-connection/client'
|
|
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
|
import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
|
import type { DeepSeekOnboardingDialogProps } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
|
import { ModelsSettingsStore } from '../src/client/store.ts'
|
|
import { en } from '../src/client/locales.ts'
|
|
|
|
afterEach(cleanup)
|
|
|
|
let nextRpc = 0
|
|
function ok<T>(value: T): RpcResponse<T> {
|
|
return { rpcId: `onboarding-${nextRpc++}` as never, result: { ok: true, value } }
|
|
}
|
|
function fail<T>(message: string): RpcResponse<T> {
|
|
return {
|
|
rpcId: `onboarding-${nextRpc++}` as never,
|
|
result: { ok: false, error: { code: 'internal', message, details: {} } },
|
|
}
|
|
}
|
|
|
|
function harness(options: {
|
|
provider?: boolean
|
|
providerSettingsNs?: string
|
|
providerActive?: boolean
|
|
settingsNamespace?: boolean
|
|
apiKeyEnv?: string | null
|
|
literal?: boolean
|
|
configured?: () => boolean
|
|
credential?: { source?: string; writable: boolean }
|
|
describeFailure?: string
|
|
settingsWritable?: boolean
|
|
providersReject?: boolean
|
|
} = {}) {
|
|
let fileConfigured = false
|
|
const configured = options.configured ?? (() => fileConfigured)
|
|
const face = {
|
|
llm: {
|
|
providers: () => {
|
|
if (options.providersReject === true) return Promise.reject(new Error('provider transport unavailable'))
|
|
return Promise.resolve(ok({
|
|
providers: options.provider === false
|
|
? []
|
|
: [{
|
|
provider: 'deepseek-official',
|
|
displayName: 'DeepSeek',
|
|
settingsNs: options.providerSettingsNs ?? 'llm-deepseek',
|
|
settingsPath: [],
|
|
active: options.providerActive ?? true,
|
|
}],
|
|
}))
|
|
},
|
|
},
|
|
settings: {
|
|
describe: () => Promise.resolve(ok({
|
|
writable: options.settingsWritable ?? true,
|
|
namespaces: options.settingsNamespace === false
|
|
? []
|
|
: [{
|
|
ns: 'llm-deepseek',
|
|
schema: {},
|
|
value: options.apiKeyEnv === null
|
|
? {}
|
|
: { apiKeyEnv: options.apiKeyEnv ?? 'DEEPSEEK_API_KEY' },
|
|
applies: 'live' as const,
|
|
secrets: [{ path: ['apiKey'], set: options.literal === true }],
|
|
revision: 0,
|
|
}],
|
|
})),
|
|
},
|
|
credentials: {
|
|
describe: () => options.describeFailure === undefined
|
|
? Promise.resolve(ok({
|
|
credentials: {
|
|
DEEPSEEK_API_KEY: {
|
|
configured: configured(),
|
|
...configured() && options.credential?.source !== undefined
|
|
? { source: options.credential.source }
|
|
: {},
|
|
writable: options.credential?.writable ?? true,
|
|
},
|
|
},
|
|
}))
|
|
: Promise.resolve(fail(options.describeFailure)),
|
|
},
|
|
}
|
|
const controller = new ModelsSettingsStore(face as never)
|
|
const openSection = vi.fn()
|
|
const complete = vi.fn()
|
|
const unusedHook = (() => { throw new Error('unused standard hook') }) as never
|
|
const props: DeepSeekOnboardingDialogProps = {
|
|
stepId: 'deepseek-official',
|
|
complete,
|
|
openSection,
|
|
useSessions: unusedHook,
|
|
useWorkspaces: unusedHook,
|
|
controller,
|
|
useSnapshot: bindSnapshotSelector(controller.store),
|
|
t: key => en[key],
|
|
}
|
|
return { controller, complete, openSection, props, configure: () => { fileConfigured = true } }
|
|
}
|
|
|
|
describe('DeepSeekOnboardingDialog', () => {
|
|
it('loads on first entry and presents one accessible route to Models', async () => {
|
|
const h = harness()
|
|
render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
expect(await screen.findByRole('region', { name: en.onboardingTitle })).toBeTruthy()
|
|
expect(screen.getByText(en.onboardingDescription)).toBeTruthy()
|
|
const action = screen.getByRole('button', { name: en.onboardingGoToSettings })
|
|
expect(action).toBeTruthy()
|
|
expect(document.activeElement).toBe(screen.getByRole('heading', { name: en.onboardingTitle }))
|
|
expect(screen.queryByRole('textbox')).toBeNull()
|
|
})
|
|
|
|
it('opens the Models section and dismisses the prompt', async () => {
|
|
const h = harness()
|
|
render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
await screen.findByRole('region')
|
|
fireEvent.click(screen.getByRole('button', { name: en.onboardingGoToSettings }))
|
|
expect(h.complete).toHaveBeenCalledOnce()
|
|
expect(h.openSection).toHaveBeenCalledWith('models')
|
|
})
|
|
|
|
it('allows configure-later dismissal without opening settings', async () => {
|
|
const h = harness()
|
|
render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
await screen.findByRole('region')
|
|
fireEvent.click(screen.getByRole('button', { name: en.onboardingLater }))
|
|
expect(h.complete).toHaveBeenCalledOnce()
|
|
expect(h.openSection).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not block the product when DeepSeek setup is unavailable', async () => {
|
|
for (const h of [
|
|
harness({ describeFailure: 'credentials service is absent' }),
|
|
harness({ credential: { writable: false } }),
|
|
harness({ settingsWritable: false }),
|
|
harness({ providersReject: true }),
|
|
harness({ providerActive: false }),
|
|
harness({ settingsNamespace: false }),
|
|
harness({ apiKeyEnv: null }),
|
|
]) {
|
|
const view = render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
await act(async () => { await h.controller.load() })
|
|
expect(screen.queryByRole('region')).toBeNull()
|
|
await waitFor(() => { expect(h.complete).toHaveBeenCalledOnce() })
|
|
expect(h.openSection).not.toHaveBeenCalled()
|
|
view.unmount()
|
|
}
|
|
})
|
|
|
|
it('skips an absent adapter and already-configured literal or environment credentials', async () => {
|
|
for (const h of [
|
|
harness({ provider: false }),
|
|
harness({ providerSettingsNs: '' }),
|
|
harness({ literal: true, describeFailure: 'credential seam absent' }),
|
|
harness({ configured: () => true, credential: { source: 'env', writable: false } }),
|
|
]) {
|
|
const view = render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
await act(async () => { await h.controller.load() })
|
|
expect(screen.queryByRole('region')).toBeNull()
|
|
await waitFor(() => { expect(h.complete).toHaveBeenCalledOnce() })
|
|
view.unmount()
|
|
}
|
|
})
|
|
|
|
it('closes when an external credential invalidation refreshes the shared join', async () => {
|
|
const h = harness()
|
|
render(<DeepSeekOnboardingDialog {...h.props} />)
|
|
await screen.findByRole('region')
|
|
h.configure()
|
|
await act(async () => { await h.controller.load() })
|
|
await waitFor(() => { expect(screen.queryByRole('region')).toBeNull() })
|
|
expect(h.complete).toHaveBeenCalledOnce()
|
|
})
|
|
})
|