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,217 +0,0 @@
1
- import { Database } from '@nozbe/watermelondb'
2
- import { makeTelemetry, makeContext, makePullMock, makePushMock } from '../../../unit/sync/helpers/index'
3
- import TestModel, { makeTestDatabase } from '../../../unit/sync/helpers/TestModel'
4
- import SyncStore from '@/services/sync/store/index'
5
- import SyncRetry from '@/services/sync/retry'
6
- import SyncRunScope from '@/services/sync/run-scope'
7
- import type { SyncUserScope } from '@/services/sync/index'
8
- import { SyncError } from '@/services/sync/errors/index'
9
-
10
- // ---
11
-
12
- let db: Database
13
-
14
- function makeStore(userScope: SyncUserScope, pull = makePullMock()) {
15
- const context = makeContext()
16
- const telemetry = makeTelemetry(userScope)
17
- const retry = new SyncRetry(context, telemetry)
18
- const runScope = new SyncRunScope()
19
-
20
- return new SyncStore<TestModel>(
21
- { model: TestModel, pull, push: makePushMock() },
22
- userScope,
23
- context,
24
- db,
25
- retry,
26
- runScope,
27
- telemetry
28
- )
29
- }
30
-
31
- function makeScope(overrides: Partial<SyncUserScope> = {}): SyncUserScope {
32
- return {
33
- initialId: 1,
34
- getCurrentId: () => 1,
35
- ...overrides,
36
- }
37
- }
38
-
39
- function makePullResponse(overrides: Partial<{ intendedUserId: number }> = {}) {
40
- return jest.fn().mockResolvedValue({
41
- ok: true,
42
- entries: [],
43
- token: Date.now(),
44
- previousToken: null,
45
- intendedUserId: 1,
46
- ...overrides,
47
- })
48
- }
49
-
50
- beforeEach(() => {
51
- db = makeTestDatabase()
52
- })
53
-
54
- afterEach(async () => {
55
- await db.write(async () => db.unsafeResetDatabase())
56
- })
57
-
58
- // ---
59
-
60
- describe('write protection (paranoidWrite)', () => {
61
- describe('same user', () => {
62
- test('write proceeds when getCurrentId matches initialId', async () => {
63
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 1 }))
64
- const result = await store.insertOne(r => { r.value = 'hello' })
65
- store.destroy()
66
-
67
- expect(result.value).toBe('hello')
68
- })
69
- })
70
-
71
- describe('different user', () => {
72
- test('throws SyncError when getCurrentId returns different id', async () => {
73
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 2 }))
74
-
75
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(SyncError)
76
- store.destroy()
77
- })
78
-
79
- test('error message indicates cross-user write', async () => {
80
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 2 }))
81
-
82
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
83
- 'Aborted cross-user write operation'
84
- )
85
- store.destroy()
86
- })
87
- })
88
-
89
- describe('null getCurrentId without fetchCurrentId', () => {
90
- test('throws SyncError', async () => {
91
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null }))
92
-
93
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(SyncError)
94
- store.destroy()
95
- })
96
-
97
- test('error message indicates cross-user write', async () => {
98
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null }))
99
-
100
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
101
- 'Aborted cross-user write operation'
102
- )
103
- store.destroy()
104
- })
105
- })
106
-
107
- describe('fetchCurrentId fallback', () => {
108
- test('fetchCurrentId is called when getCurrentId returns null', async () => {
109
- const fetchCurrentId = jest.fn().mockResolvedValue(1)
110
- const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null, fetchCurrentId }))
111
-
112
- await store.insertOne(r => { r.value = 'hello' })
113
- store.destroy()
114
-
115
- expect(fetchCurrentId).toHaveBeenCalledTimes(1)
116
- })
117
-
118
- test('write proceeds when fetchCurrentId resolves to same id', async () => {
119
- const store = makeStore(makeScope({
120
- initialId: 1,
121
- getCurrentId: () => null,
122
- fetchCurrentId: () => Promise.resolve(1),
123
- }))
124
-
125
- const result = await store.insertOne(r => { r.value = 'resolved' })
126
- store.destroy()
127
-
128
- expect(result.value).toBe('resolved')
129
- })
130
-
131
- test('throws when fetchCurrentId resolves to different id', async () => {
132
- const store = makeStore(makeScope({
133
- initialId: 1,
134
- getCurrentId: () => null,
135
- fetchCurrentId: () => Promise.resolve(2),
136
- }))
137
-
138
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
139
- 'Aborted cross-user write operation'
140
- )
141
- store.destroy()
142
- })
143
-
144
- test('throws SyncError when fetchCurrentId rejects', async () => {
145
- const store = makeStore(makeScope({
146
- initialId: 1,
147
- getCurrentId: () => null,
148
- fetchCurrentId: () => Promise.reject(new Error('network error')),
149
- }))
150
-
151
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toBeInstanceOf(SyncError)
152
- store.destroy()
153
- })
154
-
155
- test('error message indicates fetchCurrentId failed', async () => {
156
- const store = makeStore(makeScope({
157
- initialId: 1,
158
- getCurrentId: () => null,
159
- fetchCurrentId: () => Promise.reject(new Error('network error')),
160
- }))
161
-
162
- await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
163
- 'Aborted cross-user write operation after fetchCurrentId failed'
164
- )
165
- store.destroy()
166
- })
167
- })
168
- })
169
-
170
- // ---
171
-
172
- describe('pull protection (intendedUserId)', () => {
173
- test('pull succeeds when intendedUserId matches initialId', async () => {
174
- const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 1 }))
175
-
176
- await expect(store.pull('test')).resolves.not.toThrow()
177
- store.destroy()
178
- })
179
-
180
- test('throws SyncError when intendedUserId does not match initialId', async () => {
181
- const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 2 }))
182
-
183
- await expect(store.pull('test')).rejects.toThrow(SyncError)
184
- store.destroy()
185
- })
186
-
187
- test('throws when intendedUserId does not match currentId', async () => {
188
- const store = makeStore(
189
- makeScope({ initialId: 1, getCurrentId: () => 2 }),
190
- makePullResponse({ intendedUserId: 1 })
191
- )
192
-
193
- await expect(store.pull('test')).rejects.toThrow(SyncError)
194
- store.destroy()
195
- })
196
-
197
- test('error message indicates intended user mismatch', async () => {
198
- const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 2 }))
199
-
200
- await expect(store.pull('test')).rejects.toThrow('Intended user ID does not match')
201
- store.destroy()
202
- })
203
-
204
- test('throws SyncError when getCurrentId is null and fetchCurrentId rejects during pull', async () => {
205
- const store = makeStore(
206
- makeScope({
207
- initialId: 1,
208
- getCurrentId: () => null,
209
- fetchCurrentId: () => Promise.reject(new Error('auth error')),
210
- }),
211
- makePullResponse({ intendedUserId: 1 })
212
- )
213
-
214
- await expect(store.pull('test')).rejects.toThrow(SyncError)
215
- store.destroy()
216
- })
217
- })
@@ -1,180 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- *
4
- * Tests SyncStore with the IncrementalIndexedDB-backed LokiJS adapter.
5
- *
6
- * Scope: IDB-specific behavior only (fetch token persistence, initialization,
7
- * and bootstrapping — data loading from IDB into a new Database instance).
8
- * General CRUD/event/coalescing behavior is covered in store.test.ts, which uses
9
- * the plain LokiJS in-memory adapter.
10
- *
11
- * Concurrency note: loki.saveDatabase() with fakeIndexedDB's IncrementalIDBAdapter
12
- * produces conflicting IDB transactions when multiple writes happen in rapid sequence
13
- * (IncrementalIDBAdapter auto-save overlaps with ensurePersistence()'s saveDatabase()).
14
- * Bootstrapping tests use a single write per "session" to stay within the safe window.
15
- */
16
-
17
- import { IDBFactory } from 'fake-indexeddb'
18
- import { makeTelemetry, makeContext, makeUserScope, makePullMock, makePushMock, TEST_USER_ID } from '../../../unit/sync/helpers/index'
19
- import TestModel, { makeTestDatabase } from '../../../unit/sync/helpers/TestModel'
20
- import SyncStore from '@/services/sync/store/index'
21
- import SyncRetry from '@/services/sync/retry'
22
- import SyncRunScope from '@/services/sync/run-scope'
23
- import type { SyncPullResponse } from '@/services/sync/fetch'
24
-
25
- // Fresh IDB instance per test — prevents cross-test transaction leakage
26
- beforeEach(() => {
27
- ;(global as any).indexedDB = new IDBFactory()
28
- })
29
-
30
- function makeStore(overrides: { pull?: any; push?: any; dbName?: string } = {}) {
31
- const db = makeTestDatabase({ useIncrementalIndexedDB: true, dbName: overrides.dbName })
32
- const userScope = makeUserScope()
33
- const context = makeContext()
34
- const telemetry = makeTelemetry(userScope)
35
- const retry = new SyncRetry(context, telemetry)
36
- const runScope = new SyncRunScope()
37
-
38
- const store = new SyncStore<TestModel>(
39
- {
40
- model: TestModel,
41
- pull: overrides.pull ?? makePullMock(),
42
- push: overrides.push ?? makePushMock(),
43
- },
44
- userScope,
45
- context,
46
- db,
47
- retry,
48
- runScope,
49
- telemetry
50
- )
51
-
52
- return { store, db }
53
- }
54
-
55
- // ---
56
-
57
- describe('fetch token persistence (IDB localStorage)', () => {
58
- test('getLastFetchToken returns null before any pull', async () => {
59
- const { store } = makeStore()
60
- const token = await store.getLastFetchToken()
61
- store.destroy()
62
-
63
- expect(token).toBeNull()
64
- })
65
-
66
- test('getLastFetchToken is set after successful pull', async () => {
67
- const expectedToken = 1700000000000
68
- const pull = jest.fn().mockResolvedValue({
69
- ok: true,
70
- entries: [],
71
- token: expectedToken,
72
- intendedUserId: TEST_USER_ID,
73
- previousToken: null,
74
- } as Extract<SyncPullResponse, { ok: true }>)
75
-
76
- const { store } = makeStore({ pull })
77
- await store.pullRecords()
78
- const token = await store.getLastFetchToken()
79
- store.destroy()
80
-
81
- expect(token).toBe(expectedToken)
82
- })
83
-
84
- test('pullCompleted event fires after successful pull', async () => {
85
- const handler = jest.fn()
86
- const { store } = makeStore()
87
- store.on('pullCompleted', handler)
88
-
89
- await store.pullRecords()
90
- store.destroy()
91
-
92
- expect(handler).toHaveBeenCalledTimes(1)
93
- })
94
-
95
- test('failed pull does not set fetch token', async () => {
96
- const pull = jest.fn().mockResolvedValue({ ok: false, failureType: 'fetch', isRetryable: false })
97
-
98
- const { store } = makeStore({ pull })
99
- await store.pullRecords()
100
- const token = await store.getLastFetchToken()
101
- store.destroy()
102
-
103
- expect(token).toBeNull()
104
- })
105
- })
106
-
107
- // ---
108
-
109
- describe('IDB bootstrapping (prior session data loaded into new Database instance)', () => {
110
- const BOOTSTRAP_DB = 'test-bootstrap'
111
-
112
- test('records written locally are readable from a new Database instance', async () => {
113
- const { store: session1 } = makeStore({ dbName: BOOTSTRAP_DB })
114
- await session1.upsertOne('boot-rec-1', r => {
115
- r.value = 'persisted-value'
116
- r.score = 42
117
- })
118
- session1.destroy()
119
-
120
- const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
121
- const records = await session2.readAll()
122
- session2.destroy()
123
-
124
- expect(records).toHaveLength(1)
125
- expect(records[0].id).toBe('boot-rec-1')
126
- expect(records[0].value).toBe('persisted-value')
127
- expect(records[0].score).toBe(42)
128
- })
129
-
130
- test('fetch token written during pull is readable from a new Database instance', async () => {
131
- const TOKEN = 1700000000000
132
- const pull = jest.fn().mockResolvedValue({
133
- ok: true,
134
- entries: [],
135
- token: TOKEN,
136
- intendedUserId: TEST_USER_ID,
137
- previousToken: null,
138
- })
139
-
140
- const { store: session1 } = makeStore({ pull, dbName: BOOTSTRAP_DB })
141
- await session1.pullRecords()
142
- // pull doesn't call ensurePersistence — flush explicitly so IDB has the token
143
- await (session1 as any).ensurePersistence()
144
- session1.destroy()
145
-
146
- const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
147
- const token = await session2.getLastFetchToken()
148
- session2.destroy()
149
-
150
- expect(token).toBe(TOKEN)
151
- })
152
-
153
- // Two sequential upserts — if this flakes with IDB transaction conflicts,
154
- // collapse them into a single upsertSome call instead.
155
- test('multiple records survive a restart', async () => {
156
- const { store: session1 } = makeStore({ dbName: BOOTSTRAP_DB })
157
- await session1.upsertOne('boot-a', r => { r.value = 'alpha'; r.score = 1 })
158
- await session1.upsertOne('boot-b', r => { r.value = 'beta'; r.score = 2 })
159
- session1.destroy()
160
-
161
- const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
162
- const records = await session2.readAll()
163
- session2.destroy()
164
-
165
- expect(records).toHaveLength(2)
166
- const byId = Object.fromEntries(records.map(r => [r.id, r]))
167
- expect(byId['boot-a'].value).toBe('alpha')
168
- expect(byId['boot-b'].value).toBe('beta')
169
- })
170
-
171
- test('new Database instance with no prior IDB data starts empty', async () => {
172
- const { store } = makeStore({ dbName: BOOTSTRAP_DB })
173
- const records = await store.readAll()
174
- const token = await store.getLastFetchToken()
175
- store.destroy()
176
-
177
- expect(records).toHaveLength(0)
178
- expect(token).toBeNull()
179
- })
180
- })