create.snapshot.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. featureId,
  4. createPackageManager,
  5. type NestedMultiSelectValue,
  6. type PromptPort,
  7. } from '@deepseek-ai/dsh-helper'
  8. import type {
  9. ConfirmPromptRequest,
  10. MultiSelectPromptRequest,
  11. NestedMultiSelectRequest,
  12. PromptOutcome,
  13. SecretPromptRequest,
  14. SelectPromptRequest,
  15. TextPromptRequest,
  16. } from '../../helper/src/questions/prompt-port.ts'
  17. import { parseCreateArgs } from '../src/args.ts'
  18. import { CreateWizard } from '../src/create-wizard.ts'
  19. import { CREATE_TEMPLATES, packageManagerTemplateModel } from '../src/templates/create-templates.ts'
  20. class RecordingPort implements PromptPort {
  21. readonly transcript: unknown[] = []
  22. readonly #answers: unknown[]
  23. constructor(answers: unknown[]) { this.#answers = [...answers] }
  24. answer<T>(record: unknown): Promise<PromptOutcome<T>> {
  25. this.transcript.push(record)
  26. return Promise.resolve({ status: 'answered', value: this.#answers.shift() as T })
  27. }
  28. text(request: TextPromptRequest): Promise<PromptOutcome<string>> {
  29. return this.answer({
  30. kind: 'text',
  31. message: request.message,
  32. defaultValue: request.defaultValue,
  33. initialValue: request.initialValue,
  34. })
  35. }
  36. secret(request: SecretPromptRequest): Promise<PromptOutcome<string>> {
  37. return this.answer({ kind: 'secret', message: request.message })
  38. }
  39. select<T>(request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> {
  40. return this.answer({
  41. kind: 'select', message: request.message, options: request.options.map(option => option.label),
  42. initialValue: request.initialValue,
  43. })
  44. }
  45. multiselect<T>(request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> {
  46. return this.answer({
  47. kind: 'multiselect', message: request.message, options: request.options.map(option => option.label),
  48. initialValues: request.initialValues,
  49. })
  50. }
  51. confirm(request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> {
  52. return this.answer({ kind: 'confirm', message: request.message, initialValue: request.initialValue })
  53. }
  54. nestedMultiselect<TValue, TChoice>(
  55. request: NestedMultiSelectRequest<TValue, TChoice>,
  56. ): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> {
  57. return this.answer({
  58. kind: 'nested-multiselect',
  59. message: request.message,
  60. options: request.options.map(option => ({
  61. label: option.label,
  62. required: option.required,
  63. default: option.default,
  64. choices: option.choices?.map(choice => choice.label),
  65. })),
  66. })
  67. }
  68. }
  69. describe.skipIf(process.platform === 'win32')('create-sdk terminal contract', () => {
  70. it('renders package-manager-specific setup commands', () => {
  71. const model = packageManagerTemplateModel(createPackageManager('yarn', '4.0.0'))
  72. expect(CREATE_TEMPLATES.installQuestion.render(model)).toBe('Run yarn install and then build the project?\n')
  73. expect(CREATE_TEMPLATES.setupFailure.render({
  74. directory: '/workspace/agent',
  75. error: 'offline',
  76. ...model,
  77. })).toContain('yarn install && yarn build')
  78. })
  79. it('pins the full unresolved question order and completion messages', async () => {
  80. const port = new RecordingPort([
  81. 'my-agent',
  82. 'my-agent',
  83. 'Snapshot agent',
  84. 'deepseek-official',
  85. 'secret-key',
  86. 'acp',
  87. [
  88. { value: featureId('persistence'), choices: ['jsonl'] },
  89. { value: featureId('hmr'), choices: [] },
  90. { value: featureId('web'), choices: ['exa'] },
  91. { value: featureId('workflow'), choices: [] },
  92. ],
  93. true,
  94. 'exa-key',
  95. 'none',
  96. 'npm',
  97. false,
  98. ])
  99. const resolved = await new CreateWizard({
  100. args: parseCreateArgs([]),
  101. port,
  102. cwd: '/workspace',
  103. releaseVersion: '0.0.1',
  104. userAgent: '',
  105. versionProbe: async () => '10.0.0',
  106. }).run()
  107. expect({
  108. prompts: port.transcript,
  109. result: {
  110. directory: resolved.directory,
  111. name: resolved.request.name,
  112. manager: resolved.request.packageManager.name,
  113. install: resolved.install,
  114. features: resolved.request.features.map(item => ({ id: item.id, options: item.options })),
  115. },
  116. messages: {
  117. created: CREATE_TEMPLATES.created.render({
  118. name: resolved.request.name,
  119. directory: resolved.directory,
  120. }),
  121. next: CREATE_TEMPLATES.nextSteps.render({
  122. directory: resolved.directory,
  123. setupRequired: false,
  124. ...packageManagerTemplateModel(resolved.request.packageManager),
  125. }),
  126. failure: CREATE_TEMPLATES.setupFailure.render({
  127. directory: resolved.directory,
  128. error: String(new Error('offline')),
  129. ...packageManagerTemplateModel(resolved.request.packageManager),
  130. }),
  131. },
  132. }).toMatchInlineSnapshot(`
  133. {
  134. "messages": {
  135. "created": "Created my-agent in /workspace/my-agent
  136. ",
  137. "failure": "Project files are ready, but setup failed: Error: offline
  138. Retry: cd /workspace/my-agent && npm install && npm run build
  139. ",
  140. "next": "Next: cd /workspace/my-agent && npm start
  141. ",
  142. },
  143. "prompts": [
  144. {
  145. "defaultValue": "my-agent",
  146. "initialValue": undefined,
  147. "kind": "text",
  148. "message": "Where should the project be created?",
  149. },
  150. {
  151. "defaultValue": "my-agent",
  152. "initialValue": undefined,
  153. "kind": "text",
  154. "message": "Package name",
  155. },
  156. {
  157. "defaultValue": "A DeepSeek Harness agent named my-agent",
  158. "initialValue": undefined,
  159. "kind": "text",
  160. "message": "Project description",
  161. },
  162. {
  163. "initialValue": "deepseek-official",
  164. "kind": "select",
  165. "message": "Model provider",
  166. "options": [
  167. "DeepSeek",
  168. "Custom endpoint (pi-ai)",
  169. ],
  170. },
  171. {
  172. "kind": "secret",
  173. "message": "DeepSeek API key",
  174. },
  175. {
  176. "initialValue": "acp",
  177. "kind": "select",
  178. "message": "Run interface",
  179. "options": [
  180. "ACP automation server",
  181. "Embedded context",
  182. ],
  183. },
  184. {
  185. "kind": "nested-multiselect",
  186. "message": "Select features",
  187. "options": [
  188. {
  189. "choices": [
  190. "Local executor",
  191. "Sandboxed executor",
  192. ],
  193. "default": true,
  194. "label": "Command execution",
  195. "required": true,
  196. },
  197. {
  198. "choices": [
  199. "JSONL files",
  200. "SQLite database",
  201. ],
  202. "default": true,
  203. "label": "Durable session storage",
  204. "required": true,
  205. },
  206. {
  207. "choices": undefined,
  208. "default": true,
  209. "label": "Hot-module reload",
  210. "required": false,
  211. },
  212. {
  213. "choices": undefined,
  214. "default": true,
  215. "label": "Read, write, and edit local files",
  216. "required": false,
  217. },
  218. {
  219. "choices": undefined,
  220. "default": true,
  221. "label": "Model-facing task tracking",
  222. "required": false,
  223. },
  224. {
  225. "choices": undefined,
  226. "default": true,
  227. "label": "Local skill discovery",
  228. "required": false,
  229. },
  230. {
  231. "choices": [
  232. "DeepSeek search",
  233. "Exa search",
  234. "Perplexity search",
  235. "Fetch only",
  236. ],
  237. "default": false,
  238. "label": "Web search and fetch tools",
  239. "required": false,
  240. },
  241. {
  242. "choices": [
  243. "Fresh child agent",
  244. "Fork parent history",
  245. ],
  246. "default": false,
  247. "label": "Delegate work to child agents",
  248. "required": false,
  249. },
  250. {
  251. "choices": undefined,
  252. "default": false,
  253. "label": "Scripted multi-agent workflows",
  254. "required": false,
  255. },
  256. {
  257. "choices": undefined,
  258. "default": false,
  259. "label": "Automatic context compaction",
  260. "required": false,
  261. },
  262. {
  263. "choices": [
  264. "Claude Code hooks",
  265. "Codex hooks",
  266. ],
  267. "default": false,
  268. "label": "Run Claude Code or Codex hooks",
  269. "required": false,
  270. },
  271. {
  272. "choices": undefined,
  273. "default": false,
  274. "label": "Loop-hygiene reminders",
  275. "required": false,
  276. },
  277. {
  278. "choices": undefined,
  279. "default": false,
  280. "label": "Tool timeout policy",
  281. "required": false,
  282. },
  283. ],
  284. },
  285. {
  286. "initialValue": true,
  287. "kind": "confirm",
  288. "message": "Add the recommended tool timeout policy for web search and fetch tools?",
  289. },
  290. {
  291. "kind": "secret",
  292. "message": "Exa API key",
  293. },
  294. {
  295. "initialValue": "none",
  296. "kind": "select",
  297. "message": "Local plugin",
  298. "options": [
  299. "No local plugin",
  300. "Cordis plugin",
  301. "Model-facing tool",
  302. ],
  303. },
  304. {
  305. "initialValue": "npm",
  306. "kind": "select",
  307. "message": "Package manager",
  308. "options": [
  309. "npm",
  310. "pnpm",
  311. "Yarn",
  312. ],
  313. },
  314. {
  315. "initialValue": true,
  316. "kind": "confirm",
  317. "message": "Run npm install and then build the project?",
  318. },
  319. ],
  320. "result": {
  321. "directory": "/workspace/my-agent",
  322. "features": [
  323. {
  324. "id": "provider",
  325. "options": [
  326. "deepseek-official",
  327. ],
  328. },
  329. {
  330. "id": "spine",
  331. "options": [
  332. "default",
  333. ],
  334. },
  335. {
  336. "id": "app",
  337. "options": [
  338. "acp",
  339. ],
  340. },
  341. {
  342. "id": "bash",
  343. "options": [
  344. "local",
  345. ],
  346. },
  347. {
  348. "id": "persistence",
  349. "options": [
  350. "jsonl",
  351. ],
  352. },
  353. {
  354. "id": "hmr",
  355. "options": [
  356. "default",
  357. ],
  358. },
  359. {
  360. "id": "web",
  361. "options": [
  362. "exa",
  363. ],
  364. },
  365. {
  366. "id": "workflow",
  367. "options": [
  368. "workerthread",
  369. ],
  370. },
  371. {
  372. "id": "timeout-policy",
  373. "options": [
  374. "default",
  375. ],
  376. },
  377. ],
  378. "install": false,
  379. "manager": "npm",
  380. "name": "my-agent",
  381. },
  382. }
  383. `)
  384. })
  385. })