settings-mirror.client.spec.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { describe, expect, it, vi } from 'vitest'
  2. import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
  3. import { SettingsDescribeMirror, type SettingsDescribeView } from '../src/client/settings-mirror.ts'
  4. let rpc = 0
  5. function ok<T>(value: T): RpcResponse<T> {
  6. return { rpcId: `mirror-${rpc++}` as never, result: { ok: true, value } }
  7. }
  8. function rejected<T>(message: string): RpcResponse<T> {
  9. return {
  10. rpcId: `mirror-${rpc++}` as never,
  11. result: {
  12. ok: false,
  13. error: { code: 'settings-rejected', message, details: { ns: 'theme' } },
  14. },
  15. }
  16. }
  17. function view(ns: string, revision = 0): SettingsNamespaceView {
  18. return { ns, schema: {}, value: { field: ns }, applies: 'live', secrets: [], revision }
  19. }
  20. function described(namespaces: SettingsNamespaceView[]): RpcResponse<SettingsDescribeView> {
  21. return ok({ writable: true, hasDocument: true, namespaces })
  22. }
  23. function deferred<T>() {
  24. let resolve!: (value: T) => void
  25. const promise = new Promise<T>((res) => { resolve = res })
  26. return { promise, resolve }
  27. }
  28. describe('SettingsDescribeMirror', () => {
  29. it('folds loads before the wire read into it, and mid-flight loads into one rerun', async () => {
  30. const gate = deferred<RpcResponse<SettingsDescribeView>>()
  31. const describeCall = vi.fn()
  32. .mockReturnValueOnce(gate.promise)
  33. .mockResolvedValue(described([view('theme', 1)]))
  34. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  35. const first = mirror.load()
  36. // Issued before the wire read goes out: covered by that read, no rerun.
  37. const early = mirror.load()
  38. await Promise.resolve()
  39. expect(describeCall).toHaveBeenCalledTimes(1)
  40. // Issued while the read is on the wire: exactly one rerun, however many.
  41. const mid = mirror.load()
  42. const midToo = mirror.load()
  43. gate.resolve(described([view('theme', 0)]))
  44. await Promise.all([first, early, mid, midToo])
  45. expect(describeCall).toHaveBeenCalledTimes(2)
  46. expect(mirror.getSnapshot().status).toBe('ready')
  47. expect(mirror.namespace('theme')?.revision).toBe(1)
  48. })
  49. it('keeps the last good view when a later refresh fails, recording the failure', async () => {
  50. const describeCall = vi.fn()
  51. .mockResolvedValueOnce(described([view('theme', 2)]))
  52. .mockRejectedValueOnce(new Error('host gone'))
  53. .mockResolvedValueOnce(rejected('busy'))
  54. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  55. await mirror.load()
  56. expect(mirror.getSnapshot()).toMatchObject({ status: 'ready', error: null })
  57. await mirror.load()
  58. expect(mirror.getSnapshot()).toMatchObject({ status: 'ready', error: 'host gone' })
  59. expect(mirror.namespace('theme')?.revision).toBe(2)
  60. await mirror.load()
  61. expect(mirror.getSnapshot()).toMatchObject({ status: 'ready', error: 'busy' })
  62. expect(mirror.getSnapshot().view?.namespaces).toHaveLength(1)
  63. })
  64. it('returns to idle after a first read that never succeeded, so ensure retries', async () => {
  65. const describeCall = vi.fn()
  66. .mockRejectedValueOnce(new Error('offline'))
  67. .mockResolvedValueOnce(described([view('theme', 1)]))
  68. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  69. await mirror.ensure()
  70. expect(mirror.getSnapshot()).toMatchObject({ status: 'idle', view: undefined, error: 'offline' })
  71. await mirror.ensure()
  72. expect(mirror.getSnapshot()).toMatchObject({ status: 'ready', error: null })
  73. expect(describeCall).toHaveBeenCalledTimes(2)
  74. })
  75. it('treats ensure as a no-op once ready', async () => {
  76. const describeCall = vi.fn().mockResolvedValue(described([view('theme', 1)]))
  77. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  78. await mirror.ensure()
  79. await mirror.ensure()
  80. await mirror.ensure()
  81. expect(describeCall).toHaveBeenCalledTimes(1)
  82. })
  83. it('memory persistence is terminally unavailable and never touches the wire', async () => {
  84. const describeCall = vi.fn()
  85. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never, 'memory')
  86. await mirror.ensure()
  87. await mirror.load()
  88. expect(mirror.getSnapshot()).toEqual({ status: 'unavailable', view: undefined, error: null })
  89. expect(describeCall).not.toHaveBeenCalled()
  90. })
  91. it('acceptView folds one write answer into the held view without a wire read', async () => {
  92. const describeCall = vi.fn()
  93. .mockResolvedValueOnce(described([view('theme', 1), view('locale', 4)]))
  94. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  95. await mirror.load()
  96. const seen: number[] = []
  97. mirror.subscribe(() => { seen.push(mirror.namespace('theme')?.revision ?? -1) })
  98. mirror.acceptView(view('theme', 9))
  99. expect(mirror.namespace('theme')?.revision).toBe(9)
  100. expect(mirror.namespace('locale')?.revision).toBe(4)
  101. expect(seen).toEqual([9])
  102. expect(describeCall).toHaveBeenCalledTimes(1)
  103. })
  104. it('acceptView before any answer is a no-op instead of inventing a document', () => {
  105. const describeCall = vi.fn()
  106. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  107. mirror.acceptView(view('theme', 1))
  108. expect(mirror.getSnapshot()).toEqual({ status: 'idle', view: undefined, error: null })
  109. })
  110. it('acceptView appends a namespace the held view has not seen yet', async () => {
  111. const describeCall = vi.fn().mockResolvedValueOnce(described([view('theme', 1)]))
  112. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  113. await mirror.load()
  114. mirror.acceptView(view('fresh-ns', 0))
  115. expect(mirror.namespace('fresh-ns')).toBeDefined()
  116. expect(mirror.getSnapshot().view?.namespaces).toHaveLength(2)
  117. })
  118. it('never loses a load landing between a run settling and its slot clearing', async () => {
  119. // Regression: with the in-flight slot cleared by a promise .finally(),
  120. // a load() in the one-microtask gap after the rerun check marked a rerun
  121. // nobody read, and that refresh never reached the wire.
  122. const describeCall = vi.fn().mockResolvedValue(described([view('theme', 1)]))
  123. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  124. void mirror.load()
  125. await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) })
  126. void mirror.load()
  127. await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(2) })
  128. void mirror.load()
  129. await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(3) })
  130. })
  131. it('starts no second run for a load issued inside the loading publish', async () => {
  132. const gate = deferred<RpcResponse<SettingsDescribeView>>()
  133. const describeCall = vi.fn().mockReturnValue(gate.promise)
  134. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  135. let reentered = false
  136. const unsubscribe = mirror.subscribe(() => {
  137. if (reentered) return
  138. reentered = true
  139. void mirror.load()
  140. })
  141. const loading = mirror.load()
  142. await Promise.resolve()
  143. expect(describeCall).toHaveBeenCalledTimes(1)
  144. gate.resolve(described([view('theme', 1)]))
  145. await loading
  146. unsubscribe()
  147. // The reentrant load folded into the first run rather than racing it.
  148. expect(describeCall).toHaveBeenCalledTimes(1)
  149. expect(mirror.getSnapshot().status).toBe('ready')
  150. })
  151. it('lets the first read cover a write folded inside the loading publish', async () => {
  152. const describeCall = vi.fn().mockResolvedValue(described([view('theme', 2)]))
  153. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  154. const unsubscribe = mirror.subscribe(() => {
  155. unsubscribe()
  156. mirror.acceptView(view('theme', 2))
  157. })
  158. await mirror.load()
  159. expect(describeCall).toHaveBeenCalledTimes(1)
  160. expect(mirror.getSnapshot().status).toBe('ready')
  161. expect(mirror.namespace('theme')?.revision).toBe(2)
  162. })
  163. it('re-reads after a folded write invalidates an in-flight document', async () => {
  164. const slow = deferred<RpcResponse<SettingsDescribeView>>()
  165. const describeCall = vi.fn()
  166. .mockResolvedValueOnce(described([view('theme', 4), view('locale', 1)]))
  167. .mockReturnValueOnce(slow.promise)
  168. .mockResolvedValueOnce(described([view('theme', 5), view('locale', 2)]))
  169. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  170. await mirror.load()
  171. expect(describeCall).toHaveBeenCalledTimes(1)
  172. const stale = mirror.load()
  173. await Promise.resolve()
  174. mirror.acceptView(view('theme', 5))
  175. slow.resolve(described([view('theme', 4), view('locale', 2)]))
  176. await stale
  177. expect(describeCall).toHaveBeenCalledTimes(3)
  178. expect(mirror.namespace('theme')?.revision).toBe(5)
  179. expect(mirror.namespace('locale')?.revision).toBe(2)
  180. })
  181. it('re-reads after a pre-answer write invalidates the in-flight document', async () => {
  182. const slow = deferred<RpcResponse<SettingsDescribeView>>()
  183. const describeCall = vi.fn()
  184. .mockReturnValueOnce(slow.promise)
  185. .mockResolvedValueOnce(described([view('theme', 2)]))
  186. const mirror = new SettingsDescribeMirror({ settings: { describe: describeCall } } as never)
  187. const loading = mirror.load()
  188. await Promise.resolve()
  189. mirror.acceptView(view('theme', 2))
  190. slow.resolve(described([view('theme', 1)]))
  191. await loading
  192. expect(describeCall).toHaveBeenCalledTimes(2)
  193. expect(mirror.namespace('theme')?.revision).toBe(2)
  194. })
  195. })