config.snapshot.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { mkdtemp, rm } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { Writable } from 'node:stream'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import {
  7. NpmPackageManager,
  8. SdkProject,
  9. featureId,
  10. createBuiltinRegistry,
  11. type NestedMultiSelectValue,
  12. type PromptPort,
  13. } from '@deepseek-ai/dsh-helper'
  14. import type {
  15. ConfirmPromptRequest,
  16. MultiSelectPromptRequest,
  17. NestedMultiSelectRequest,
  18. PromptOutcome,
  19. SecretPromptRequest,
  20. SelectPromptRequest,
  21. TextPromptRequest,
  22. } from '../../helper/src/questions/prompt-port.ts'
  23. import { ConfigWorkflow } from '../src/config/config-workflow.ts'
  24. class RecordingPort implements PromptPort {
  25. readonly transcript: unknown[] = []
  26. readonly #answers: unknown[]
  27. constructor(answers: unknown[]) { this.#answers = [...answers] }
  28. answer<T>(record: unknown): Promise<PromptOutcome<T>> {
  29. this.transcript.push(record)
  30. return Promise.resolve({ status: 'answered', value: this.#answers.shift() as T })
  31. }
  32. text(request: TextPromptRequest): Promise<PromptOutcome<string>> {
  33. return this.answer({ kind: 'text', message: request.message })
  34. }
  35. secret(request: SecretPromptRequest): Promise<PromptOutcome<string>> {
  36. return this.answer({ kind: 'secret', message: request.message })
  37. }
  38. select<T>(request: SelectPromptRequest<T>): Promise<PromptOutcome<T>> {
  39. return this.answer({
  40. kind: 'select', message: request.message,
  41. options: request.options.map(option => ({ value: option.value, label: option.label })),
  42. })
  43. }
  44. multiselect<T>(request: MultiSelectPromptRequest<T>): Promise<PromptOutcome<readonly T[]>> {
  45. return this.answer({ kind: 'multiselect', message: request.message })
  46. }
  47. confirm(request: ConfirmPromptRequest): Promise<PromptOutcome<boolean>> {
  48. return this.answer({ kind: 'confirm', message: request.message, initialValue: request.initialValue })
  49. }
  50. nestedMultiselect<TValue, TChoice>(
  51. request: NestedMultiSelectRequest<TValue, TChoice>,
  52. ): Promise<PromptOutcome<readonly NestedMultiSelectValue<TValue, TChoice>[]>> {
  53. return this.answer({
  54. kind: 'nested-multiselect',
  55. message: request.message,
  56. showChanges: request.showChanges,
  57. options: request.options.map(option => ({
  58. value: option.value,
  59. label: option.label,
  60. required: option.required,
  61. default: option.default,
  62. disabled: option.disabled,
  63. warning: option.warning,
  64. choiceMode: option.choiceMode,
  65. choices: option.choices?.map(choice => ({
  66. value: choice.value,
  67. label: choice.label,
  68. default: choice.default,
  69. })),
  70. })),
  71. })
  72. }
  73. }
  74. const temporary: string[] = []
  75. afterEach(async () => {
  76. await Promise.all(temporary.splice(0).map(path => rm(path, { recursive: true, force: true })))
  77. })
  78. async function baseProject(): Promise<SdkProject> {
  79. const root = await mkdtemp(join(tmpdir(), 'dsh-config-snapshot-'))
  80. temporary.push(root)
  81. const request = {
  82. name: 'snapshot-agent',
  83. description: 'snapshot',
  84. runtime: { model: 'deepseek-v4-flash' },
  85. packageManager: new NpmPackageManager('10.0.0'),
  86. releaseVersion: '0.0.1',
  87. features: [
  88. { id: featureId('provider'), options: ['deepseek-official'], secrets: { apiKey: 'key' } },
  89. { id: featureId('bash'), options: ['local'] },
  90. { id: featureId('app'), options: ['acp'] },
  91. { id: featureId('persistence'), options: ['jsonl'] },
  92. ],
  93. localPlugins: [],
  94. }
  95. const project = SdkProject.create(root, request)
  96. const registry = createBuiltinRegistry(project.profile)
  97. const edit = project.edit(registry)
  98. for (const item of request.features) edit.installFeature(registry.get(item.id), item)
  99. return (await edit.commit()).project
  100. }
  101. describe('dsh-sdk config terminal contract', () => {
  102. it('pins the feature tree and Review & Apply output', async () => {
  103. const project = await baseProject()
  104. const registry = createBuiltinRegistry(project.profile)
  105. const port = new RecordingPort([
  106. [{ value: 'feature:todo', choices: [] }],
  107. true,
  108. ])
  109. let output = ''
  110. const stream = new Writable({ write(chunk, _encoding, callback) { output += String(chunk); callback() } })
  111. let installs = 0
  112. const result = await new ConfigWorkflow(port, stream, async () => { installs += 1 }).run(project, registry)
  113. expect({
  114. transcript: port.transcript,
  115. review: output,
  116. installs,
  117. committed: result.commit?.changes,
  118. }).toMatchSnapshot()
  119. })
  120. })