settings.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. /** The shared `bash` settings section as the pwsh executor family resolves it. */
  2. import { describe, expect, it } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import type { Fiber } from '@deepseek-ai/cordis'
  5. import { SettingsProvider } from '@deepseek-ai/dsh-settings'
  6. import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
  7. import { SHELL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-shell'
  8. import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
  9. import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
  10. /** The smallest real provider: one in-memory document, always writable. */
  11. class MemorySettings extends SettingsProvider {
  12. doc: Record<string, unknown> = {}
  13. get writable(): boolean {
  14. return true
  15. }
  16. protected load(): Promise<Record<string, unknown>> {
  17. return Promise.resolve(structuredClone(this.doc))
  18. }
  19. protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
  20. this.doc = { ...this.doc, [ns]: structuredClone(section) }
  21. return Promise.resolve()
  22. }
  23. }
  24. async function boot(config: ConstructorParameters<typeof PwshLocalExecutor>[1] = {}): Promise<{
  25. ctx: Context
  26. settingsFiber: Fiber
  27. executorFiber: Fiber
  28. pwsh: PwshLocalExecutor
  29. }> {
  30. const ctx = new Context()
  31. await ctx.plugin(LocalSubprocessRuntime)
  32. const settingsFiber = ctx.plugin(MemorySettings)
  33. await settingsFiber.await()
  34. const executorFiber = ctx.plugin(PwshLocalExecutor, { timeoutMs: 60_000, ...config })
  35. await executorFiber.await()
  36. return { ctx, settingsFiber, executorFiber, pwsh: ctx.shell as PwshLocalExecutor }
  37. }
  38. describe('pwsh executor over the bash settings section', () => {
  39. it('resolves the user layer over the composition entry', async () => {
  40. const bench = await boot()
  41. expect(bench.pwsh.config.timeoutMs).toBe(60_000)
  42. await bench.ctx.settings.update(SHELL_SETTINGS_NAMESPACE, { timeoutMs: 5_000 })
  43. expect(bench.pwsh.config.timeoutMs).toBe(5_000)
  44. await bench.ctx.fiber.dispose()
  45. })
  46. it('refuses a stored value the constructor would have rejected', async () => {
  47. const bench = await boot()
  48. await expect(bench.ctx.settings.update(SHELL_SETTINGS_NAMESPACE, { timeoutMs: 0 }))
  49. .rejects.toThrow(/pwsh-local: timeoutMs must be a positive finite number/)
  50. expect(bench.pwsh.config.timeoutMs).toBe(60_000)
  51. await bench.ctx.fiber.dispose()
  52. })
  53. it('re-resolves the executable when the stored path changes', async () => {
  54. const bench = await boot({ pwshPath: '/opt/first/pwsh' })
  55. expect(bench.pwsh.pwshPath).toBe('/opt/first/pwsh')
  56. await bench.ctx.settings.update(SHELL_SETTINGS_NAMESPACE, { pwshPath: '/opt/second/pwsh' })
  57. expect(bench.pwsh.pwshPath).toBe('/opt/second/pwsh')
  58. await bench.ctx.fiber.dispose()
  59. })
  60. it('keeps the resolved executable when an unrelated field changes', async () => {
  61. const bench = await boot({ pwshPath: '/opt/first/pwsh' })
  62. const before = bench.pwsh.pwshPath
  63. await bench.ctx.settings.update(SHELL_SETTINGS_NAMESPACE, { timeoutMs: 5_000 })
  64. expect(bench.pwsh.pwshPath).toBe(before)
  65. await bench.ctx.fiber.dispose()
  66. })
  67. it('falls back to the composition entry when the settings provider detaches', async () => {
  68. const bench = await boot({ pwshPath: '/opt/first/pwsh' })
  69. await bench.ctx.settings.update(SHELL_SETTINGS_NAMESPACE, { timeoutMs: 5_000, pwshPath: '/opt/second/pwsh' })
  70. expect(bench.pwsh.config.timeoutMs).toBe(5_000)
  71. expect(bench.pwsh.pwshPath).toBe('/opt/second/pwsh')
  72. await bench.settingsFiber.dispose()
  73. expect(bench.pwsh.config.timeoutMs).toBe(60_000)
  74. expect(bench.pwsh.pwshPath).toBe('/opt/first/pwsh')
  75. await bench.ctx.fiber.dispose()
  76. })
  77. it('releases the namespace when the executor unloads', async () => {
  78. const bench = await boot()
  79. expect(bench.ctx.settings.describe().map(row => String(row.ns))).toContain('shell')
  80. await bench.executorFiber.dispose()
  81. expect(bench.ctx.settings.describe().map(row => String(row.ns))).not.toContain('shell')
  82. await bench.ctx.fiber.dispose()
  83. })
  84. })