| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- import { describe, expect, it } from 'vitest'
- import {
- featureId,
- createPackageManager,
- type NestedMultiSelectValue,
- type PromptPort,
- } from '@deepseek-ai/dsh-helper'
- import type {
- ConfirmPromptRequest,
- MultiSelectPromptRequest,
- NestedMultiSelectRequest,
- PromptOutcome,
- SecretPromptRequest,
- SelectPromptRequest,
- TextPromptRequest,
- } from '../../helper/src/questions/prompt-port.ts'
- import { parseCreateArgs } from '../src/args.ts'
- import { CreateWizard } from '../src/create-wizard.ts'
- import { CREATE_TEMPLATES, packageManagerTemplateModel } from '../src/templates/create-templates.ts'
- class RecordingPort implements PromptPort {
- readonly transcript: unknown[] = []
- readonly #answers: unknown[]
- constructor(answers: unknown[]) { this.#answers = [...answers] }
- answer<T>(record: unknown): Promise<PromptOutcome<T>> {
- this.transcript.push(record)
- return Promise.resolve({ status: 'answered', value: this.#answers.shift() as T })
- }
- text(request: TextPromptRequest): Promise<PromptOutcome<string>> {
- return this.answer({
- kind: 'text',
- message: request.message,
- defaultValue: request.defaultValue,
- initialValue: request.initialValue,
- })
- }
- secret(request: SecretPromptRequest): Promise<PromptOutcome<string>> {
- return this.answer({ kind: 'secret', message: request.message })
- }
- select<T>(request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> {
- return this.answer({
- kind: 'select', message: request.message, options: request.options.map(option => option.label),
- initialValue: request.initialValue,
- })
- }
- multiselect<T>(request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> {
- return this.answer({
- kind: 'multiselect', message: request.message, options: request.options.map(option => option.label),
- initialValues: request.initialValues,
- })
- }
- confirm(request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> {
- return this.answer({ kind: 'confirm', message: request.message, initialValue: request.initialValue })
- }
- nestedMultiselect<TValue, TChoice>(
- request: NestedMultiSelectRequest<TValue, TChoice>,
- ): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> {
- return this.answer({
- kind: 'nested-multiselect',
- message: request.message,
- options: request.options.map(option => ({
- label: option.label,
- required: option.required,
- default: option.default,
- choices: option.choices?.map(choice => choice.label),
- })),
- })
- }
- }
- describe.skipIf(process.platform === 'win32')('create-sdk terminal contract', () => {
- it('renders package-manager-specific setup commands', () => {
- const model = packageManagerTemplateModel(createPackageManager('yarn', '4.0.0'))
- expect(CREATE_TEMPLATES.installQuestion.render(model)).toBe('Run yarn install and then build the project?\n')
- expect(CREATE_TEMPLATES.setupFailure.render({
- directory: '/workspace/agent',
- error: 'offline',
- ...model,
- })).toContain('yarn install && yarn build')
- })
- it('pins the full unresolved question order and completion messages', async () => {
- const port = new RecordingPort([
- 'my-agent',
- 'my-agent',
- 'Snapshot agent',
- 'deepseek-official',
- 'secret-key',
- 'acp',
- [
- { value: featureId('persistence'), choices: ['jsonl'] },
- { value: featureId('hmr'), choices: [] },
- { value: featureId('web'), choices: ['exa'] },
- { value: featureId('workflow'), choices: [] },
- ],
- true,
- 'exa-key',
- 'none',
- 'npm',
- false,
- ])
- const resolved = await new CreateWizard({
- args: parseCreateArgs([]),
- port,
- cwd: '/workspace',
- releaseVersion: '0.0.1',
- userAgent: '',
- versionProbe: async () => '10.0.0',
- }).run()
- expect({
- prompts: port.transcript,
- result: {
- directory: resolved.directory,
- name: resolved.request.name,
- manager: resolved.request.packageManager.name,
- install: resolved.install,
- features: resolved.request.features.map(item => ({ id: item.id, options: item.options })),
- },
- messages: {
- created: CREATE_TEMPLATES.created.render({
- name: resolved.request.name,
- directory: resolved.directory,
- }),
- next: CREATE_TEMPLATES.nextSteps.render({
- directory: resolved.directory,
- setupRequired: false,
- ...packageManagerTemplateModel(resolved.request.packageManager),
- }),
- failure: CREATE_TEMPLATES.setupFailure.render({
- directory: resolved.directory,
- error: String(new Error('offline')),
- ...packageManagerTemplateModel(resolved.request.packageManager),
- }),
- },
- }).toMatchInlineSnapshot(`
- {
- "messages": {
- "created": "Created my-agent in /workspace/my-agent
- ",
- "failure": "Project files are ready, but setup failed: Error: offline
- Retry: cd /workspace/my-agent && npm install && npm run build
- ",
- "next": "Next: cd /workspace/my-agent && npm start
- ",
- },
- "prompts": [
- {
- "defaultValue": "my-agent",
- "initialValue": undefined,
- "kind": "text",
- "message": "Where should the project be created?",
- },
- {
- "defaultValue": "my-agent",
- "initialValue": undefined,
- "kind": "text",
- "message": "Package name",
- },
- {
- "defaultValue": "A DeepSeek Harness agent named my-agent",
- "initialValue": undefined,
- "kind": "text",
- "message": "Project description",
- },
- {
- "initialValue": "deepseek-official",
- "kind": "select",
- "message": "Model provider",
- "options": [
- "DeepSeek",
- "Custom endpoint (pi-ai)",
- ],
- },
- {
- "kind": "secret",
- "message": "DeepSeek API key",
- },
- {
- "initialValue": "acp",
- "kind": "select",
- "message": "Run interface",
- "options": [
- "ACP automation server",
- "Embedded context",
- ],
- },
- {
- "kind": "nested-multiselect",
- "message": "Select features",
- "options": [
- {
- "choices": [
- "Local executor",
- "Sandboxed executor",
- ],
- "default": true,
- "label": "Command execution",
- "required": true,
- },
- {
- "choices": [
- "JSONL files",
- "SQLite database",
- ],
- "default": true,
- "label": "Durable session storage",
- "required": true,
- },
- {
- "choices": undefined,
- "default": true,
- "label": "Hot-module reload",
- "required": false,
- },
- {
- "choices": undefined,
- "default": true,
- "label": "Read, write, and edit local files",
- "required": false,
- },
- {
- "choices": undefined,
- "default": true,
- "label": "Model-facing task tracking",
- "required": false,
- },
- {
- "choices": undefined,
- "default": true,
- "label": "Local skill discovery",
- "required": false,
- },
- {
- "choices": [
- "DeepSeek search",
- "Exa search",
- "Perplexity search",
- "Fetch only",
- ],
- "default": false,
- "label": "Web search and fetch tools",
- "required": false,
- },
- {
- "choices": [
- "Fresh child agent",
- "Fork parent history",
- ],
- "default": false,
- "label": "Delegate work to child agents",
- "required": false,
- },
- {
- "choices": undefined,
- "default": false,
- "label": "Scripted multi-agent workflows",
- "required": false,
- },
- {
- "choices": undefined,
- "default": false,
- "label": "Automatic context compaction",
- "required": false,
- },
- {
- "choices": [
- "Claude Code hooks",
- "Codex hooks",
- ],
- "default": false,
- "label": "Run Claude Code or Codex hooks",
- "required": false,
- },
- {
- "choices": undefined,
- "default": false,
- "label": "Loop-hygiene reminders",
- "required": false,
- },
- {
- "choices": undefined,
- "default": false,
- "label": "Tool timeout policy",
- "required": false,
- },
- ],
- },
- {
- "initialValue": true,
- "kind": "confirm",
- "message": "Add the recommended tool timeout policy for web search and fetch tools?",
- },
- {
- "kind": "secret",
- "message": "Exa API key",
- },
- {
- "initialValue": "none",
- "kind": "select",
- "message": "Local plugin",
- "options": [
- "No local plugin",
- "Cordis plugin",
- "Model-facing tool",
- ],
- },
- {
- "initialValue": "npm",
- "kind": "select",
- "message": "Package manager",
- "options": [
- "npm",
- "pnpm",
- "Yarn",
- ],
- },
- {
- "initialValue": true,
- "kind": "confirm",
- "message": "Run npm install and then build the project?",
- },
- ],
- "result": {
- "directory": "/workspace/my-agent",
- "features": [
- {
- "id": "provider",
- "options": [
- "deepseek-official",
- ],
- },
- {
- "id": "spine",
- "options": [
- "default",
- ],
- },
- {
- "id": "app",
- "options": [
- "acp",
- ],
- },
- {
- "id": "bash",
- "options": [
- "local",
- ],
- },
- {
- "id": "persistence",
- "options": [
- "jsonl",
- ],
- },
- {
- "id": "hmr",
- "options": [
- "default",
- ],
- },
- {
- "id": "web",
- "options": [
- "exa",
- ],
- },
- {
- "id": "workflow",
- "options": [
- "workerthread",
- ],
- },
- {
- "id": "timeout-policy",
- "options": [
- "default",
- ],
- },
- ],
- "install": false,
- "manager": "npm",
- "name": "my-agent",
- },
- }
- `)
- })
- })
|