settings-scope.ts 1.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. /** Test double for the client settings-scope seam. */
  2. import { vi } from 'vitest'
  3. import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
  4. /** Handle over one stubbed scope: the scope, its write spy, and publication controls. */
  5. export interface StubSettingsScope<T> {
  6. /** The scope face handed to the service under test. */
  7. scope: SettingsScope<T>
  8. /** Spy behind `scope.set`; resolves immediately. */
  9. set: ReturnType<typeof vi.fn>
  10. /** @returns how many listeners are currently subscribed (disposal assertions). */
  11. listenerCount(): number
  12. /**
  13. * Replace part of the snapshot and notify subscribers, as a Host
  14. * acceptance would.
  15. * @param next - snapshot fields to replace.
  16. */
  17. publish(next: Partial<SettingsScopeSnapshot<T>>): void
  18. }
  19. /**
  20. * Build an in-memory settings scope for service specs: starts in the host
  21. * loading state, records writes, and lets the test publish Host acceptances.
  22. * @returns the stub handle.
  23. */
  24. export function stubSettingsScope<T>(): StubSettingsScope<T> {
  25. let snapshot: SettingsScopeSnapshot<T> = {
  26. status: 'loading', value: undefined, revision: undefined, writable: false, mode: 'host',
  27. }
  28. const listeners = new Set<() => void>()
  29. const set = vi.fn(() => Promise.resolve())
  30. return {
  31. scope: {
  32. getSnapshot: () => snapshot,
  33. subscribe: (listener) => {
  34. listeners.add(listener)
  35. return () => { listeners.delete(listener) }
  36. },
  37. set,
  38. },
  39. set,
  40. listenerCount: () => listeners.size,
  41. publish: (next) => {
  42. snapshot = { ...snapshot, ...next }
  43. for (const listener of [...listeners]) listener()
  44. },
  45. }
  46. }