store.client.spec.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /**
  2. * The store's write set: pages keyed by their first line, invalidated by a newer
  3. * file version; a view that survives a reset; one bucket per tab, dropped on
  4. * `forget` so a closed tab leaves nothing behind.
  5. */
  6. import { describe, expect, it } from 'vitest'
  7. import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client'
  8. import { createTextStore, fresh } from '../src/client/store.ts'
  9. import { page } from './fixtures.client.ts'
  10. import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
  11. const TAB_1 = 'tab-1' as TabId
  12. const TAB_2 = 'tab-2' as TabId
  13. const TAB_9 = 'tab-9' as TabId
  14. function pageValue(offset: number, lines: readonly string[], eof: boolean, version = 'v1') {
  15. const result = page(offset, lines, eof, version)
  16. if (!result.ok) throw new Error('fixture')
  17. return result.value
  18. }
  19. describe('text store', () => {
  20. it('clears an explicit implementation choice to resume automatic selection', () => {
  21. const instance = createTextStore().create()
  22. instance.actions.selected(TAB_1, 'custom')
  23. expect(instance.getSnapshot().byTab[TAB_1]?.rendererId).toBe('custom')
  24. instance.actions.selected(TAB_1, undefined)
  25. expect(instance.getSnapshot().byTab[TAB_1]?.rendererId).toBeUndefined()
  26. })
  27. it('starts empty and mints a bucket on the first write', () => {
  28. const instance = createTextStore().create()
  29. expect(instance.getSnapshot().byTab).toEqual({})
  30. instance.actions.loading(TAB_1)
  31. expect(instance.getSnapshot().byTab[TAB_1]).toEqual({ ...fresh(), loading: true })
  32. })
  33. it('keeps pages by their first line and reports the end of the file', () => {
  34. const instance = createTextStore().create()
  35. instance.actions.loading(TAB_1)
  36. instance.actions.page(TAB_1, pageValue(1, ['a', 'b'], false))
  37. instance.actions.page(TAB_1, pageValue(3, ['c'], true))
  38. const state = instance.getSnapshot().byTab[TAB_1]
  39. expect(state?.pages).toEqual({ 1: { text: 'a\nb', lines: 2 }, 3: { text: 'c', lines: 1 } })
  40. expect(state?.version).toBe('v1')
  41. expect(state?.eof).toBe(true)
  42. expect(state?.loading).toBe(false)
  43. })
  44. it('drops the pages of an older version when a newer page arrives', () => {
  45. const instance = createTextStore().create()
  46. instance.actions.page(TAB_1, pageValue(1, ['a'], false))
  47. instance.actions.page(TAB_1, pageValue(2, ['B'], true, 'v2'))
  48. expect(instance.getSnapshot().byTab[TAB_1]?.pages).toEqual({ 2: { text: 'B', lines: 1 } })
  49. expect(instance.getSnapshot().byTab[TAB_1]?.version).toBe('v2')
  50. })
  51. it('records a failure beside the pages already held, and the next page clears it', () => {
  52. const instance = createTextStore().create()
  53. instance.actions.page(TAB_1, pageValue(1, ['a'], false))
  54. const failure = { code: 'workspace-file/too-large', message: 'x', details: {} } as unknown as RemoteFailure
  55. instance.actions.failed(TAB_1, failure)
  56. expect(instance.getSnapshot().byTab[TAB_1]?.failure).toBe(failure)
  57. expect(instance.getSnapshot().byTab[TAB_1]?.pages).toEqual({ 1: { text: 'a', lines: 1 } })
  58. instance.actions.page(TAB_1, pageValue(2, ['b'], true))
  59. expect(instance.getSnapshot().byTab[TAB_1]?.failure).toBeUndefined()
  60. })
  61. it('resets the pages but keeps the view', () => {
  62. const instance = createTextStore().create()
  63. instance.actions.page(TAB_1, pageValue(1, ['a'], true))
  64. instance.actions.scrolled(TAB_1, 120)
  65. instance.actions.toggledWrap(TAB_1)
  66. instance.actions.navigated(TAB_1, 3)
  67. instance.actions.reset(TAB_1)
  68. expect(instance.getSnapshot().byTab[TAB_1]).toEqual({
  69. ...fresh(), scrollTop: 120, wrap: false, revision: 3,
  70. })
  71. })
  72. it('forgets one tab and keeps the rest', () => {
  73. const instance = createTextStore().create()
  74. instance.actions.toggledWrap(TAB_1)
  75. instance.actions.toggledWrap(TAB_2)
  76. instance.actions.forget(TAB_1)
  77. expect(Object.keys(instance.getSnapshot().byTab)).toEqual([TAB_2])
  78. // Forgetting an unknown tab is a no-op, not a fault: the abort listener may
  79. // fire for a tab that never wrote anything.
  80. instance.actions.forget(TAB_9)
  81. expect(Object.keys(instance.getSnapshot().byTab)).toEqual([TAB_2])
  82. })
  83. })
  84. describe('read observation baseline', () => {
  85. it('retains the first observation across overlapping reads and resets it only for a new generation', () => {
  86. const instance = createTextStore().create()
  87. instance.actions.loading(TAB_1, 'text-pages', 'observed-1')
  88. instance.actions.loading(TAB_1, 'text-pages', 'observed-2')
  89. expect(instance.getSnapshot().byTab[TAB_1]?.observedVersion).toBe('observed-1')
  90. instance.actions.page(TAB_1, pageValue(1, ['first'], false))
  91. instance.actions.loading(TAB_1, 'text-pages', 'observed-3')
  92. expect(instance.getSnapshot().byTab[TAB_1]?.observedVersion).toBe('observed-1')
  93. instance.actions.reset(TAB_1)
  94. instance.actions.loading(TAB_1, 'text-pages', 'observed-3')
  95. expect(instance.getSnapshot().byTab[TAB_1]?.observedVersion).toBe('observed-3')
  96. })
  97. })