config-ledger.client.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. /** The projection of the plugins carrying configuration: what it reads from the ledgers and when it moves. */
  2. import { describe, expect, it, vi } from 'vitest'
  3. import { configLedgerSource, rowConfigKey } from '../src/client/config-ledger.ts'
  4. interface Registration {
  5. options: { id?: string; key?: string; label?: string | (() => string) }
  6. }
  7. /** A slot registry and locale stand-in: ledgers by slot, versions that move on registration, and a locale revision. */
  8. function bench() {
  9. const entries: Record<string, Registration[]> = { 'plugins.item': [], 'plugins.bundle.config': [], 'plugins.row.config': [] }
  10. const versions: Record<string, number> = { 'plugins.item': 0, 'plugins.bundle.config': 0, 'plugins.row.config': 0 }
  11. const listeners = new Map<string, Set<() => void>>()
  12. const localeListeners = new Set<() => void>()
  13. let revision = 0
  14. const ctx = {
  15. slots: {
  16. entries: (name: string) => entries[name] ?? [],
  17. getVersion: (name: string) => versions[name] ?? 0,
  18. subscribe: (name: string, listener: () => void) => {
  19. const set = listeners.get(name) ?? new Set<() => void>()
  20. set.add(listener)
  21. listeners.set(name, set)
  22. return () => { set.delete(listener) }
  23. },
  24. },
  25. locale: {
  26. getSnapshot: () => ({ revision }),
  27. subscribe: (listener: () => void) => {
  28. localeListeners.add(listener)
  29. return () => { localeListeners.delete(listener) }
  30. },
  31. },
  32. }
  33. return {
  34. ctx: ctx as never,
  35. register: (name: string, options: Registration['options']): void => {
  36. entries[name]?.push({ options })
  37. versions[name] = (versions[name] ?? 0) + 1
  38. for (const listener of listeners.get(name) ?? []) listener()
  39. },
  40. setLocale: (): void => {
  41. revision += 1
  42. for (const listener of localeListeners) listener()
  43. },
  44. listenerCount: (): number => [...listeners.values()].reduce((count, set) => count + set.size, 0) + localeListeners.size,
  45. }
  46. }
  47. describe('configLedgerSource', () => {
  48. it('projects the official items in ledger order with locale-following labels, and the bundle and row keys', () => {
  49. const b = bench()
  50. const source = configLedgerSource(b.ctx)
  51. expect(source.getSnapshot()).toEqual({ items: [], bundles: new Set(), rows: new Set() })
  52. let title = 'Shell'
  53. b.register('plugins.item', { id: 'bash', label: () => title })
  54. b.register('plugins.item', { id: 'loop', label: 'Agent loop' })
  55. b.register('plugins.bundle.config', { key: 'dsh-x' })
  56. b.register('plugins.row.config', { key: rowConfigKey('dsh-x', 'row') })
  57. expect(source.getSnapshot()).toEqual({
  58. items: [{ id: 'bash', label: 'Shell' }, { id: 'loop', label: 'Agent loop' }],
  59. bundles: new Set(['dsh-x']),
  60. rows: new Set(['dsh-x#row']),
  61. })
  62. // A label thunk is re-read when the locale moves, not before.
  63. title = '终端'
  64. expect(source.getSnapshot().items[0]?.label).toBe('Shell')
  65. b.setLocale()
  66. expect(source.getSnapshot().items[0]?.label).toBe('终端')
  67. })
  68. it('lists an entry without a label under an empty title and skips a keyed entry without a key', () => {
  69. const b = bench()
  70. const source = configLedgerSource(b.ctx)
  71. b.register('plugins.item', { id: 'bare' })
  72. b.register('plugins.bundle.config', {})
  73. expect(source.getSnapshot()).toEqual({ items: [{ id: 'bare', label: '' }], bundles: new Set(), rows: new Set() })
  74. })
  75. it('keeps its snapshot until a ledger or the locale moves, and follows every source while subscribed', () => {
  76. const b = bench()
  77. const source = configLedgerSource(b.ctx)
  78. const first = source.getSnapshot()
  79. expect(source.getSnapshot()).toBe(first)
  80. const listener = vi.fn()
  81. const off = source.subscribe(listener)
  82. b.register('plugins.item', { id: 'bash', label: 'Shell' })
  83. expect(listener).toHaveBeenCalledTimes(1)
  84. expect(source.getSnapshot()).not.toBe(first)
  85. b.setLocale()
  86. expect(listener).toHaveBeenCalledTimes(2)
  87. off()
  88. expect(b.listenerCount()).toBe(0)
  89. b.register('plugins.item', { id: 'loop', label: 'Loop' })
  90. expect(listener).toHaveBeenCalledTimes(2)
  91. })
  92. })