musora-content-services 2.167.1 → 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 (136) hide show
  1. package/.agent/decisions/2026-07-17-onboarding-statuses.md +21 -0
  2. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  3. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  4. package/CHANGELOG.md +15 -0
  5. package/package.json +2 -1
  6. package/src/contentTypeConfig.js +2 -0
  7. package/src/index.d.ts +2 -0
  8. package/src/index.js +2 -0
  9. package/src/lib/sanity/query.ts +5 -0
  10. package/src/services/config.js +15 -0
  11. package/src/services/content/artist.ts +1 -3
  12. package/src/services/content/counts.ts +47 -0
  13. package/src/services/content/genre.ts +1 -3
  14. package/src/services/content/instructor.ts +1 -3
  15. package/src/services/contentProgress.js +1 -1
  16. package/src/services/eventsAPI.js +49 -7
  17. package/src/services/user/onboarding.ts +22 -0
  18. package/src/services/userActivity.js +2 -0
  19. package/src/version-info.js +1 -0
  20. package/.claude/settings.local.json +0 -52
  21. package/test/initializeTests.js +0 -60
  22. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  23. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  24. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  25. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  26. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  27. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  28. package/test/integration/content-progress/getters.test.ts +0 -334
  29. package/test/integration/content-progress/helpers.test.ts +0 -263
  30. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  31. package/test/integration/forums.test.ts +0 -209
  32. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  33. package/test/integration/initializeTestDB.ts +0 -80
  34. package/test/integration/learning-paths.test.ts +0 -854
  35. package/test/integration/notifications.test.ts +0 -178
  36. package/test/integration/progress-row/content-card.test.ts +0 -283
  37. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  38. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  39. package/test/integration/sync/fetch.test.ts +0 -224
  40. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  41. package/test/integration/sync/repositories/practices.test.ts +0 -179
  42. package/test/integration/sync/repositories/progress.test.ts +0 -245
  43. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  44. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  45. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  46. package/test/integration/sync/store/store-idb.test.ts +0 -180
  47. package/test/integration/sync/store/store.test.ts +0 -361
  48. package/test/integration/user/permissions.test.js +0 -20
  49. package/test/integration/userActivity.test.ts +0 -102
  50. package/test/live/README.md +0 -29
  51. package/test/live/content.test.js +0 -116
  52. package/test/localStorageMock.js +0 -21
  53. package/test/log.js +0 -5
  54. package/test/mockData/award-definitions.js +0 -171
  55. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  56. package/test/mockData/mockData_progress_content.json +0 -182
  57. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  58. package/test/mockData/mockData_user_practices.json +0 -9
  59. package/test/setupConsole.js +0 -6
  60. package/test/setupNetworkGuard.js +0 -3
  61. package/test/setupTimers.js +0 -13
  62. package/test/unit/HttpClient.test.ts +0 -267
  63. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  64. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  65. package/test/unit/awards/award-calculations.test.ts +0 -34
  66. package/test/unit/awards/award-callbacks.test.ts +0 -144
  67. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  68. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  69. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  70. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  71. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  72. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  73. package/test/unit/awards/award-query-messages.test.ts +0 -448
  74. package/test/unit/awards/award-user-collection.test.ts +0 -416
  75. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  76. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  77. package/test/unit/awards/helpers/index.ts +0 -3
  78. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  79. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  80. package/test/unit/awards/message-generator.test.ts +0 -162
  81. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  82. package/test/unit/content-progress/helpers.test.ts +0 -329
  83. package/test/unit/content-progress/navigateTo.test.ts +0 -381
  84. package/test/unit/contentLikes.test.ts +0 -62
  85. package/test/unit/contentMetaData.test.ts +0 -58
  86. package/test/unit/dataContext.test.ts +0 -38
  87. package/test/unit/dateUtils.test.ts +0 -188
  88. package/test/unit/endScreen.test.js +0 -712
  89. package/test/unit/imageSRCBuilder.test.ts +0 -37
  90. package/test/unit/imageSRCVerify.test.ts +0 -155
  91. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  92. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  93. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  94. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  95. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  96. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  97. package/test/unit/lib/filter.test.ts +0 -1154
  98. package/test/unit/lib/lastUpdated.test.ts +0 -21
  99. package/test/unit/lib/query.test.ts +0 -1469
  100. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  101. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  102. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  103. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  104. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  105. package/test/unit/playerSettings.test.ts +0 -73
  106. package/test/unit/progress-row/content-card.test.ts +0 -314
  107. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  108. package/test/unit/progressRows.test.ts +0 -243
  109. package/test/unit/recommendations.test.js +0 -47
  110. package/test/unit/sanityQueryService.test.ts +0 -200
  111. package/test/unit/search.test.ts +0 -44
  112. package/test/unit/sentry.test.ts +0 -62
  113. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  114. package/test/unit/services/progress.test.ts +0 -396
  115. package/test/unit/streakMessage.test.ts +0 -251
  116. package/test/unit/sync/context.test.ts +0 -51
  117. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  118. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  119. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  120. package/test/unit/sync/errors/validators.test.ts +0 -61
  121. package/test/unit/sync/helpers/TestModel.ts +0 -44
  122. package/test/unit/sync/helpers/index.ts +0 -172
  123. package/test/unit/sync/manager.test.ts +0 -303
  124. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  125. package/test/unit/sync/resolver.test.ts +0 -229
  126. package/test/unit/sync/retry.test.ts +0 -314
  127. package/test/unit/sync/run-scope.test.ts +0 -23
  128. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  129. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  130. package/test/unit/sync/store-configs.test.ts +0 -37
  131. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  132. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  133. package/test/unit/sync/utils/throttle.test.ts +0 -245
  134. package/test/unit/url-builder.test.ts +0 -72
  135. package/test/unit/user/account.test.ts +0 -66
  136. package/test/unit/user/permissions.test.js +0 -191
@@ -1,172 +0,0 @@
1
- import { Database } from '@nozbe/watermelondb'
2
- import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
3
- import { SyncTelemetry, SeverityLevel } from '@/services/sync/telemetry/index'
4
- import SyncContext from '@/services/sync/context/index'
5
- import {
6
- BaseSessionProvider,
7
- BaseConnectivityProvider,
8
- BaseVisibilityProvider,
9
- NullTabsProvider,
10
- BaseDurabilityProvider,
11
- } from '@/services/sync/context/providers/index'
12
- import SyncRetry from '@/services/sync/retry'
13
- import SyncRunScope from '@/services/sync/run-scope'
14
- import SyncStore, { type SyncStoreConfig } from '@/services/sync/store/index'
15
- import schema from '@/services/sync/schema/index'
16
- import * as modelClasses from '@/services/sync/models/index'
17
- import type BaseModel from '@/services/sync/models/Base'
18
- import type { ModelClass, SyncUserScope } from '@/services/sync/index'
19
- import type { SyncPullResponse, SyncPushResponse } from '@/services/sync/fetch'
20
-
21
- // --- Sentry / Telemetry ---
22
-
23
- function makeDummySentry() {
24
- return {
25
- captureException: jest.fn(),
26
- captureMessage: jest.fn(),
27
- addBreadcrumb: jest.fn(),
28
- startSpan: jest.fn((options: any, callback: any) => {
29
- const mockSpan = {
30
- _spanId: 'mock-span-id-0000',
31
- _parentSpanId: undefined as string | undefined,
32
- end: jest.fn(),
33
- setStatus: jest.fn(),
34
- setData: jest.fn(),
35
- setAttribute: jest.fn(),
36
- }
37
- return callback ? callback(mockSpan) : mockSpan
38
- }),
39
- logger: {
40
- debug: jest.fn(),
41
- info: jest.fn(),
42
- warn: jest.fn(),
43
- error: jest.fn(),
44
- trace: jest.fn(),
45
- fatal: jest.fn(),
46
- fmt: jest.fn(),
47
- },
48
- }
49
- }
50
-
51
- export function makeTelemetry(userScope: SyncUserScope) {
52
- return new SyncTelemetry(userScope, {
53
- Sentry: makeDummySentry(),
54
- level: SeverityLevel.FATAL, // silence output in tests
55
- pretty: false,
56
- })
57
- }
58
-
59
- // --- Context providers ---
60
-
61
- class TestSessionProvider extends BaseSessionProvider {
62
- getClientId() { return 'test-client-id' }
63
- }
64
-
65
- class TestConnectivityProvider extends BaseConnectivityProvider {
66
- private online = true
67
- getValue() { return this.online }
68
- setOnline(v: boolean) { this.online = v; this['notifyListeners']() }
69
- }
70
-
71
- class TestVisibilityProvider extends BaseVisibilityProvider {
72
- getValue() { return true }
73
- }
74
-
75
- class TestDurabilityProvider extends BaseDurabilityProvider {
76
- getValue() { return true }
77
- failed() {}
78
- }
79
-
80
- export function makeContext() {
81
- return new SyncContext({
82
- session: new TestSessionProvider(),
83
- connectivity: new TestConnectivityProvider(),
84
- visibility: new TestVisibilityProvider(),
85
- tabs: new NullTabsProvider(),
86
- durability: new TestDurabilityProvider(),
87
- })
88
- }
89
-
90
- export const TEST_USER_ID = 1
91
-
92
- export function makeUserScope(): SyncUserScope {
93
- return {
94
- initialId: TEST_USER_ID,
95
- getCurrentId: () => TEST_USER_ID,
96
- }
97
- }
98
-
99
- // --- Database ---
100
-
101
- export function makeDatabase(opts: { useIncrementalIndexedDB?: boolean } = {}) {
102
- const adapter = new LokiJSAdapter({
103
- schema,
104
- useWebWorker: false,
105
- useIncrementalIndexedDB: opts.useIncrementalIndexedDB ?? false,
106
- dbName: `test_${Date.now()}_${Math.random()}`,
107
- extraLokiOptions: { autosave: false },
108
- })
109
-
110
- return new Database({
111
- adapter,
112
- modelClasses: Object.values(modelClasses),
113
- })
114
- }
115
-
116
- // --- Pull / Push mocks ---
117
-
118
- export function makePullMock(overrides: Partial<Extract<SyncPullResponse, { ok: true }>> = {}) {
119
- return jest.fn().mockResolvedValue({
120
- ok: true,
121
- entries: [],
122
- token: Date.now(),
123
- intendedUserId: TEST_USER_ID,
124
- previousToken: null,
125
- ...overrides,
126
- } as Extract<SyncPullResponse, { ok: true }>)
127
- }
128
-
129
- export function makePushMock(overrides: Partial<Extract<SyncPushResponse, { ok: true }>> = {}) {
130
- return jest.fn().mockResolvedValue({
131
- ok: true,
132
- results: [],
133
- ...overrides,
134
- } as Extract<SyncPushResponse, { ok: true }>)
135
- }
136
-
137
- // --- SyncStore ---
138
-
139
- export function makeStore<TModel extends BaseModel>(
140
- model: ModelClass<TModel>,
141
- db: Database,
142
- config: Partial<SyncStoreConfig<TModel>> = {}
143
- ) {
144
- const userScope = makeUserScope()
145
- const context = makeContext()
146
- const telemetry = makeTelemetry(userScope)
147
- const retry = new SyncRetry(context, telemetry)
148
- const runScope = new SyncRunScope()
149
-
150
- const store = new SyncStore<TModel>(
151
- {
152
- model,
153
- pull: makePullMock(),
154
- push: makePushMock(),
155
- ...config,
156
- },
157
- userScope,
158
- context,
159
- db,
160
- retry,
161
- runScope,
162
- telemetry
163
- )
164
-
165
- return { store, userScope, context, telemetry, retry, runScope }
166
- }
167
-
168
- export async function resetDatabase(db: Database) {
169
- await db.write(async () => {
170
- await db.unsafeResetDatabase()
171
- })
172
- }
@@ -1,303 +0,0 @@
1
- import SyncManager from '@/services/sync/manager'
2
- import { SyncTelemetry } from '@/services/sync/telemetry/index'
3
- import { SyncError } from '@/services/sync/errors/index'
4
- import BaseModel from '@/services/sync/models/Base'
5
- import createStoreConfigs from '@/services/sync/store-configs'
6
- import { makeTelemetry, makeContext, makeUserScope, makeDatabase } from './helpers/index'
7
- import type { ModelClass, SyncUserScope } from '@/services/sync/index'
8
-
9
- class DummyModelA extends BaseModel { static table = 'dummy_a' }
10
- class DummyModelB extends BaseModel { static table = 'dummy_b' }
11
- class UnknownModel extends BaseModel { static table = 'not_a_real_table' }
12
-
13
- jest.mock('../../../src/services/awards/internal/content-progress-observer', () => ({
14
- contentProgressObserver: {
15
- start: jest.fn().mockResolvedValue(undefined),
16
- stop: jest.fn(),
17
- },
18
- }))
19
-
20
- // ---
21
-
22
- function makeMockStrategy() {
23
- return {
24
- start: jest.fn(),
25
- stop: jest.fn(),
26
- onTrigger: jest.fn().mockReturnThis(),
27
- }
28
- }
29
-
30
- function makeMockEffect() {
31
- const teardown = jest.fn()
32
- const effect = jest.fn().mockReturnValue(teardown)
33
- return { effect, teardown }
34
- }
35
-
36
- function buildManager(opts: {
37
- userScope?: SyncUserScope
38
- destroy?: jest.Mock
39
- abort?: jest.Mock
40
- } = {}) {
41
- const userScope = opts.userScope ?? makeUserScope()
42
- const context = makeContext()
43
- const teardownDatabase = {
44
- ...(opts.destroy && { destroy: opts.destroy }),
45
- ...(opts.abort && { abort: opts.abort }),
46
- }
47
-
48
- return new SyncManager(userScope, context, () => makeDatabase(), teardownDatabase)
49
- }
50
-
51
- function assignManager(manager: SyncManager) {
52
- return SyncManager.assignAndSetupInstance(manager)
53
- }
54
-
55
- function setupManager(opts: Parameters<typeof buildManager>[0] = {}) {
56
- const manager = buildManager(opts)
57
- const teardown = assignManager(manager)
58
- return { manager, teardown }
59
- }
60
-
61
- // ---
62
-
63
- beforeEach(() => {
64
- SyncTelemetry.setInstance(makeTelemetry(makeUserScope()))
65
- })
66
-
67
- afterEach(() => {
68
- SyncTelemetry.clearInstance()
69
- ;(SyncManager as any).instance = null
70
- })
71
-
72
- // ---
73
-
74
- describe('singleton lifecycle', () => {
75
- test('getInstance throws before any setup', () => {
76
- expect(() => SyncManager.getInstance()).toThrow(SyncError)
77
- })
78
-
79
- test('getInstanceOrNull returns null before any setup', () => {
80
- expect(SyncManager.getInstanceOrNull()).toBeNull()
81
- })
82
-
83
- test('getInstance returns manager after setup', async () => {
84
- const { manager, teardown } = setupManager()
85
- expect(SyncManager.getInstance()).toBe(manager)
86
- await teardown()
87
- })
88
-
89
- test('getInstanceOrNull returns manager after setup', async () => {
90
- const { manager, teardown } = setupManager()
91
- expect(SyncManager.getInstanceOrNull()).toBe(manager)
92
- await teardown()
93
- })
94
-
95
- test('assignAndSetupInstance throws if called while instance exists', async () => {
96
- const { teardown } = setupManager()
97
- expect(() => assignManager(buildManager())).toThrow(SyncError)
98
- await teardown()
99
- })
100
-
101
- test('getInstance throws after teardown', async () => {
102
- const { teardown } = setupManager()
103
- await teardown()
104
- expect(() => SyncManager.getInstance()).toThrow(SyncError)
105
- })
106
-
107
- test('getInstanceOrNull returns null after teardown', async () => {
108
- const { teardown } = setupManager()
109
- await teardown()
110
- expect(SyncManager.getInstanceOrNull()).toBeNull()
111
- })
112
-
113
- test('new instance can be set up after teardown', async () => {
114
- const { teardown: t1 } = setupManager()
115
- await t1()
116
-
117
- const { manager: m2, teardown: t2 } = setupManager()
118
- expect(SyncManager.getInstance()).toBe(m2)
119
- await t2()
120
- })
121
- })
122
-
123
- // ---
124
-
125
- describe('store registry', () => {
126
- test('all configured model tables have stores', async () => {
127
- const { manager, teardown } = setupManager()
128
- const configuredTables = createStoreConfigs().map(c => c.model.table)
129
- const registeredTables = Object.keys(manager.getAllStores())
130
-
131
- expect(registeredTables.sort()).toEqual(configuredTables.sort())
132
- await teardown()
133
- })
134
-
135
- test('getStore returns store for a registered model', async () => {
136
- const { manager, teardown } = setupManager()
137
- const anyConfiguredModel = createStoreConfigs()[0].model as unknown as ModelClass
138
- expect(manager.getStore(anyConfiguredModel)).toBeDefined()
139
- await teardown()
140
- })
141
-
142
- test('getStore throws SyncError for unregistered model', async () => {
143
- const { manager, teardown } = setupManager()
144
- expect(() => manager.getStore(UnknownModel)).toThrow(SyncError)
145
- await teardown()
146
- })
147
- })
148
-
149
- // ---
150
-
151
- describe('getId', () => {
152
- test('successive instances get different ids', async () => {
153
- const { manager: m1, teardown: t1 } = setupManager()
154
- const id1 = m1.getId()
155
- await t1()
156
-
157
- const { manager: m2, teardown: t2 } = setupManager()
158
- const id2 = m2.getId()
159
- await t2()
160
-
161
- expect(id1).not.toBe(id2)
162
- })
163
- })
164
-
165
- // ---
166
-
167
- describe('strategy registration', () => {
168
- test('strategy.start called during setup', async () => {
169
- const manager = buildManager()
170
- const strategy = makeMockStrategy()
171
- manager.registerStrategies([DummyModelA], [strategy])
172
- const teardown = assignManager(manager)
173
-
174
- expect(strategy.start).toHaveBeenCalledTimes(1)
175
- await teardown()
176
- })
177
-
178
- test('strategy.stop called during teardown', async () => {
179
- const manager = buildManager()
180
- const strategy = makeMockStrategy()
181
- manager.registerStrategies([DummyModelA], [strategy])
182
- const teardown = assignManager(manager)
183
- await teardown()
184
-
185
- expect(strategy.stop).toHaveBeenCalledTimes(1)
186
- })
187
-
188
- test('onTrigger called once per registered model', async () => {
189
- const manager = buildManager()
190
- const strategy = makeMockStrategy()
191
- manager.registerStrategies([DummyModelA, DummyModelB], [strategy])
192
- const teardown = assignManager(manager)
193
-
194
- expect(strategy.onTrigger).toHaveBeenCalledTimes(2)
195
- await teardown()
196
- })
197
-
198
- test('multiple strategies all started and stopped', async () => {
199
- const manager = buildManager()
200
- const s1 = makeMockStrategy()
201
- const s2 = makeMockStrategy()
202
- manager.registerStrategies([DummyModelA], [s1, s2])
203
- const teardown = assignManager(manager)
204
- await teardown()
205
-
206
- expect(s1.start).toHaveBeenCalledTimes(1)
207
- expect(s2.start).toHaveBeenCalledTimes(1)
208
- expect(s1.stop).toHaveBeenCalledTimes(1)
209
- expect(s2.stop).toHaveBeenCalledTimes(1)
210
- })
211
- })
212
-
213
- // ---
214
-
215
- describe('effect registration', () => {
216
- test('effect called with context and stores during setup', async () => {
217
- const manager = buildManager()
218
- const { effect, teardown: effectTeardown } = makeMockEffect()
219
- manager.registerEffects([DummyModelA], [effect])
220
- const teardown = assignManager(manager)
221
-
222
- expect(effect).toHaveBeenCalledTimes(1)
223
- expect(effect).toHaveBeenCalledWith(
224
- expect.anything(),
225
- expect.any(Array)
226
- )
227
-
228
- await teardown()
229
- })
230
-
231
- test('effect receives one store per registered model', async () => {
232
- const manager = buildManager()
233
- const { effect } = makeMockEffect()
234
- manager.registerEffects([DummyModelA, DummyModelB], [effect])
235
- const teardown = assignManager(manager)
236
-
237
- const [, stores] = effect.mock.calls[0]
238
- expect(stores).toHaveLength(2)
239
-
240
- await teardown()
241
- })
242
-
243
- test('effect teardown called during manager teardown', async () => {
244
- const manager = buildManager()
245
- const { effect, teardown: effectTeardown } = makeMockEffect()
246
- manager.registerEffects([DummyModelA], [effect])
247
- const teardown = assignManager(manager)
248
- await teardown()
249
-
250
- expect(effectTeardown).toHaveBeenCalledTimes(1)
251
- })
252
- })
253
-
254
- // ---
255
-
256
- describe('teardown modes', () => {
257
- test('reset mode resolves', async () => {
258
- const { teardown } = setupManager()
259
- await expect(teardown('reset')).resolves.toBeUndefined()
260
- })
261
-
262
- test('default mode (no arg) resolves', async () => {
263
- const { teardown } = setupManager()
264
- await expect(teardown()).resolves.toBeUndefined()
265
- })
266
-
267
- test('destroyOrReset calls destroy with dbName and adapter', async () => {
268
- const destroy = jest.fn().mockResolvedValue(undefined)
269
- const { teardown } = setupManager({ destroy })
270
- await expect(teardown('destroyOrReset')).resolves.toBeUndefined()
271
- expect(destroy).toHaveBeenCalledTimes(1)
272
- expect(destroy).toHaveBeenCalledWith(expect.any(String), expect.anything())
273
- })
274
-
275
- test('abortWrites rejects when no implementation provided', async () => {
276
- const { teardown } = setupManager()
277
- await expect(teardown('abortWrites')).rejects.toThrow(
278
- 'Cannot abort writes to database - implementation not provided'
279
- )
280
- })
281
-
282
- test('abortWrites calls abort with underlying adapter', async () => {
283
- const abort = jest.fn().mockResolvedValue(undefined)
284
- const { teardown } = setupManager({ abort })
285
- await expect(teardown('abortWrites')).resolves.toBeUndefined()
286
- expect(abort).toHaveBeenCalledTimes(1)
287
- })
288
-
289
- test('concurrent teardown calls both resolve', async () => {
290
- const { teardown } = setupManager()
291
- await expect(Promise.all([teardown(), teardown()])).resolves.toBeDefined()
292
- })
293
- })
294
-
295
- // ---
296
-
297
- describe('abort', () => {
298
- test('does not throw', async () => {
299
- const { manager, teardown } = setupManager()
300
- expect(() => manager.abort('test reason')).not.toThrow()
301
- await teardown()
302
- })
303
- })
@@ -1,82 +0,0 @@
1
- jest.mock('../../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
- jest.mock('../../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
-
4
- import { Database } from '@nozbe/watermelondb'
5
- import { makeDatabase, makeStore, resetDatabase } from '../helpers/index'
6
- import UserAwardProgress from '../../../../src/services/sync/models/UserAwardProgress'
7
- import UserAwardProgressRepository from '../../../../src/services/sync/repositories/user-award-progress'
8
- let db: Database
9
- let repo: UserAwardProgressRepository
10
- beforeEach(() => {
11
- db = makeDatabase()
12
- const { store } = makeStore(UserAwardProgress, db)
13
- repo = new UserAwardProgressRepository(store)
14
- })
15
- afterEach(async () => {
16
- await resetDatabase(db)
17
- })
18
- describe('UserAwardProgress model', () => {
19
- describe('getters', () => {
20
- test('award_id returns raw string value', async () => {
21
- await repo.recordAwardProgress('test-award-id', 50)
22
- const result = await repo.getByAwardId('test-award-id')
23
- expect(result.data?.award_id).toBe('test-award-id')
24
- })
25
- test('progress_percentage returns raw number value', async () => {
26
- await repo.recordAwardProgress('test-award-id', 75)
27
- const result = await repo.getByAwardId('test-award-id')
28
- expect(result.data?.progress_percentage).toBe(75)
29
- })
30
- test('completed_at returns null when not set', async () => {
31
- await repo.recordAwardProgress('test-award-id', 50)
32
- const result = await repo.getByAwardId('test-award-id')
33
- expect(result.data?.completed_at).toBeNull()
34
- })
35
- test('progress_data parses JSON when set', async () => {
36
- const progressData = { step: 1, total: 5 }
37
- await repo.recordAwardProgress('test-award-id', 50, { progressData })
38
- const result = await repo.getByAwardId('test-award-id')
39
- expect(result.data?.progress_data).toEqual(progressData)
40
- })
41
- test('progress_data returns null when not set', async () => {
42
- await repo.recordAwardProgress('test-award-id', 50)
43
- const result = await repo.getByAwardId('test-award-id')
44
- expect(result.data?.progress_data).toBeNull()
45
- })
46
- test('completion_data parses JSON when set', async () => {
47
- const completionData = {
48
- content_title: 'Blues Foundations',
49
- completed_at: '2024-01-01T00:00:00Z',
50
- days_user_practiced: 14,
51
- practice_minutes: 180,
52
- }
53
- await repo.recordAwardProgress('test-award-id', 100, { completionData })
54
- const result = await repo.getByAwardId('test-award-id')
55
- expect(result.data?.completion_data).toEqual(completionData)
56
- })
57
- test('completion_data returns null when not set', async () => {
58
- await repo.recordAwardProgress('test-award-id', 50)
59
- const result = await repo.getByAwardId('test-award-id')
60
- expect(result.data?.completion_data).toBeNull()
61
- })
62
- })
63
- describe('setters via validators', () => {
64
- test.todo('progress_percentage throws SyncValidationError when value exceeds 100')
65
- test('completed_at accepts null', async () => {
66
- await repo.recordAwardProgress('test-award-id', 50, { completedAt: null })
67
- const result = await repo.getByAwardId('test-award-id')
68
- expect(result.data?.completed_at).toBeNull()
69
- })
70
- test('completion_data serialises to JSON when set', async () => {
71
- const completionData = {
72
- content_title: 'Test',
73
- completed_at: '2024-01-01T00:00:00Z',
74
- days_user_practiced: 5,
75
- practice_minutes: 60,
76
- }
77
- await repo.recordAwardProgress('test-award-id', 100, { completionData })
78
- const result = await repo.getByAwardId('test-award-id')
79
- expect(result.data?.completion_data).toEqual(completionData)
80
- })
81
- })
82
- })