@vue-skuilder/db 0.1.18 → 0.1.21
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.
- package/CLAUDE.md +2 -2
- package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
- package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +735 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +708 -1539
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +8 -23
- package/dist/impl/couch/index.d.ts +8 -23
- package/dist/impl/couch/index.js +723 -1578
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +692 -1552
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +25 -8
- package/dist/impl/static/index.d.ts +25 -8
- package/dist/impl/static/index.js +700 -1400
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +688 -1393
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +71 -63
- package/dist/index.d.ts +71 -63
- package/dist/index.js +1162 -1996
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1124 -1955
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -0
- package/dist/pouch/index.js.map +1 -1
- package/dist/pouch/index.mjs +3 -0
- package/dist/pouch/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -17
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +15 -7
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +136 -52
- package/src/core/navigators/PipelineAssembler.ts +1 -1
- package/src/core/navigators/defaults.ts +84 -0
- package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
- package/src/core/navigators/filters/userGoalStub.ts +136 -0
- package/src/core/navigators/filters/userTagPreference.ts +217 -0
- package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
- package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
- package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
- package/src/core/navigators/generators/types.ts +1 -1
- package/src/core/navigators/index.ts +95 -91
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +35 -91
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +87 -37
- package/src/study/SessionController.ts +122 -202
- package/src/study/SourceMixer.ts +65 -0
- package/src/study/TagFilteredContentSource.ts +49 -92
- package/src/study/index.ts +1 -0
- package/src/study/services/CardHydrationService.ts +165 -81
- package/src/util/dataDirectory.ts +1 -1
- package/src/util/index.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
- package/tests/core/navigators/Pipeline.test.ts +6 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import CompositeGenerator, {
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import CompositeGenerator, {
|
|
3
|
+
AggregationMode,
|
|
4
|
+
} from '../../../src/core/navigators/generators/CompositeGenerator';
|
|
3
5
|
import { ContentNavigator, WeightedCard } from '../../../src/core/navigators/index';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { GeneratorContext } from '../../../src/core/navigators/generators/types';
|
|
7
|
+
import { UserDBInterface } from '../../../src/core/interfaces/userDB';
|
|
8
|
+
import { CourseDBInterface } from '../../../src/core/interfaces/courseDB';
|
|
6
9
|
|
|
7
10
|
// Test helper to create weighted cards with provenance
|
|
8
11
|
function makeWeightedCard(
|
|
@@ -31,39 +34,45 @@ function makeWeightedCard(
|
|
|
31
34
|
|
|
32
35
|
// Mock ContentNavigator for testing
|
|
33
36
|
class MockGenerator extends ContentNavigator {
|
|
37
|
+
name: string = 'MockGenerator';
|
|
34
38
|
private mockWeightedCards: WeightedCard[] = [];
|
|
35
|
-
private mockNewCards: StudySessionNewItem[] = [];
|
|
36
|
-
private mockReviews: (StudySessionReviewItem & ScheduledCard)[] = [];
|
|
37
39
|
|
|
38
40
|
setWeightedCards(cards: WeightedCard[]) {
|
|
39
41
|
this.mockWeightedCards = cards;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
setNewCards(cards: StudySessionNewItem[]) {
|
|
43
|
-
this.mockNewCards = cards;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setReviews(reviews: (StudySessionReviewItem & ScheduledCard)[]) {
|
|
47
|
-
this.mockReviews = reviews;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
51
45
|
return this.mockWeightedCards.slice(0, limit);
|
|
52
46
|
}
|
|
47
|
+
}
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
// Create a mock context for tests
|
|
50
|
+
function createMockContext(): GeneratorContext {
|
|
51
|
+
const mockUser = {
|
|
52
|
+
getCourseRegDoc: vi.fn().mockResolvedValue({
|
|
53
|
+
elo: { global: { score: 1000, count: 10 }, tags: {} },
|
|
54
|
+
}),
|
|
55
|
+
} as unknown as UserDBInterface;
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
57
|
+
const mockCourse = {
|
|
58
|
+
getCourseID: vi.fn().mockReturnValue('test-course'),
|
|
59
|
+
} as unknown as CourseDBInterface;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
user: mockUser,
|
|
63
|
+
course: mockCourse,
|
|
64
|
+
userElo: 1000,
|
|
65
|
+
};
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
describe('CompositeGenerator', () => {
|
|
69
|
+
const mockContext = createMockContext();
|
|
70
|
+
|
|
64
71
|
describe('constructor', () => {
|
|
65
72
|
it('throws error when no generators provided', () => {
|
|
66
|
-
expect(() => new CompositeGenerator([])).toThrow(
|
|
73
|
+
expect(() => new CompositeGenerator([])).toThrow(
|
|
74
|
+
'CompositeGenerator requires at least one generator'
|
|
75
|
+
);
|
|
67
76
|
});
|
|
68
77
|
|
|
69
78
|
it('accepts single generator', () => {
|
|
@@ -87,7 +96,7 @@ describe('CompositeGenerator', () => {
|
|
|
87
96
|
]);
|
|
88
97
|
|
|
89
98
|
const composite = new CompositeGenerator([generator]);
|
|
90
|
-
const result = await composite.getWeightedCards(10);
|
|
99
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
91
100
|
|
|
92
101
|
expect(result).toHaveLength(2);
|
|
93
102
|
expect(result[0].cardId).toBe('card-1');
|
|
@@ -105,7 +114,7 @@ describe('CompositeGenerator', () => {
|
|
|
105
114
|
]);
|
|
106
115
|
|
|
107
116
|
const composite = new CompositeGenerator([generator]);
|
|
108
|
-
const result = await composite.getWeightedCards(2);
|
|
117
|
+
const result = await composite.getWeightedCards(2, mockContext);
|
|
109
118
|
|
|
110
119
|
expect(result).toHaveLength(2);
|
|
111
120
|
expect(result[0].cardId).toBe('card-1');
|
|
@@ -128,7 +137,7 @@ describe('CompositeGenerator', () => {
|
|
|
128
137
|
]);
|
|
129
138
|
|
|
130
139
|
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.AVERAGE);
|
|
131
|
-
const result = await composite.getWeightedCards(10);
|
|
140
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
132
141
|
|
|
133
142
|
// Should have 3 unique cards
|
|
134
143
|
expect(result).toHaveLength(3);
|
|
@@ -148,7 +157,7 @@ describe('CompositeGenerator', () => {
|
|
|
148
157
|
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
149
158
|
|
|
150
159
|
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.MAX);
|
|
151
|
-
const result = await composite.getWeightedCards(10);
|
|
160
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
152
161
|
|
|
153
162
|
expect(result).toHaveLength(1);
|
|
154
163
|
expect(result[0].score).toBe(0.9);
|
|
@@ -164,7 +173,7 @@ describe('CompositeGenerator', () => {
|
|
|
164
173
|
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
165
174
|
|
|
166
175
|
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.AVERAGE);
|
|
167
|
-
const result = await composite.getWeightedCards(10);
|
|
176
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
168
177
|
|
|
169
178
|
expect(result).toHaveLength(1);
|
|
170
179
|
expect(result[0].score).toBeCloseTo(0.7); // (0.8 + 0.6) / 2
|
|
@@ -181,7 +190,7 @@ describe('CompositeGenerator', () => {
|
|
|
181
190
|
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
182
191
|
|
|
183
192
|
const composite = new CompositeGenerator([gen1, gen2, gen3], AggregationMode.AVERAGE);
|
|
184
|
-
const result = await composite.getWeightedCards(10);
|
|
193
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
185
194
|
|
|
186
195
|
expect(result).toHaveLength(1);
|
|
187
196
|
expect(result[0].score).toBeCloseTo(0.7); // (0.9 + 0.6 + 0.6) / 3
|
|
@@ -197,7 +206,7 @@ describe('CompositeGenerator', () => {
|
|
|
197
206
|
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
198
207
|
|
|
199
208
|
const composite = new CompositeGenerator([gen1, gen2]); // default mode
|
|
200
|
-
const result = await composite.getWeightedCards(10);
|
|
209
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
201
210
|
|
|
202
211
|
expect(result).toHaveLength(1);
|
|
203
212
|
// avg = (0.8 + 0.6) / 2 = 0.7
|
|
@@ -217,7 +226,7 @@ describe('CompositeGenerator', () => {
|
|
|
217
226
|
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
218
227
|
|
|
219
228
|
const composite = new CompositeGenerator([gen1, gen2, gen3]);
|
|
220
|
-
const result = await composite.getWeightedCards(10);
|
|
229
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
221
230
|
|
|
222
231
|
expect(result).toHaveLength(1);
|
|
223
232
|
// avg = 0.6
|
|
@@ -234,7 +243,7 @@ describe('CompositeGenerator', () => {
|
|
|
234
243
|
gen2.setWeightedCards([makeWeightedCard('card-2', 'course-1', 0.6, 'new')]);
|
|
235
244
|
|
|
236
245
|
const composite = new CompositeGenerator([gen1, gen2]);
|
|
237
|
-
const result = await composite.getWeightedCards(10);
|
|
246
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
238
247
|
|
|
239
248
|
expect(result).toHaveLength(2);
|
|
240
249
|
// No boost for single-generator cards
|
|
@@ -254,7 +263,7 @@ describe('CompositeGenerator', () => {
|
|
|
254
263
|
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
255
264
|
|
|
256
265
|
const composite = new CompositeGenerator([gen1, gen2]);
|
|
257
|
-
const result = await composite.getWeightedCards(10);
|
|
266
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
258
267
|
|
|
259
268
|
expect(result).toHaveLength(1);
|
|
260
269
|
// avg = 0.9, boost = 1.1, result = 0.99 (would be 0.99, not clamped)
|
|
@@ -273,7 +282,7 @@ describe('CompositeGenerator', () => {
|
|
|
273
282
|
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 1.0, 'new')]);
|
|
274
283
|
|
|
275
284
|
const composite = new CompositeGenerator([gen1, gen2, gen3]);
|
|
276
|
-
const result = await composite.getWeightedCards(10);
|
|
285
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
277
286
|
|
|
278
287
|
expect(result).toHaveLength(1);
|
|
279
288
|
// avg = 1.0, boost = 1.2, result would be 1.2 but clamped to 1.0
|
|
@@ -291,7 +300,7 @@ describe('CompositeGenerator', () => {
|
|
|
291
300
|
]);
|
|
292
301
|
|
|
293
302
|
const composite = new CompositeGenerator([gen1]);
|
|
294
|
-
const result = await composite.getWeightedCards(10);
|
|
303
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
295
304
|
|
|
296
305
|
expect(result).toHaveLength(3);
|
|
297
306
|
expect(result[0].cardId).toBe('card-high');
|
|
@@ -307,12 +316,10 @@ describe('CompositeGenerator', () => {
|
|
|
307
316
|
]);
|
|
308
317
|
|
|
309
318
|
const gen2 = new MockGenerator();
|
|
310
|
-
gen2.setWeightedCards([
|
|
311
|
-
makeWeightedCard('card-boosted', 'course-1', 0.5, 'new'),
|
|
312
|
-
]);
|
|
319
|
+
gen2.setWeightedCards([makeWeightedCard('card-boosted', 'course-1', 0.5, 'new')]);
|
|
313
320
|
|
|
314
321
|
const composite = new CompositeGenerator([gen1, gen2]);
|
|
315
|
-
const result = await composite.getWeightedCards(10);
|
|
322
|
+
const result = await composite.getWeightedCards(10, mockContext);
|
|
316
323
|
|
|
317
324
|
expect(result).toHaveLength(2);
|
|
318
325
|
// card-boosted: avg=0.5, boost=1.1, final=0.55
|
|
@@ -321,135 +328,4 @@ describe('CompositeGenerator', () => {
|
|
|
321
328
|
expect(result[1].cardId).toBe('card-boosted');
|
|
322
329
|
});
|
|
323
330
|
});
|
|
324
|
-
|
|
325
|
-
describe('getNewCards', () => {
|
|
326
|
-
it('deduplicates new cards by cardID', async () => {
|
|
327
|
-
const gen1 = new MockGenerator();
|
|
328
|
-
gen1.setNewCards([
|
|
329
|
-
{
|
|
330
|
-
cardID: 'card-1',
|
|
331
|
-
courseID: 'course-1',
|
|
332
|
-
contentSourceType: 'course',
|
|
333
|
-
contentSourceID: 'course-1',
|
|
334
|
-
status: 'new',
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
cardID: 'card-2',
|
|
338
|
-
courseID: 'course-1',
|
|
339
|
-
contentSourceType: 'course',
|
|
340
|
-
contentSourceID: 'course-1',
|
|
341
|
-
status: 'new',
|
|
342
|
-
},
|
|
343
|
-
]);
|
|
344
|
-
|
|
345
|
-
const gen2 = new MockGenerator();
|
|
346
|
-
gen2.setNewCards([
|
|
347
|
-
{
|
|
348
|
-
cardID: 'card-1',
|
|
349
|
-
courseID: 'course-1',
|
|
350
|
-
contentSourceType: 'course',
|
|
351
|
-
contentSourceID: 'course-1',
|
|
352
|
-
status: 'new',
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
cardID: 'card-3',
|
|
356
|
-
courseID: 'course-1',
|
|
357
|
-
contentSourceType: 'course',
|
|
358
|
-
contentSourceID: 'course-1',
|
|
359
|
-
status: 'new',
|
|
360
|
-
},
|
|
361
|
-
]);
|
|
362
|
-
|
|
363
|
-
const composite = new CompositeGenerator([gen1, gen2]);
|
|
364
|
-
const result = await composite.getNewCards();
|
|
365
|
-
|
|
366
|
-
expect(result).toHaveLength(3);
|
|
367
|
-
const cardIds = result.map((c) => c.cardID);
|
|
368
|
-
expect(cardIds).toContain('card-1');
|
|
369
|
-
expect(cardIds).toContain('card-2');
|
|
370
|
-
expect(cardIds).toContain('card-3');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('respects limit parameter', async () => {
|
|
374
|
-
const gen1 = new MockGenerator();
|
|
375
|
-
gen1.setNewCards([
|
|
376
|
-
{
|
|
377
|
-
cardID: 'card-1',
|
|
378
|
-
courseID: 'course-1',
|
|
379
|
-
contentSourceType: 'course',
|
|
380
|
-
contentSourceID: 'course-1',
|
|
381
|
-
status: 'new',
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
cardID: 'card-2',
|
|
385
|
-
courseID: 'course-1',
|
|
386
|
-
contentSourceType: 'course',
|
|
387
|
-
contentSourceID: 'course-1',
|
|
388
|
-
status: 'new',
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
cardID: 'card-3',
|
|
392
|
-
courseID: 'course-1',
|
|
393
|
-
contentSourceType: 'course',
|
|
394
|
-
contentSourceID: 'course-1',
|
|
395
|
-
status: 'new',
|
|
396
|
-
},
|
|
397
|
-
]);
|
|
398
|
-
|
|
399
|
-
const composite = new CompositeGenerator([gen1]);
|
|
400
|
-
const result = await composite.getNewCards(2);
|
|
401
|
-
|
|
402
|
-
expect(result).toHaveLength(2);
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
describe('getPendingReviews', () => {
|
|
407
|
-
it('deduplicates reviews by cardID', async () => {
|
|
408
|
-
const review1: StudySessionReviewItem & ScheduledCard = {
|
|
409
|
-
cardID: 'card-1',
|
|
410
|
-
courseID: 'course-1',
|
|
411
|
-
contentSourceType: 'course',
|
|
412
|
-
contentSourceID: 'course-1',
|
|
413
|
-
status: 'review',
|
|
414
|
-
reviewID: 'review-1',
|
|
415
|
-
_id: 'scheduled-1',
|
|
416
|
-
cardId: 'card-1',
|
|
417
|
-
courseId: 'course-1',
|
|
418
|
-
scheduledFor: 'course',
|
|
419
|
-
schedulingAgentId: 'agent-1',
|
|
420
|
-
reviewTime: new Date(),
|
|
421
|
-
scheduledAt: new Date(),
|
|
422
|
-
} as unknown as StudySessionReviewItem & ScheduledCard;
|
|
423
|
-
|
|
424
|
-
const review2: StudySessionReviewItem & ScheduledCard = {
|
|
425
|
-
cardID: 'card-2',
|
|
426
|
-
courseID: 'course-1',
|
|
427
|
-
contentSourceType: 'course',
|
|
428
|
-
contentSourceID: 'course-1',
|
|
429
|
-
status: 'review',
|
|
430
|
-
reviewID: 'review-2',
|
|
431
|
-
_id: 'scheduled-2',
|
|
432
|
-
cardId: 'card-2',
|
|
433
|
-
courseId: 'course-1',
|
|
434
|
-
scheduledFor: 'course',
|
|
435
|
-
schedulingAgentId: 'agent-1',
|
|
436
|
-
reviewTime: new Date(),
|
|
437
|
-
scheduledAt: new Date(),
|
|
438
|
-
} as unknown as StudySessionReviewItem & ScheduledCard;
|
|
439
|
-
|
|
440
|
-
const gen1 = new MockGenerator();
|
|
441
|
-
gen1.setReviews([review1, review2]);
|
|
442
|
-
|
|
443
|
-
const gen2 = new MockGenerator();
|
|
444
|
-
gen2.setReviews([review1]); // Duplicate review1
|
|
445
|
-
|
|
446
|
-
const composite = new CompositeGenerator([gen1, gen2]);
|
|
447
|
-
const result = await composite.getPendingReviews();
|
|
448
|
-
|
|
449
|
-
expect(result).toHaveLength(2);
|
|
450
|
-
const cardIds = result.map((c) => c.cardID);
|
|
451
|
-
expect(cardIds).toContain('card-1');
|
|
452
|
-
expect(cardIds).toContain('card-2');
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
331
|
});
|
|
@@ -2,8 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { Pipeline } from '../../../src/core/navigators/Pipeline';
|
|
3
3
|
import { WeightedCard, ContentNavigator } from '../../../src/core/navigators/index';
|
|
4
4
|
import { CardFilter, FilterContext } from '../../../src/core/navigators/filters/types';
|
|
5
|
-
|
|
6
|
-
import { ScheduledCard } from '../../../src/core/types/user';
|
|
5
|
+
|
|
7
6
|
import { CourseDBInterface } from '../../../src/core/interfaces/courseDB';
|
|
8
7
|
import { UserDBInterface } from '../../../src/core/interfaces/userDB';
|
|
9
8
|
|
|
@@ -15,6 +14,7 @@ import { UserDBInterface } from '../../../src/core/interfaces/userDB';
|
|
|
15
14
|
* Mock generator that returns predefined weighted cards
|
|
16
15
|
*/
|
|
17
16
|
class MockGenerator extends ContentNavigator {
|
|
17
|
+
name: string = 'MockGenerator';
|
|
18
18
|
private cards: WeightedCard[];
|
|
19
19
|
|
|
20
20
|
constructor(cards: WeightedCard[]) {
|
|
@@ -25,14 +25,6 @@ class MockGenerator extends ContentNavigator {
|
|
|
25
25
|
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
26
26
|
return this.cards.slice(0, limit);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
/**
|
|
@@ -102,6 +94,8 @@ function createMockContext(): { user: UserDBInterface; course: CourseDBInterface
|
|
|
102
94
|
|
|
103
95
|
const mockCourse = {
|
|
104
96
|
getCourseID: vi.fn().mockReturnValue('test-course'),
|
|
97
|
+
getCourseConfig: vi.fn().mockResolvedValue({ name: 'Test Course', id: 'test-course' }),
|
|
98
|
+
getAppliedTagsBatch: vi.fn().mockResolvedValue(new Map()),
|
|
105
99
|
} as unknown as CourseDBInterface;
|
|
106
100
|
|
|
107
101
|
return { user: mockUser, course: mockCourse };
|
|
@@ -156,11 +150,7 @@ describe('Pipeline', () => {
|
|
|
156
150
|
});
|
|
157
151
|
|
|
158
152
|
it('should sort cards by score descending', async () => {
|
|
159
|
-
const cards = [
|
|
160
|
-
createCard('low', 0.3),
|
|
161
|
-
createCard('high', 0.9),
|
|
162
|
-
createCard('mid', 0.6),
|
|
163
|
-
];
|
|
153
|
+
const cards = [createCard('low', 0.3), createCard('high', 0.9), createCard('mid', 0.6)];
|
|
164
154
|
const generator = new MockGenerator(cards);
|
|
165
155
|
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
166
156
|
|
|
@@ -225,10 +215,7 @@ describe('Pipeline', () => {
|
|
|
225
215
|
});
|
|
226
216
|
|
|
227
217
|
it('should remove zero-score cards after filtering', async () => {
|
|
228
|
-
const cards = [
|
|
229
|
-
createCard('keep', 0.8),
|
|
230
|
-
createCard('block', 0.9),
|
|
231
|
-
];
|
|
218
|
+
const cards = [createCard('keep', 0.8), createCard('block', 0.9)];
|
|
232
219
|
const generator = new MockGenerator(cards);
|
|
233
220
|
const filter = createBlockingFilter('Blocker', ['block']);
|
|
234
221
|
const pipeline = new Pipeline(generator, [filter], mockUser, mockCourse);
|
|
@@ -325,59 +312,6 @@ describe('Pipeline', () => {
|
|
|
325
312
|
});
|
|
326
313
|
});
|
|
327
314
|
|
|
328
|
-
describe('legacy API compatibility', () => {
|
|
329
|
-
it('should delegate getNewCards to generator', async () => {
|
|
330
|
-
const mockNewCards: StudySessionNewItem[] = [
|
|
331
|
-
{
|
|
332
|
-
cardID: 'new-1',
|
|
333
|
-
courseID: 'test-course',
|
|
334
|
-
contentSourceType: 'course',
|
|
335
|
-
contentSourceID: 'test-course',
|
|
336
|
-
status: 'new',
|
|
337
|
-
},
|
|
338
|
-
];
|
|
339
|
-
|
|
340
|
-
const generator = new MockGenerator([]);
|
|
341
|
-
generator.getNewCards = vi.fn().mockResolvedValue(mockNewCards);
|
|
342
|
-
|
|
343
|
-
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
344
|
-
const result = await pipeline.getNewCards(10);
|
|
345
|
-
|
|
346
|
-
expect(generator.getNewCards).toHaveBeenCalledWith(10);
|
|
347
|
-
expect(result).toEqual(mockNewCards);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('should delegate getPendingReviews to generator', async () => {
|
|
351
|
-
const mockReviews: (StudySessionReviewItem & ScheduledCard)[] = [
|
|
352
|
-
{
|
|
353
|
-
cardID: 'review-1',
|
|
354
|
-
courseID: 'test-course',
|
|
355
|
-
contentSourceType: 'course',
|
|
356
|
-
contentSourceID: 'test-course',
|
|
357
|
-
status: 'review',
|
|
358
|
-
qualifiedID: 'test-course-review-1',
|
|
359
|
-
reviewID: 'SCHEDULED_CARD-review-1',
|
|
360
|
-
_id: 'SCHEDULED_CARD-review-1',
|
|
361
|
-
cardId: 'review-1',
|
|
362
|
-
courseId: 'test-course',
|
|
363
|
-
reviewTime: '2024-01-15T12:00:00Z',
|
|
364
|
-
scheduledAt: '2024-01-14T12:00:00Z',
|
|
365
|
-
scheduledFor: 'course',
|
|
366
|
-
schedulingAgentId: 'test-course',
|
|
367
|
-
},
|
|
368
|
-
];
|
|
369
|
-
|
|
370
|
-
const generator = new MockGenerator([]);
|
|
371
|
-
generator.getPendingReviews = vi.fn().mockResolvedValue(mockReviews);
|
|
372
|
-
|
|
373
|
-
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
374
|
-
const result = await pipeline.getPendingReviews();
|
|
375
|
-
|
|
376
|
-
expect(generator.getPendingReviews).toHaveBeenCalled();
|
|
377
|
-
expect(result).toEqual(mockReviews);
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
315
|
describe('over-fetching', () => {
|
|
382
316
|
it('should over-fetch from generator based on filter count', async () => {
|
|
383
317
|
const manyCards = Array.from({ length: 20 }, (_, i) =>
|
|
@@ -8,7 +8,7 @@ import { DocType } from '../../../src/core';
|
|
|
8
8
|
import { Pipeline } from '../../../src/core/navigators/Pipeline';
|
|
9
9
|
|
|
10
10
|
// Mock the dynamic import in ContentNavigator.create
|
|
11
|
-
vi.mock('../../../src/core/navigators/elo', () => ({
|
|
11
|
+
vi.mock('../../../src/core/navigators/generators/elo', () => ({
|
|
12
12
|
default: class MockELONavigator {
|
|
13
13
|
name = 'ELO';
|
|
14
14
|
constructor(
|
|
@@ -19,16 +19,10 @@ vi.mock('../../../src/core/navigators/elo', () => ({
|
|
|
19
19
|
async getWeightedCards() {
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
|
-
async getNewCards() {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
async getPendingReviews() {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
22
|
},
|
|
29
23
|
}));
|
|
30
24
|
|
|
31
|
-
vi.mock('../../../src/core/navigators/srs', () => ({
|
|
25
|
+
vi.mock('../../../src/core/navigators/generators/srs', () => ({
|
|
32
26
|
default: class MockSRSNavigator {
|
|
33
27
|
name = 'SRS';
|
|
34
28
|
constructor(
|
|
@@ -39,36 +33,10 @@ vi.mock('../../../src/core/navigators/srs', () => ({
|
|
|
39
33
|
async getWeightedCards() {
|
|
40
34
|
return [];
|
|
41
35
|
}
|
|
42
|
-
async getNewCards() {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
async getPendingReviews() {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
vi.mock('../../../src/core/navigators/hardcodedOrder', () => ({
|
|
52
|
-
default: class MockHardcodedOrderNavigator {
|
|
53
|
-
name = 'Hardcoded Order';
|
|
54
|
-
constructor(
|
|
55
|
-
public user: any,
|
|
56
|
-
public course: any,
|
|
57
|
-
public strategyData: any
|
|
58
|
-
) {}
|
|
59
|
-
async getWeightedCards() {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
async getNewCards() {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
async getPendingReviews() {
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
36
|
},
|
|
69
37
|
}));
|
|
70
38
|
|
|
71
|
-
vi.mock('../../../src/core/navigators/hierarchyDefinition', () => ({
|
|
39
|
+
vi.mock('../../../src/core/navigators/filters/hierarchyDefinition', () => ({
|
|
72
40
|
default: class MockHierarchyDefinitionNavigator {
|
|
73
41
|
name = 'Hierarchy Definition';
|
|
74
42
|
constructor(
|
|
@@ -84,16 +52,10 @@ vi.mock('../../../src/core/navigators/hierarchyDefinition', () => ({
|
|
|
84
52
|
async getWeightedCards() {
|
|
85
53
|
throw new Error('Filter should not be used as generator');
|
|
86
54
|
}
|
|
87
|
-
async getNewCards() {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
async getPendingReviews() {
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
55
|
},
|
|
94
56
|
}));
|
|
95
57
|
|
|
96
|
-
vi.mock('../../../src/core/navigators/interferenceMitigator', () => ({
|
|
58
|
+
vi.mock('../../../src/core/navigators/filters/interferenceMitigator', () => ({
|
|
97
59
|
default: class MockInterferenceMitigatorNavigator {
|
|
98
60
|
name = 'Interference Mitigator';
|
|
99
61
|
constructor(
|
|
@@ -109,16 +71,10 @@ vi.mock('../../../src/core/navigators/interferenceMitigator', () => ({
|
|
|
109
71
|
async getWeightedCards() {
|
|
110
72
|
throw new Error('Filter should not be used as generator');
|
|
111
73
|
}
|
|
112
|
-
async getNewCards() {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
async getPendingReviews() {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
74
|
},
|
|
119
75
|
}));
|
|
120
76
|
|
|
121
|
-
vi.mock('../../../src/core/navigators/relativePriority', () => ({
|
|
77
|
+
vi.mock('../../../src/core/navigators/filters/relativePriority', () => ({
|
|
122
78
|
default: class MockRelativePriorityNavigator {
|
|
123
79
|
name = 'Relative Priority';
|
|
124
80
|
constructor(
|
|
@@ -134,12 +90,6 @@ vi.mock('../../../src/core/navigators/relativePriority', () => ({
|
|
|
134
90
|
async getWeightedCards() {
|
|
135
91
|
throw new Error('Filter should not be used as generator');
|
|
136
92
|
}
|
|
137
|
-
async getNewCards() {
|
|
138
|
-
return [];
|
|
139
|
-
}
|
|
140
|
-
async getPendingReviews() {
|
|
141
|
-
return [];
|
|
142
|
-
}
|
|
143
93
|
},
|
|
144
94
|
}));
|
|
145
95
|
|
|
@@ -157,6 +107,7 @@ describe('PipelineAssembler', () => {
|
|
|
157
107
|
|
|
158
108
|
const mockCourse = {
|
|
159
109
|
getCourseID: vi.fn().mockReturnValue('test-course'),
|
|
110
|
+
getCourseConfig: vi.fn().mockResolvedValue({ name: 'Test Course', id: 'test-course' }),
|
|
160
111
|
getCardEloData: vi.fn().mockResolvedValue([]),
|
|
161
112
|
getAppliedTags: vi.fn().mockResolvedValue({ rows: [] }),
|
|
162
113
|
};
|
|
@@ -305,15 +256,14 @@ describe('PipelineAssembler', () => {
|
|
|
305
256
|
describe('complex scenarios', () => {
|
|
306
257
|
it('handles multiple generators and multiple filters', async () => {
|
|
307
258
|
const elo = createStrategy('elo', 'elo');
|
|
308
|
-
const hardcoded = createStrategy('hardcoded', 'hardcodedOrder');
|
|
309
259
|
const hierarchy = createStrategy('hierarchy', 'hierarchyDefinition');
|
|
310
260
|
const relativePriority = createStrategy('priority', 'relativePriority');
|
|
311
261
|
|
|
312
|
-
const input = createInput([elo,
|
|
262
|
+
const input = createInput([elo, hierarchy, relativePriority]);
|
|
313
263
|
const result = await assembler.assemble(input);
|
|
314
264
|
|
|
315
265
|
// Should have both generators
|
|
316
|
-
expect(result.generatorStrategies).toEqual([elo
|
|
266
|
+
expect(result.generatorStrategies).toEqual([elo]);
|
|
317
267
|
|
|
318
268
|
// Should have both filters (sorted alphabetically)
|
|
319
269
|
expect(result.filterStrategies.map((f) => f.name)).toEqual(['hierarchy', 'priority']);
|