changes-diff.client.spec.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. // @vitest-environment jsdom
  2. /** The comparison store: served and missing comparisons stay, failures retry, resets and disposal forget. */
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { SessionId } from '@deepseek-ai/dsh-session/types'
  5. import { changesDiffUrl, type ChangesDiff } from '../src/changes.ts'
  6. import { ChangesDiffStore } from '../src/client/changes-diff.ts'
  7. afterEach(() => { vi.unstubAllGlobals() })
  8. const SESSION = SessionId('viewed')
  9. const URL_ = changesDiffUrl(SESSION, 5, 1)
  10. const text: ChangesDiff = {
  11. kind: 'text', path: 'a.ts', display: 'a.ts', before: true, after: true, coarse: false,
  12. hunks: [{ oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-a', '+b'] }],
  13. }
  14. describe('ChangesDiffStore', () => {
  15. it('keeps served and missing comparisons, retries failures, and forgets on reset and disposal', async () => {
  16. const fetcher = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>()
  17. vi.stubGlobal('fetch', fetcher)
  18. const store = new ChangesDiffStore()
  19. fetcher.mockResolvedValueOnce(Response.json(text))
  20. await store.load(SESSION, 5, 1)
  21. expect(store.state.getSnapshot()[URL_]).toEqual(text)
  22. await store.load(SESSION, 5, 1)
  23. expect(fetcher).toHaveBeenCalledTimes(1)
  24. fetcher.mockResolvedValueOnce(new Response('gone', { status: 404 }))
  25. await store.load(SESSION, 5, 2)
  26. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 5, 2)]).toBe('missing')
  27. await store.load(SESSION, 5, 2)
  28. expect(fetcher).toHaveBeenCalledTimes(2)
  29. fetcher.mockResolvedValueOnce(new Response('boom', { status: 500 }))
  30. await store.load(SESSION, 5, 3)
  31. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 5, 3)]).toBe('error')
  32. fetcher.mockResolvedValueOnce(Response.json({ kind: 'text' }))
  33. await store.load(SESSION, 5, 3)
  34. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 5, 3)]).toBe('error')
  35. fetcher.mockRejectedValueOnce(new Error('offline'))
  36. await store.load(SESSION, 5, 3)
  37. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 5, 3)]).toBe('error')
  38. fetcher.mockResolvedValueOnce(Response.json({ kind: 'oversized', path: 'p', display: 'p' }))
  39. await store.load(SESSION, 5, 3)
  40. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 5, 3)]).toEqual({ kind: 'oversized', path: 'p', display: 'p' })
  41. store.reset()
  42. expect(store.state.getSnapshot()).toEqual({})
  43. let settle!: (response: Response) => void
  44. fetcher.mockReturnValueOnce(new Promise<Response>((resolve) => { settle = resolve }))
  45. const stale = store.load(SESSION, 5, 1)
  46. expect(store.state.getSnapshot()[URL_]).toBe('loading')
  47. store.reset()
  48. settle(Response.json(text))
  49. await stale
  50. expect(store.state.getSnapshot()[URL_]).toBeUndefined()
  51. fetcher.mockReturnValueOnce(new Promise<Response>((resolve) => { settle = resolve }))
  52. const late = store.load(SESSION, 5, 1)
  53. const disposal = store.dispose()
  54. settle(Response.json(text))
  55. await Promise.all([late, disposal])
  56. expect(store.state.getSnapshot()[URL_]).toBe('loading')
  57. await store.load(SESSION, 6, 0)
  58. expect(store.state.getSnapshot()[changesDiffUrl(SESSION, 6, 0)]).toBeUndefined()
  59. })
  60. })