catalog.client.spec.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import type { IApiClient, ModelCatalog } from '@deepseek-ai/dsh-client-connection/client'
  2. import { describe, expect, it, vi } from 'vitest'
  3. import { ModelCatalogDirectory } from '../src/client/catalog.ts'
  4. const catalog = (model: string): ModelCatalog => ({
  5. default: { provider: 'fixture', model },
  6. routableProviders: ['fixture'],
  7. groups: [{ id: 'fixture', name: 'Fixture', models: [{ id: model, name: model }] }],
  8. failures: [],
  9. })
  10. function directory(models: () => Promise<unknown>): ModelCatalogDirectory {
  11. return new ModelCatalogDirectory({ llm: { models } } as unknown as IApiClient)
  12. }
  13. describe('ModelCatalogDirectory', () => {
  14. it('shares one failing request, exposes the RPC error, and permits a retry', async () => {
  15. const models = vi.fn()
  16. .mockResolvedValueOnce({
  17. result: { ok: false, error: { code: 'unavailable', message: 'catalog offline', details: {} } },
  18. })
  19. .mockResolvedValueOnce({ result: { ok: true, value: catalog('recovered') } })
  20. const subject = directory(models)
  21. const first = subject.load()
  22. expect(subject.load()).toBe(first)
  23. await expect(first).rejects.toThrow('unavailable: catalog offline')
  24. expect(subject.store.getSnapshot()).toMatchObject({ status: 'error', error: 'unavailable: catalog offline' })
  25. await expect(subject.load()).resolves.toEqual(catalog('recovered'))
  26. expect(models).toHaveBeenCalledTimes(2)
  27. })
  28. it('does not publish a successful result from an invalidated generation', async () => {
  29. const first = Promise.withResolvers<unknown>()
  30. const second = Promise.withResolvers<unknown>()
  31. const models = vi.fn()
  32. .mockReturnValueOnce(first.promise)
  33. .mockReturnValueOnce(second.promise)
  34. const subject = directory(models)
  35. const stale = subject.load()
  36. subject.resetGeneration()
  37. first.resolve({ result: { ok: true, value: catalog('stale') } })
  38. await expect(stale).resolves.toEqual(catalog('stale'))
  39. expect(subject.store.getSnapshot()).toMatchObject({ value: null, status: 'loading' })
  40. second.resolve({ result: { ok: true, value: catalog('fresh') } })
  41. await vi.waitFor(() => {
  42. expect(subject.store.getSnapshot()).toMatchObject({ value: catalog('fresh'), status: 'ready' })
  43. })
  44. })
  45. it('does not publish a failure from an invalidated generation', async () => {
  46. const first = Promise.withResolvers<unknown>()
  47. const second = Promise.withResolvers<unknown>()
  48. const models = vi.fn()
  49. .mockReturnValueOnce(first.promise)
  50. .mockReturnValueOnce(second.promise)
  51. const subject = directory(models)
  52. const stale = subject.load()
  53. subject.resetGeneration()
  54. first.reject(new Error('stale failure'))
  55. await expect(stale).rejects.toThrow('stale failure')
  56. expect(subject.store.getSnapshot()).toMatchObject({ value: null, status: 'loading', error: null })
  57. second.resolve({ result: { ok: true, value: catalog('fresh') } })
  58. await vi.waitFor(() => {
  59. expect(subject.store.getSnapshot()).toMatchObject({ value: catalog('fresh'), status: 'ready' })
  60. })
  61. })
  62. it('contains refresh failures while retaining old data and clears it on a failed Host reset', async () => {
  63. const models = vi.fn()
  64. .mockResolvedValueOnce({ result: { ok: true, value: catalog('old') } })
  65. .mockRejectedValueOnce('refresh failed')
  66. .mockRejectedValueOnce(new Error('reset failed'))
  67. const subject = directory(models)
  68. await subject.load()
  69. subject.refresh()
  70. await vi.waitFor(() => {
  71. expect(subject.store.getSnapshot()).toEqual({
  72. value: catalog('old'), status: 'error', error: 'refresh failed',
  73. })
  74. })
  75. subject.resetGeneration()
  76. await vi.waitFor(() => {
  77. expect(subject.store.getSnapshot()).toEqual({
  78. value: null, status: 'error', error: 'reset failed',
  79. })
  80. })
  81. })
  82. })