@vue-skuilder/db 0.1.16 → 0.1.18
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/dist/{userDB-DNa0XPtn.d.ts → classroomDB-BgfrVb8d.d.ts} +357 -103
- package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CTOenngH.d.cts} +358 -104
- package/dist/core/index.d.cts +230 -0
- package/dist/core/index.d.ts +161 -23
- package/dist/core/index.js +1964 -154
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1925 -121
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-CZxC9GtB.d.ts} +1 -1
- package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D6PoCwS6.d.cts} +1 -1
- package/dist/impl/couch/{index.d.mts → index.d.cts} +46 -5
- package/dist/impl/couch/index.d.ts +44 -3
- package/dist/impl/couch/index.js +1971 -171
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +1933 -134
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/{index.d.mts → index.d.cts} +5 -6
- package/dist/impl/static/index.d.ts +2 -3
- package/dist/impl/static/index.js +1614 -119
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +1585 -92
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Bmll7Xse.d.mts → index-D-Fa4Smt.d.cts} +1 -1
- package/dist/{index.d.mts → index.d.cts} +97 -13
- package/dist/index.d.ts +90 -6
- package/dist/index.js +2085 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2031 -106
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -3
- package/dist/{types-Dbp5DaRR.d.mts → types-CzPDLAK6.d.cts} +1 -1
- package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
- package/dist/util/packer/index.js.map +1 -1
- package/dist/util/packer/index.mjs.map +1 -1
- package/docs/brainstorm-navigation-paradigm.md +369 -0
- package/docs/navigators-architecture.md +265 -0
- package/docs/todo-evolutionary-orchestration.md +310 -0
- package/docs/todo-nominal-tag-types.md +121 -0
- package/docs/todo-pipeline-optimization.md +117 -0
- package/docs/todo-strategy-authoring.md +401 -0
- package/docs/todo-strategy-state-storage.md +278 -0
- package/eslint.config.mjs +1 -1
- package/package.json +9 -4
- package/src/core/interfaces/contentSource.ts +88 -4
- package/src/core/interfaces/navigationStrategyManager.ts +0 -5
- package/src/core/navigators/CompositeGenerator.ts +268 -0
- package/src/core/navigators/Pipeline.ts +205 -0
- package/src/core/navigators/PipelineAssembler.ts +194 -0
- package/src/core/navigators/elo.ts +104 -15
- package/src/core/navigators/filters/eloDistance.ts +132 -0
- package/src/core/navigators/filters/index.ts +6 -0
- package/src/core/navigators/filters/types.ts +115 -0
- package/src/core/navigators/generators/index.ts +2 -0
- package/src/core/navigators/generators/types.ts +107 -0
- package/src/core/navigators/hardcodedOrder.ts +111 -12
- package/src/core/navigators/hierarchyDefinition.ts +266 -0
- package/src/core/navigators/index.ts +345 -3
- package/src/core/navigators/interferenceMitigator.ts +367 -0
- package/src/core/navigators/relativePriority.ts +267 -0
- package/src/core/navigators/srs.ts +195 -0
- package/src/impl/couch/classroomDB.ts +51 -0
- package/src/impl/couch/courseDB.ts +117 -39
- package/src/impl/static/courseDB.ts +0 -4
- package/src/study/SessionController.ts +149 -1
- package/src/study/TagFilteredContentSource.ts +255 -0
- package/src/study/index.ts +1 -0
- package/src/util/dataDirectory.test.ts +51 -22
- package/src/util/logger.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
- package/tests/core/navigators/Pipeline.test.ts +405 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
- package/tests/core/navigators/SRSNavigator.test.ts +344 -0
- package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
- package/tests/core/navigators/navigators.test.ts +710 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +29 -0
- package/dist/core/index.d.mts +0 -92
- /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
- /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
- /package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-6ettoclI.d.cts} +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Pipeline } from '../../../src/core/navigators/Pipeline';
|
|
3
|
+
import { WeightedCard, ContentNavigator } from '../../../src/core/navigators/index';
|
|
4
|
+
import { CardFilter, FilterContext } from '../../../src/core/navigators/filters/types';
|
|
5
|
+
import { StudySessionNewItem, StudySessionReviewItem } from '../../../src/core/interfaces/contentSource';
|
|
6
|
+
import { ScheduledCard } from '../../../src/core/types/user';
|
|
7
|
+
import { CourseDBInterface } from '../../../src/core/interfaces/courseDB';
|
|
8
|
+
import { UserDBInterface } from '../../../src/core/interfaces/userDB';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// MOCK IMPLEMENTATIONS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mock generator that returns predefined weighted cards
|
|
16
|
+
*/
|
|
17
|
+
class MockGenerator extends ContentNavigator {
|
|
18
|
+
private cards: WeightedCard[];
|
|
19
|
+
|
|
20
|
+
constructor(cards: WeightedCard[]) {
|
|
21
|
+
super();
|
|
22
|
+
this.cards = cards;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
26
|
+
return this.cards.slice(0, limit);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a mock filter that multiplies scores by a fixed factor
|
|
40
|
+
*/
|
|
41
|
+
function createMultiplierFilter(name: string, multiplier: number): CardFilter {
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
|
|
45
|
+
return cards.map((card) => ({
|
|
46
|
+
...card,
|
|
47
|
+
score: card.score * multiplier,
|
|
48
|
+
provenance: [
|
|
49
|
+
...card.provenance,
|
|
50
|
+
{
|
|
51
|
+
strategy: name.toLowerCase().replace(/\s+/g, '-'),
|
|
52
|
+
strategyName: name,
|
|
53
|
+
strategyId: `FILTER-${name.toUpperCase()}`,
|
|
54
|
+
action: multiplier < 1 ? 'penalized' : multiplier > 1 ? 'boosted' : 'passed',
|
|
55
|
+
score: card.score * multiplier,
|
|
56
|
+
reason: `Multiplied by ${multiplier}`,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a mock filter that zeros out specific cards
|
|
66
|
+
*/
|
|
67
|
+
function createBlockingFilter(name: string, blockedCardIds: string[]): CardFilter {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
|
|
71
|
+
return cards.map((card) => {
|
|
72
|
+
const blocked = blockedCardIds.includes(card.cardId);
|
|
73
|
+
return {
|
|
74
|
+
...card,
|
|
75
|
+
score: blocked ? 0 : card.score,
|
|
76
|
+
provenance: [
|
|
77
|
+
...card.provenance,
|
|
78
|
+
{
|
|
79
|
+
strategy: 'blocker',
|
|
80
|
+
strategyName: name,
|
|
81
|
+
strategyId: `FILTER-BLOCKER`,
|
|
82
|
+
action: blocked ? 'penalized' : 'passed',
|
|
83
|
+
score: blocked ? 0 : card.score,
|
|
84
|
+
reason: blocked ? 'Blocked' : 'Passed',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create mock user and course interfaces
|
|
95
|
+
*/
|
|
96
|
+
function createMockContext(): { user: UserDBInterface; course: CourseDBInterface } {
|
|
97
|
+
const mockUser = {
|
|
98
|
+
getCourseRegDoc: vi.fn().mockResolvedValue({
|
|
99
|
+
elo: { global: { score: 1000, count: 10 }, tags: {} },
|
|
100
|
+
}),
|
|
101
|
+
} as unknown as UserDBInterface;
|
|
102
|
+
|
|
103
|
+
const mockCourse = {
|
|
104
|
+
getCourseID: vi.fn().mockReturnValue('test-course'),
|
|
105
|
+
} as unknown as CourseDBInterface;
|
|
106
|
+
|
|
107
|
+
return { user: mockUser, course: mockCourse };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Helper to create a weighted card
|
|
112
|
+
*/
|
|
113
|
+
function createCard(id: string, score: number): WeightedCard {
|
|
114
|
+
return {
|
|
115
|
+
cardId: id,
|
|
116
|
+
courseId: 'test-course',
|
|
117
|
+
score,
|
|
118
|
+
provenance: [
|
|
119
|
+
{
|
|
120
|
+
strategy: 'test-generator',
|
|
121
|
+
strategyName: 'Test Generator',
|
|
122
|
+
strategyId: 'TEST_GENERATOR',
|
|
123
|
+
action: 'generated',
|
|
124
|
+
score,
|
|
125
|
+
reason: 'Test card, new',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// TESTS
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
describe('Pipeline', () => {
|
|
136
|
+
let mockUser: UserDBInterface;
|
|
137
|
+
let mockCourse: CourseDBInterface;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
const context = createMockContext();
|
|
141
|
+
mockUser = context.user;
|
|
142
|
+
mockCourse = context.course;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('basic functionality', () => {
|
|
146
|
+
it('should return cards from generator when no filters', async () => {
|
|
147
|
+
const cards = [createCard('card-1', 0.8), createCard('card-2', 0.6)];
|
|
148
|
+
const generator = new MockGenerator(cards);
|
|
149
|
+
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
150
|
+
|
|
151
|
+
const result = await pipeline.getWeightedCards(10);
|
|
152
|
+
|
|
153
|
+
expect(result).toHaveLength(2);
|
|
154
|
+
expect(result[0].cardId).toBe('card-1');
|
|
155
|
+
expect(result[1].cardId).toBe('card-2');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
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
|
+
];
|
|
164
|
+
const generator = new MockGenerator(cards);
|
|
165
|
+
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
166
|
+
|
|
167
|
+
const result = await pipeline.getWeightedCards(10);
|
|
168
|
+
|
|
169
|
+
expect(result[0].cardId).toBe('high');
|
|
170
|
+
expect(result[1].cardId).toBe('mid');
|
|
171
|
+
expect(result[2].cardId).toBe('low');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should respect limit parameter', async () => {
|
|
175
|
+
const cards = [
|
|
176
|
+
createCard('card-1', 0.9),
|
|
177
|
+
createCard('card-2', 0.8),
|
|
178
|
+
createCard('card-3', 0.7),
|
|
179
|
+
createCard('card-4', 0.6),
|
|
180
|
+
];
|
|
181
|
+
const generator = new MockGenerator(cards);
|
|
182
|
+
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
183
|
+
|
|
184
|
+
const result = await pipeline.getWeightedCards(2);
|
|
185
|
+
|
|
186
|
+
expect(result).toHaveLength(2);
|
|
187
|
+
expect(result[0].cardId).toBe('card-1');
|
|
188
|
+
expect(result[1].cardId).toBe('card-2');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return empty array when generator returns no cards', async () => {
|
|
192
|
+
const generator = new MockGenerator([]);
|
|
193
|
+
const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
|
|
194
|
+
|
|
195
|
+
const result = await pipeline.getWeightedCards(10);
|
|
196
|
+
|
|
197
|
+
expect(result).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('filter application', () => {
|
|
202
|
+
it('should apply single filter to all cards', async () => {
|
|
203
|
+
const cards = [createCard('card-1', 1.0), createCard('card-2', 0.8)];
|
|
204
|
+
const generator = new MockGenerator(cards);
|
|
205
|
+
const filter = createMultiplierFilter('Half', 0.5);
|
|
206
|
+
const pipeline = new Pipeline(generator, [filter], mockUser, mockCourse);
|
|
207
|
+
|
|
208
|
+
const result = await pipeline.getWeightedCards(10);
|
|
209
|
+
|
|
210
|
+
expect(result[0].score).toBe(0.5); // 1.0 * 0.5
|
|
211
|
+
expect(result[1].score).toBe(0.4); // 0.8 * 0.5
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should apply multiple filters sequentially', async () => {
|
|
215
|
+
const cards = [createCard('card-1', 1.0)];
|
|
216
|
+
const generator = new MockGenerator(cards);
|
|
217
|
+
const filter1 = createMultiplierFilter('Half', 0.5);
|
|
218
|
+
const filter2 = createMultiplierFilter('Double', 2.0);
|
|
219
|
+
const pipeline = new Pipeline(generator, [filter1, filter2], mockUser, mockCourse);
|
|
220
|
+
|
|
221
|
+
const result = await pipeline.getWeightedCards(10);
|
|
222
|
+
|
|
223
|
+
// 1.0 * 0.5 * 2.0 = 1.0
|
|
224
|
+
expect(result[0].score).toBe(1.0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should remove zero-score cards after filtering', async () => {
|
|
228
|
+
const cards = [
|
|
229
|
+
createCard('keep', 0.8),
|
|
230
|
+
createCard('block', 0.9),
|
|
231
|
+
];
|
|
232
|
+
const generator = new MockGenerator(cards);
|
|
233
|
+
const filter = createBlockingFilter('Blocker', ['block']);
|
|
234
|
+
const pipeline = new Pipeline(generator, [filter], mockUser, mockCourse);
|
|
235
|
+
|
|
236
|
+
const result = await pipeline.getWeightedCards(10);
|
|
237
|
+
|
|
238
|
+
expect(result).toHaveLength(1);
|
|
239
|
+
expect(result[0].cardId).toBe('keep');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should accumulate provenance from all filters', async () => {
|
|
243
|
+
const cards = [createCard('card-1', 1.0)];
|
|
244
|
+
const generator = new MockGenerator(cards);
|
|
245
|
+
const filter1 = createMultiplierFilter('Filter A', 0.9);
|
|
246
|
+
const filter2 = createMultiplierFilter('Filter B', 0.8);
|
|
247
|
+
const pipeline = new Pipeline(generator, [filter1, filter2], mockUser, mockCourse);
|
|
248
|
+
|
|
249
|
+
const result = await pipeline.getWeightedCards(10);
|
|
250
|
+
|
|
251
|
+
expect(result[0].provenance).toHaveLength(3); // generator + 2 filters
|
|
252
|
+
expect(result[0].provenance[0].strategyName).toBe('Test Generator');
|
|
253
|
+
expect(result[0].provenance[1].strategyName).toBe('Filter A');
|
|
254
|
+
expect(result[0].provenance[2].strategyName).toBe('Filter B');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('filter order independence (multipliers)', () => {
|
|
259
|
+
it('should produce same final score regardless of filter order', async () => {
|
|
260
|
+
const cards = [createCard('card-1', 1.0)];
|
|
261
|
+
|
|
262
|
+
const filterA = createMultiplierFilter('A', 0.5);
|
|
263
|
+
const filterB = createMultiplierFilter('B', 0.8);
|
|
264
|
+
|
|
265
|
+
// Order: A then B
|
|
266
|
+
const generator1 = new MockGenerator([...cards]);
|
|
267
|
+
const pipeline1 = new Pipeline(generator1, [filterA, filterB], mockUser, mockCourse);
|
|
268
|
+
const result1 = await pipeline1.getWeightedCards(10);
|
|
269
|
+
|
|
270
|
+
// Order: B then A
|
|
271
|
+
const generator2 = new MockGenerator([...cards]);
|
|
272
|
+
const pipeline2 = new Pipeline(generator2, [filterB, filterA], mockUser, mockCourse);
|
|
273
|
+
const result2 = await pipeline2.getWeightedCards(10);
|
|
274
|
+
|
|
275
|
+
// Both should yield 1.0 * 0.5 * 0.8 = 0.4
|
|
276
|
+
expect(result1[0].score).toBeCloseTo(0.4);
|
|
277
|
+
expect(result2[0].score).toBeCloseTo(0.4);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('context building', () => {
|
|
282
|
+
it('should pass context to filters', async () => {
|
|
283
|
+
const cards = [createCard('card-1', 0.8)];
|
|
284
|
+
const generator = new MockGenerator(cards);
|
|
285
|
+
|
|
286
|
+
let capturedContext: FilterContext | null = null;
|
|
287
|
+
const contextCapturingFilter: CardFilter = {
|
|
288
|
+
name: 'Context Capturer',
|
|
289
|
+
async transform(cards, context) {
|
|
290
|
+
capturedContext = context;
|
|
291
|
+
return cards;
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const pipeline = new Pipeline(generator, [contextCapturingFilter], mockUser, mockCourse);
|
|
296
|
+
await pipeline.getWeightedCards(10);
|
|
297
|
+
|
|
298
|
+
expect(capturedContext).not.toBeNull();
|
|
299
|
+
expect(capturedContext!.user).toBe(mockUser);
|
|
300
|
+
expect(capturedContext!.course).toBe(mockCourse);
|
|
301
|
+
expect(capturedContext!.userElo).toBe(1000);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should use default ELO when user registration fails', async () => {
|
|
305
|
+
const cards = [createCard('card-1', 0.8)];
|
|
306
|
+
const generator = new MockGenerator(cards);
|
|
307
|
+
|
|
308
|
+
const failingUser = {
|
|
309
|
+
getCourseRegDoc: vi.fn().mockRejectedValue(new Error('Not registered')),
|
|
310
|
+
} as unknown as UserDBInterface;
|
|
311
|
+
|
|
312
|
+
let capturedElo = 0;
|
|
313
|
+
const eloCapturingFilter: CardFilter = {
|
|
314
|
+
name: 'ELO Capturer',
|
|
315
|
+
async transform(cards, context) {
|
|
316
|
+
capturedElo = context.userElo;
|
|
317
|
+
return cards;
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const pipeline = new Pipeline(generator, [eloCapturingFilter], failingUser, mockCourse);
|
|
322
|
+
await pipeline.getWeightedCards(10);
|
|
323
|
+
|
|
324
|
+
expect(capturedElo).toBe(1000); // Default ELO
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
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
|
+
describe('over-fetching', () => {
|
|
382
|
+
it('should over-fetch from generator based on filter count', async () => {
|
|
383
|
+
const manyCards = Array.from({ length: 20 }, (_, i) =>
|
|
384
|
+
createCard(`card-${i}`, 0.5 + Math.random() * 0.5)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const generator = new MockGenerator(manyCards);
|
|
388
|
+
const getWeightedCardsSpy = vi.spyOn(generator, 'getWeightedCards');
|
|
389
|
+
|
|
390
|
+
const filters = [
|
|
391
|
+
createMultiplierFilter('F1', 0.9),
|
|
392
|
+
createMultiplierFilter('F2', 0.9),
|
|
393
|
+
createMultiplierFilter('F3', 0.9),
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const pipeline = new Pipeline(generator, filters, mockUser, mockCourse);
|
|
397
|
+
await pipeline.getWeightedCards(5);
|
|
398
|
+
|
|
399
|
+
// With 3 filters, multiplier is 2 + 3*0.5 = 3.5, so fetch 5 * 3.5 = 17.5 → 18
|
|
400
|
+
const fetchLimit = getWeightedCardsSpy.mock.calls[0][0];
|
|
401
|
+
expect(fetchLimit).toBeGreaterThan(5);
|
|
402
|
+
expect(fetchLimit).toBeLessThanOrEqual(20);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|