catalog.client.spec.ts 3.8 KB

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