musora-content-services 2.167.2 → 2.167.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  2. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  3. package/CHANGELOG.md +8 -0
  4. package/package.json +2 -1
  5. package/src/contentTypeConfig.js +1 -0
  6. package/src/lib/sanity/query.ts +5 -0
  7. package/src/services/config.js +15 -0
  8. package/src/services/content/artist.ts +1 -3
  9. package/src/services/content/counts.ts +47 -0
  10. package/src/services/content/genre.ts +1 -3
  11. package/src/services/content/instructor.ts +1 -3
  12. package/src/services/userActivity.js +2 -0
  13. package/src/version-info.js +1 -0
  14. package/.claude/settings.local.json +0 -53
  15. package/test/initializeTests.js +0 -60
  16. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  17. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  18. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  19. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  20. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  21. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  22. package/test/integration/content-progress/getters.test.ts +0 -334
  23. package/test/integration/content-progress/helpers.test.ts +0 -263
  24. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  25. package/test/integration/forums.test.ts +0 -209
  26. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  27. package/test/integration/initializeTestDB.ts +0 -80
  28. package/test/integration/learning-paths.test.ts +0 -854
  29. package/test/integration/notifications.test.ts +0 -178
  30. package/test/integration/progress-row/content-card.test.ts +0 -283
  31. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  32. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  33. package/test/integration/sync/fetch.test.ts +0 -224
  34. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  35. package/test/integration/sync/repositories/practices.test.ts +0 -179
  36. package/test/integration/sync/repositories/progress.test.ts +0 -245
  37. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  38. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  39. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  40. package/test/integration/sync/store/store-idb.test.ts +0 -180
  41. package/test/integration/sync/store/store.test.ts +0 -361
  42. package/test/integration/user/permissions.test.js +0 -20
  43. package/test/integration/userActivity.test.ts +0 -102
  44. package/test/live/README.md +0 -29
  45. package/test/live/content.test.js +0 -116
  46. package/test/localStorageMock.js +0 -21
  47. package/test/log.js +0 -5
  48. package/test/mockData/award-definitions.js +0 -171
  49. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  50. package/test/mockData/mockData_progress_content.json +0 -182
  51. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  52. package/test/mockData/mockData_user_practices.json +0 -9
  53. package/test/setupConsole.js +0 -6
  54. package/test/setupNetworkGuard.js +0 -3
  55. package/test/setupTimers.js +0 -13
  56. package/test/unit/HttpClient.test.ts +0 -267
  57. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  58. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  59. package/test/unit/awards/award-calculations.test.ts +0 -34
  60. package/test/unit/awards/award-callbacks.test.ts +0 -144
  61. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  62. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  63. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  64. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  65. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  66. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  67. package/test/unit/awards/award-query-messages.test.ts +0 -448
  68. package/test/unit/awards/award-user-collection.test.ts +0 -416
  69. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  70. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  71. package/test/unit/awards/helpers/index.ts +0 -3
  72. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  73. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  74. package/test/unit/awards/message-generator.test.ts +0 -162
  75. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  76. package/test/unit/content-progress/helpers.test.ts +0 -329
  77. package/test/unit/content-progress/navigateTo.test.ts +0 -392
  78. package/test/unit/contentLikes.test.ts +0 -62
  79. package/test/unit/contentMetaData.test.ts +0 -58
  80. package/test/unit/dataContext.test.ts +0 -38
  81. package/test/unit/dateUtils.test.ts +0 -188
  82. package/test/unit/endScreen.test.js +0 -712
  83. package/test/unit/eventsAPI.test.ts +0 -159
  84. package/test/unit/imageSRCBuilder.test.ts +0 -37
  85. package/test/unit/imageSRCVerify.test.ts +0 -155
  86. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  87. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  88. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  89. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  90. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  91. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  92. package/test/unit/lib/filter.test.ts +0 -1154
  93. package/test/unit/lib/lastUpdated.test.ts +0 -21
  94. package/test/unit/lib/query.test.ts +0 -1469
  95. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  96. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  97. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  98. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  99. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  100. package/test/unit/playerSettings.test.ts +0 -73
  101. package/test/unit/progress-row/content-card.test.ts +0 -314
  102. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  103. package/test/unit/progressRows.test.ts +0 -243
  104. package/test/unit/recommendations.test.js +0 -47
  105. package/test/unit/sanityQueryService.test.ts +0 -200
  106. package/test/unit/search.test.ts +0 -44
  107. package/test/unit/sentry.test.ts +0 -62
  108. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  109. package/test/unit/services/progress.test.ts +0 -396
  110. package/test/unit/streakMessage.test.ts +0 -251
  111. package/test/unit/sync/context.test.ts +0 -51
  112. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  113. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  114. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  115. package/test/unit/sync/errors/validators.test.ts +0 -61
  116. package/test/unit/sync/helpers/TestModel.ts +0 -44
  117. package/test/unit/sync/helpers/index.ts +0 -172
  118. package/test/unit/sync/manager.test.ts +0 -303
  119. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  120. package/test/unit/sync/resolver.test.ts +0 -229
  121. package/test/unit/sync/retry.test.ts +0 -314
  122. package/test/unit/sync/run-scope.test.ts +0 -23
  123. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  124. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  125. package/test/unit/sync/store-configs.test.ts +0 -37
  126. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  127. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  128. package/test/unit/sync/utils/throttle.test.ts +0 -245
  129. package/test/unit/url-builder.test.ts +0 -72
  130. package/test/unit/user/account.test.ts +0 -66
  131. package/test/unit/user/permissions.test.js +0 -191
@@ -1,51 +0,0 @@
1
- jest.mock('../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
- jest.mock('../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
- import { makeContext } from './helpers/index'
4
- describe('SyncContext', () => {
5
- test('start calls start on all providers', () => {
6
- const context = makeContext() as any
7
- const providers = [
8
- context.session,
9
- context.connectivity,
10
- context.visibility,
11
- context.tabs,
12
- context.durability,
13
- ]
14
- providers.forEach(p => jest.spyOn(p, 'start'))
15
- context.start()
16
- providers.forEach(p => expect(p.start).toHaveBeenCalled())
17
- })
18
- test('stop calls stop on all providers', () => {
19
- const context = makeContext() as any
20
- const providers = [
21
- context.session,
22
- context.connectivity,
23
- context.visibility,
24
- context.tabs,
25
- context.durability,
26
- ]
27
- providers.forEach(p => jest.spyOn(p, 'stop'))
28
- context.stop()
29
- providers.forEach(p => expect(p.stop).toHaveBeenCalled())
30
- })
31
- test('session getter returns session provider', () => {
32
- const context = makeContext()
33
- expect(context.session).toBeDefined()
34
- })
35
- test('connectivity getter returns connectivity provider', () => {
36
- const context = makeContext()
37
- expect(context.connectivity).toBeDefined()
38
- })
39
- test('visibility getter returns visibility provider', () => {
40
- const context = makeContext()
41
- expect(context.visibility).toBeDefined()
42
- })
43
- test('tabs getter returns tabs provider', () => {
44
- const context = makeContext()
45
- expect(context.tabs).toBeDefined()
46
- })
47
- test('durability getter returns durability provider', () => {
48
- const context = makeContext()
49
- expect(context.durability).toBeDefined()
50
- })
51
- })
@@ -1,158 +0,0 @@
1
- import createLogoutWarningEffect from '@/services/sync/effects/logout-warning'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- function makeMockStore(model: any = { table: 'test' }) {
8
- let handler: ((records: any[]) => void) | null = null
9
-
10
- const store = {
11
- model,
12
- collection: {
13
- query: jest.fn().mockReturnValue({
14
- observe: jest.fn().mockReturnValue({
15
- subscribe: (h: (records: any[]) => void) => {
16
- handler = h
17
- return { unsubscribe: () => { handler = null } }
18
- },
19
- }),
20
- }),
21
- },
22
- } as unknown as SyncStore
23
-
24
- const push = (records: any[]) => handler?.(records)
25
-
26
- return { store, push }
27
- }
28
-
29
- // ---
30
-
31
- describe('notification on unsynced records', () => {
32
- test('notifyCallback called when unsynced records appear', () => {
33
- const notify = jest.fn()
34
- const { store, push } = makeMockStore()
35
-
36
- createLogoutWarningEffect(notify)(makeContext(), [store])
37
- push([{ id: 'rec-1' }])
38
-
39
- expect(notify).toHaveBeenCalledTimes(1)
40
- })
41
-
42
- test('notifyCallback called with the model class of the store', () => {
43
- const notify = jest.fn()
44
- const model = { table: 'content_progress' }
45
- const { store, push } = makeMockStore(model)
46
-
47
- createLogoutWarningEffect(notify)(makeContext(), [store])
48
- push([{ id: 'rec-1' }])
49
-
50
- expect(notify.mock.calls[0][0]).toContain(model)
51
- })
52
-
53
- test('notifyCallback called with empty array when records go back to zero', () => {
54
- const notify = jest.fn()
55
- const { store, push } = makeMockStore()
56
-
57
- createLogoutWarningEffect(notify)(makeContext(), [store])
58
- push([{ id: 'rec-1' }])
59
- push([])
60
-
61
- expect(notify).toHaveBeenCalledTimes(2)
62
- expect(notify.mock.calls[1][0]).toHaveLength(0)
63
- })
64
-
65
- test('model removed from unsyncedModels when records cleared', () => {
66
- const notify = jest.fn()
67
- const model = { table: 'content_progress' }
68
- const { store, push } = makeMockStore(model)
69
-
70
- createLogoutWarningEffect(notify)(makeContext(), [store])
71
- push([{ id: 'rec-1' }])
72
- push([])
73
-
74
- expect(notify.mock.calls[1][0]).not.toContain(model)
75
- })
76
- })
77
-
78
- // ---
79
-
80
- describe('multiple stores', () => {
81
- test('tracks unsynced models across stores independently', () => {
82
- const notify = jest.fn()
83
- const modelA = { table: 'content_progress' }
84
- const modelB = { table: 'content_likes' }
85
- const storeA = makeMockStore(modelA)
86
- const storeB = makeMockStore(modelB)
87
-
88
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
89
-
90
- storeA.push([{ id: 'rec-1' }])
91
- storeB.push([{ id: 'rec-2' }])
92
-
93
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
94
- expect(lastCall).toContain(modelA)
95
- expect(lastCall).toContain(modelB)
96
- })
97
-
98
- test('clears only the model whose records were resolved', () => {
99
- const notify = jest.fn()
100
- const modelA = { table: 'content_progress' }
101
- const modelB = { table: 'content_likes' }
102
- const storeA = makeMockStore(modelA)
103
- const storeB = makeMockStore(modelB)
104
-
105
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
106
-
107
- storeA.push([{ id: 'rec-1' }])
108
- storeB.push([{ id: 'rec-2' }])
109
- storeA.push([]) // storeA resolved
110
-
111
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
112
- expect(lastCall).not.toContain(modelA)
113
- expect(lastCall).toContain(modelB)
114
- })
115
-
116
- test('notifies once per store event regardless of other stores', () => {
117
- const notify = jest.fn()
118
- const storeA = makeMockStore({ table: 'a' })
119
- const storeB = makeMockStore({ table: 'b' })
120
-
121
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
122
-
123
- storeA.push([{ id: '1' }])
124
- storeA.push([{ id: '1' }])
125
- storeB.push([{ id: '2' }])
126
-
127
- expect(notify).toHaveBeenCalledTimes(3)
128
- })
129
- })
130
-
131
- // ---
132
-
133
- describe('teardown', () => {
134
- test('teardown unsubscribes all store subscriptions', () => {
135
- const notify = jest.fn()
136
- const { store, push } = makeMockStore()
137
-
138
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [store])
139
- teardown()
140
- push([{ id: 'rec-1' }])
141
-
142
- expect(notify).not.toHaveBeenCalled()
143
- })
144
-
145
- test('teardown unsubscribes all stores', () => {
146
- const notify = jest.fn()
147
- const storeA = makeMockStore({ table: 'a' })
148
- const storeB = makeMockStore({ table: 'b' })
149
-
150
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
151
- teardown()
152
-
153
- storeA.push([{ id: '1' }])
154
- storeB.push([{ id: '2' }])
155
-
156
- expect(notify).not.toHaveBeenCalled()
157
- })
158
- })
@@ -1,196 +0,0 @@
1
- import createPushFailureNotificationEffect, { NOTIFICATION_COOLDOWN, MUTE_PERIOD } from '@/services/sync/effects/push-failure-notification'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- beforeEach(() => {
8
- jest.useFakeTimers()
9
- })
10
-
11
- afterEach(() => {
12
- jest.useRealTimers()
13
- })
14
-
15
- // ---
16
-
17
- function makeMockStore() {
18
- const listeners = new Map<string, (() => void)[]>()
19
-
20
- const store = {
21
- on: jest.fn().mockImplementation((event: string, handler: () => void) => {
22
- const list = listeners.get(event) ?? []
23
- list.push(handler)
24
- listeners.set(event, list)
25
- return () => {
26
- const idx = list.indexOf(handler)
27
- if (idx >= 0) list.splice(idx, 1)
28
- }
29
- }),
30
- } as unknown as SyncStore
31
-
32
- const emit = (event: string) => listeners.get(event)?.forEach(h => h())
33
-
34
- return { store, emit }
35
- }
36
-
37
- function makeEffect(notify = jest.fn()) {
38
- const effect = createPushFailureNotificationEffect(notify)
39
- return { effect, notify }
40
- }
41
-
42
- // ---
43
-
44
- describe('notification triggering', () => {
45
- test('notifyCallback called when failedPush fires', () => {
46
- const { store, emit } = makeMockStore()
47
- const { effect, notify } = makeEffect()
48
-
49
- effect(makeContext(), [store])
50
- emit('failedPush')
51
-
52
- expect(notify).toHaveBeenCalledTimes(1)
53
- })
54
-
55
- test('notifyCallback called with mute function', () => {
56
- const { store, emit } = makeMockStore()
57
- const { effect, notify } = makeEffect()
58
-
59
- effect(makeContext(), [store])
60
- emit('failedPush')
61
-
62
- expect(notify.mock.calls[0][0]).toHaveProperty('mute')
63
- expect(typeof notify.mock.calls[0][0].mute).toBe('function')
64
- })
65
-
66
- test('failedPush on any store triggers notification', () => {
67
- const s1 = makeMockStore()
68
- const s2 = makeMockStore()
69
- const { effect, notify } = makeEffect()
70
-
71
- effect(makeContext(), [s1.store, s2.store])
72
-
73
- s2.emit('failedPush')
74
-
75
- expect(notify).toHaveBeenCalledTimes(1)
76
- })
77
- })
78
-
79
- // ---
80
-
81
- describe('cooldown', () => {
82
- test('second failedPush within cooldown does not notify again', () => {
83
- const { store, emit } = makeMockStore()
84
- const { effect, notify } = makeEffect()
85
-
86
- effect(makeContext(), [store])
87
- emit('failedPush')
88
- emit('failedPush')
89
-
90
- expect(notify).toHaveBeenCalledTimes(1)
91
- })
92
-
93
- test('notifies again after cooldown expires', () => {
94
- const { store, emit } = makeMockStore()
95
- const { effect, notify } = makeEffect()
96
-
97
- effect(makeContext(), [store])
98
- emit('failedPush')
99
-
100
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
101
- emit('failedPush')
102
-
103
- expect(notify).toHaveBeenCalledTimes(2)
104
- })
105
-
106
- test('does not notify just before cooldown expires', () => {
107
- const { store, emit } = makeMockStore()
108
- const { effect, notify } = makeEffect()
109
-
110
- effect(makeContext(), [store])
111
- emit('failedPush')
112
-
113
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN - 1)
114
- emit('failedPush')
115
-
116
- expect(notify).toHaveBeenCalledTimes(1)
117
- })
118
- })
119
-
120
- // ---
121
-
122
- describe('mute', () => {
123
- test('mute() prevents further notifications', () => {
124
- const { store, emit } = makeMockStore()
125
- const { effect, notify } = makeEffect()
126
-
127
- effect(makeContext(), [store])
128
- emit('failedPush')
129
-
130
- const { mute } = notify.mock.calls[0][0]
131
- mute()
132
-
133
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
134
- emit('failedPush')
135
-
136
- expect(notify).toHaveBeenCalledTimes(1)
137
- })
138
-
139
- test('notifications resume after mute period expires', () => {
140
- const { store, emit } = makeMockStore()
141
- const { effect, notify } = makeEffect()
142
-
143
- effect(makeContext(), [store])
144
- emit('failedPush')
145
-
146
- const { mute } = notify.mock.calls[0][0]
147
- mute()
148
-
149
- jest.advanceTimersByTime(MUTE_PERIOD + NOTIFICATION_COOLDOWN)
150
- emit('failedPush')
151
-
152
- expect(notify).toHaveBeenCalledTimes(2)
153
- })
154
-
155
- test('mute does not suppress notification already in progress', () => {
156
- const { store, emit } = makeMockStore()
157
- const { effect, notify } = makeEffect()
158
-
159
- effect(makeContext(), [store])
160
- emit('failedPush')
161
-
162
- expect(notify).toHaveBeenCalledTimes(1)
163
- })
164
- })
165
-
166
- // ---
167
-
168
- describe('teardown', () => {
169
- test('teardown prevents further notifications', () => {
170
- const { store, emit } = makeMockStore()
171
- const { effect, notify } = makeEffect()
172
-
173
- const teardown = effect(makeContext(), [store])
174
- teardown()
175
-
176
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
177
- emit('failedPush')
178
-
179
- expect(notify).not.toHaveBeenCalled()
180
- })
181
-
182
- test('teardown unsubscribes all stores', () => {
183
- const s1 = makeMockStore()
184
- const s2 = makeMockStore()
185
- const { effect, notify } = makeEffect()
186
-
187
- const teardown = effect(makeContext(), [s1.store, s2.store])
188
- teardown()
189
-
190
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
191
- s1.emit('failedPush')
192
- s2.emit('failedPush')
193
-
194
- expect(notify).not.toHaveBeenCalled()
195
- })
196
- })
@@ -1,106 +0,0 @@
1
- import { SyncError, SyncValidationError, SyncAbortError, SyncInitError, SyncSetupError, SyncUnexpectedError } from '../../../../src/services/sync/errors/index'
2
- import { inBoundary } from '../../../../src/services/sync/errors/boundary'
3
- import { SyncTelemetry } from '../../../../src/services/sync/telemetry/index'
4
-
5
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
6
- capture: jest.fn()
7
- } as any)
8
-
9
- describe('SyncError', () => {
10
- test('has correct name property', () => {
11
- const error = new SyncError('test message')
12
- expect(error.name).toBe('SyncError')
13
- })
14
- test('isReported() returns false by default', () => {
15
- const error = new SyncError('test message')
16
- expect(error.isReported()).toBe(false)
17
- })
18
- test('markReported() sets isReported() to true', () => {
19
- const error = new SyncError('test message')
20
- error.markReported()
21
- expect(error.isReported()).toBe(true)
22
- })
23
- test('getDetails() returns details passed to constructor', () => {
24
- const details = { table: 'content_progress', userId: 1 }
25
- const error = new SyncError('test message', details)
26
- expect(error.getDetails()).toEqual(details)
27
- })
28
- })
29
- describe('SyncValidationError', () => {
30
- test('has correct name property', () => {
31
- const error = new SyncValidationError('invalid value', {} as any)
32
- expect(error.name).toBe('SyncValidationError')
33
- })
34
- test('is instanceof SyncError', () => {
35
- const error = new SyncValidationError('invalid value', {} as any)
36
- expect(error).toBeInstanceOf(SyncError)
37
- })
38
- })
39
- describe('SyncAbortError', () => {
40
- test('has default message when none provided', () => {
41
- const error = new SyncAbortError()
42
- expect(error.message).toBe('Sync operation was aborted')
43
- })
44
- test('accepts custom message', () => {
45
- const error = new SyncAbortError('custom abort message')
46
- expect(error.message).toBe('custom abort message')
47
- })
48
- })
49
- describe('SyncInitError', () => {
50
- test('has correct name and stores cause in details', () => {
51
- const cause = new Error('original error')
52
- const error = new SyncInitError(cause)
53
- expect(error.name).toBe('SyncInitError')
54
- expect(error.getDetails()).toEqual({ cause })
55
- })
56
- })
57
- describe('SyncSetupError', () => {
58
- test('is instanceof SyncError', () => {
59
- const error = new SyncSetupError(new Error('setup failed'))
60
- expect(error).toBeInstanceOf(SyncError)
61
- })
62
- })
63
- describe('SyncUnexpectedError', () => {
64
- test('is instanceof SyncError', () => {
65
- const error = new SyncUnexpectedError('unexpected')
66
- expect(error).toBeInstanceOf(SyncError)
67
- })
68
- })
69
-
70
- describe('inBoundary', () => {
71
- let mockCapture: jest.Mock
72
-
73
- beforeEach(() => {
74
- mockCapture = jest.fn()
75
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
76
- capture: mockCapture
77
- } as any)
78
- })
79
- test('returns result of sync function when no error', () => {
80
- const result = inBoundary(() => 'hello')
81
- expect(result).toBe('hello')
82
- expect(mockCapture).not.toHaveBeenCalled()
83
- })
84
- test('throws and calls capture when sync function throws', () => {
85
- const error = new Error('sync error')
86
- expect(() => inBoundary(() => { throw error })).toThrow('sync error')
87
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
88
- })
89
- test('returns resolved value of async function when no error', async () => {
90
- const result = await inBoundary(async () => 'async hello')
91
- expect(result).toBe('async hello')
92
- expect(mockCapture).not.toHaveBeenCalled()
93
- })
94
- test('re-throws and calls capture when async function rejects', async () => {
95
- const error = new Error('async error')
96
- await expect(inBoundary(async () => { throw error })).rejects.toThrow('async error')
97
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
98
- })
99
- test('passes context to fn and to capture on error', () => {
100
- const context = { table: 'content_progress' }
101
- const error = new Error('context error')
102
- expect(() => inBoundary((ctx) => { throw error }, context)).toThrow('context error')
103
- expect(mockCapture).toHaveBeenCalledWith(error, context)
104
- })
105
- })
106
-
@@ -1,61 +0,0 @@
1
- import { percent, nullableString, string, number, varchar, numberInRange, enumValue } from '../../../../src/services/sync/errors/validators'
2
- import { SyncValidationError } from '../../../../src/services/sync/errors/index'
3
- describe('validators', () => {
4
- describe('percent', () => {
5
- test('returns value when within 0-100', () => {
6
- expect(percent(50)).toBe(50)
7
- })
8
- })
9
- test('returns 0 at minimum boundary', () => {
10
- expect(percent(0)).toBe(0)
11
- })
12
- test('returns 100 at maximum boundary', () => {
13
- expect(percent(100)).toBe(100)
14
- })
15
- test('throws SyncValidationError when value exceeds 100', () => {
16
- expect(() => percent(101)).toThrow(SyncValidationError)
17
- })
18
- test('throws SyncValidationError when value is negative', () => {
19
- expect(() => percent(-1)).toThrow(SyncValidationError)
20
- })
21
- })
22
- describe('nullableString', () => {
23
- test('returns null when value is null', () => {
24
- expect(nullableString(null)).toBeNull()
25
- })
26
- test('returns string when value is a string', () => {
27
- expect(nullableString('hello')).toBe('hello')
28
- })
29
- })
30
- describe('string', () => {
31
- test('throws SyncValidationError when value is not a string', () => {
32
- expect(() => string(123)).toThrow(SyncValidationError)
33
- })
34
- })
35
- describe('number', () => {
36
- test('throws SyncValidationError when value is not a number', () => {
37
- expect(() => number('abc')).toThrow(SyncValidationError)
38
- })
39
- })
40
- describe('varchar', () => {
41
- test('returns string when within max length', () => {
42
- expect(varchar(10)('hello')).toBe('hello')
43
- })
44
- test('throws SyncValidationError when string exceeds max length', () => {
45
- expect(() => varchar(5)('toolong')).toThrow(SyncValidationError)
46
- })
47
- })
48
- describe('numberInRange', () => {
49
- test('returns value when within range', () => {
50
- expect(numberInRange(0, 10)(5)).toBe(5)
51
- })
52
- test('throws SyncValidationError when value exceeds max', () => {
53
- expect(() => numberInRange(0, 10)(11)).toThrow(SyncValidationError)
54
- })
55
- })
56
- describe('enumValue', () => {
57
- test('throws SyncValidationError when value is not in enum', () => {
58
- enum TestEnum { A = 'a', B = 'b' }
59
- expect(() => enumValue(TestEnum)('c')).toThrow(SyncValidationError)
60
- })
61
- })
@@ -1,44 +0,0 @@
1
- import { appSchema, tableSchema } from '@nozbe/watermelondb'
2
- import { Database } from '@nozbe/watermelondb'
3
- import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
4
- import BaseModel from '@/services/sync/models/Base'
5
-
6
- export const TEST_TABLE = 'test_items'
7
-
8
- export const testSchema = appSchema({
9
- version: 1,
10
- tables: [
11
- tableSchema({
12
- name: TEST_TABLE,
13
- columns: [
14
- { name: 'server_record_id', type: 'number' },
15
- { name: 'value', type: 'string' },
16
- { name: 'score', type: 'number' },
17
- { name: 'created_at', type: 'number' },
18
- { name: 'updated_at', type: 'number' },
19
- ],
20
- }),
21
- ],
22
- })
23
-
24
- export default class TestModel extends BaseModel {
25
- static table = TEST_TABLE
26
-
27
- get value() { return this._getRaw('value') as string }
28
- get score() { return this._getRaw('score') as number }
29
-
30
- set value(v: string) { this._setRaw('value', v) }
31
- set score(v: number) { this._setRaw('score', v) }
32
- }
33
-
34
- export function makeTestDatabase(opts: { useIncrementalIndexedDB?: boolean; dbName?: string } = {}) {
35
- const adapter = new LokiJSAdapter({
36
- schema: testSchema,
37
- useWebWorker: false,
38
- useIncrementalIndexedDB: opts.useIncrementalIndexedDB ?? false,
39
- dbName: opts.dbName ?? `test_store_${Date.now()}_${Math.random()}`,
40
- extraLokiOptions: { autosave: false },
41
- })
42
-
43
- return new Database({ adapter, modelClasses: [TestModel] })
44
- }