@vue-skuilder/db 0.1.17 → 0.1.20
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-BqwxtJ_7.d.mts → classroomDB-CZdMBiTU.d.ts} +427 -104
- package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-PxDZTky3.d.cts} +427 -104
- package/dist/core/index.d.cts +304 -0
- package/dist/core/index.d.ts +237 -25
- package/dist/core/index.js +2246 -118
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2235 -114
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
- package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
- package/dist/impl/couch/{index.d.mts → index.d.cts} +47 -5
- package/dist/impl/couch/index.d.ts +46 -4
- package/dist/impl/couch/index.js +2250 -134
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2212 -97
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/{index.d.mts → index.d.cts} +6 -6
- package/dist/impl/static/index.d.ts +5 -5
- package/dist/impl/static/index.js +1950 -143
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +1922 -117
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Bmll7Xse.d.mts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/{index.d.mts → index.d.cts} +97 -13
- package/dist/index.d.ts +96 -12
- package/dist/index.js +2439 -180
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2386 -135
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -3
- package/dist/{types-Dbp5DaRR.d.mts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
- package/dist/util/packer/index.d.ts +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 +370 -0
- package/docs/todo-evolutionary-orchestration.md +310 -0
- package/docs/todo-nominal-tag-types.md +121 -0
- package/docs/todo-strategy-authoring.md +401 -0
- package/eslint.config.mjs +1 -1
- package/package.json +9 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/contentSource.ts +88 -4
- package/src/core/interfaces/courseDB.ts +13 -0
- package/src/core/interfaces/navigationStrategyManager.ts +0 -5
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/CompositeGenerator.ts +268 -0
- package/src/core/navigators/Pipeline.ts +318 -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 +9 -0
- package/src/core/navigators/filters/types.ts +115 -0
- package/src/core/navigators/filters/userTagPreference.ts +232 -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 +404 -3
- package/src/core/navigators/inferredPreference.ts +107 -0
- package/src/core/navigators/interferenceMitigator.ts +355 -0
- package/src/core/navigators/relativePriority.ts +255 -0
- package/src/core/navigators/srs.ts +195 -0
- package/src/core/navigators/userGoal.ts +136 -0
- 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 +51 -0
- package/src/impl/couch/courseDB.ts +147 -49
- package/src/impl/static/courseDB.ts +11 -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 +406 -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
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import CompositeGenerator, { AggregationMode } from '../../../src/core/navigators/CompositeGenerator';
|
|
3
|
+
import { ContentNavigator, WeightedCard } from '../../../src/core/navigators/index';
|
|
4
|
+
import { StudySessionNewItem, StudySessionReviewItem } from '../../../src/core';
|
|
5
|
+
import { ScheduledCard } from '../../../src/core/types/user';
|
|
6
|
+
|
|
7
|
+
// Test helper to create weighted cards with provenance
|
|
8
|
+
function makeWeightedCard(
|
|
9
|
+
cardId: string,
|
|
10
|
+
courseId: string,
|
|
11
|
+
score: number,
|
|
12
|
+
origin: 'new' | 'review' | 'failed' = 'new',
|
|
13
|
+
strategy: string = 'test'
|
|
14
|
+
): WeightedCard {
|
|
15
|
+
return {
|
|
16
|
+
cardId,
|
|
17
|
+
courseId,
|
|
18
|
+
score,
|
|
19
|
+
provenance: [
|
|
20
|
+
{
|
|
21
|
+
strategy,
|
|
22
|
+
strategyName: 'Test Strategy',
|
|
23
|
+
strategyId: 'TEST_STRATEGY',
|
|
24
|
+
action: 'generated',
|
|
25
|
+
score,
|
|
26
|
+
reason: `Test card, ${origin}`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Mock ContentNavigator for testing
|
|
33
|
+
class MockGenerator extends ContentNavigator {
|
|
34
|
+
private mockWeightedCards: WeightedCard[] = [];
|
|
35
|
+
private mockNewCards: StudySessionNewItem[] = [];
|
|
36
|
+
private mockReviews: (StudySessionReviewItem & ScheduledCard)[] = [];
|
|
37
|
+
|
|
38
|
+
setWeightedCards(cards: WeightedCard[]) {
|
|
39
|
+
this.mockWeightedCards = cards;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setNewCards(cards: StudySessionNewItem[]) {
|
|
43
|
+
this.mockNewCards = cards;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setReviews(reviews: (StudySessionReviewItem & ScheduledCard)[]) {
|
|
47
|
+
this.mockReviews = reviews;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
51
|
+
return this.mockWeightedCards.slice(0, limit);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getNewCards(n?: number): Promise<StudySessionNewItem[]> {
|
|
55
|
+
return n ? this.mockNewCards.slice(0, n) : this.mockNewCards;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
59
|
+
return this.mockReviews;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('CompositeGenerator', () => {
|
|
64
|
+
describe('constructor', () => {
|
|
65
|
+
it('throws error when no generators provided', () => {
|
|
66
|
+
expect(() => new CompositeGenerator([])).toThrow('CompositeGenerator requires at least one generator');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('accepts single generator', () => {
|
|
70
|
+
const generator = new MockGenerator();
|
|
71
|
+
expect(() => new CompositeGenerator([generator])).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts multiple generators', () => {
|
|
75
|
+
const gen1 = new MockGenerator();
|
|
76
|
+
const gen2 = new MockGenerator();
|
|
77
|
+
expect(() => new CompositeGenerator([gen1, gen2])).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getWeightedCards - single generator', () => {
|
|
82
|
+
it('returns cards from single generator unchanged', async () => {
|
|
83
|
+
const generator = new MockGenerator();
|
|
84
|
+
generator.setWeightedCards([
|
|
85
|
+
makeWeightedCard('card-1', 'course-1', 0.8, 'new'),
|
|
86
|
+
makeWeightedCard('card-2', 'course-1', 0.6, 'review'),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const composite = new CompositeGenerator([generator]);
|
|
90
|
+
const result = await composite.getWeightedCards(10);
|
|
91
|
+
|
|
92
|
+
expect(result).toHaveLength(2);
|
|
93
|
+
expect(result[0].cardId).toBe('card-1');
|
|
94
|
+
expect(result[0].score).toBe(0.8);
|
|
95
|
+
expect(result[1].cardId).toBe('card-2');
|
|
96
|
+
expect(result[1].score).toBe(0.6);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('respects limit parameter', async () => {
|
|
100
|
+
const generator = new MockGenerator();
|
|
101
|
+
generator.setWeightedCards([
|
|
102
|
+
makeWeightedCard('card-1', 'course-1', 0.9, 'new'),
|
|
103
|
+
makeWeightedCard('card-2', 'course-1', 0.8, 'new'),
|
|
104
|
+
makeWeightedCard('card-3', 'course-1', 0.7, 'new'),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const composite = new CompositeGenerator([generator]);
|
|
108
|
+
const result = await composite.getWeightedCards(2);
|
|
109
|
+
|
|
110
|
+
expect(result).toHaveLength(2);
|
|
111
|
+
expect(result[0].cardId).toBe('card-1');
|
|
112
|
+
expect(result[1].cardId).toBe('card-2');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('getWeightedCards - multiple generators with deduplication', () => {
|
|
117
|
+
it('deduplicates cards appearing in multiple generators', async () => {
|
|
118
|
+
const gen1 = new MockGenerator();
|
|
119
|
+
gen1.setWeightedCards([
|
|
120
|
+
makeWeightedCard('card-1', 'course-1', 0.8, 'new'),
|
|
121
|
+
makeWeightedCard('card-2', 'course-1', 0.6, 'new'),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const gen2 = new MockGenerator();
|
|
125
|
+
gen2.setWeightedCards([
|
|
126
|
+
makeWeightedCard('card-1', 'course-1', 0.7, 'new'),
|
|
127
|
+
makeWeightedCard('card-3', 'course-1', 0.5, 'new'),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.AVERAGE);
|
|
131
|
+
const result = await composite.getWeightedCards(10);
|
|
132
|
+
|
|
133
|
+
// Should have 3 unique cards
|
|
134
|
+
expect(result).toHaveLength(3);
|
|
135
|
+
const cardIds = result.map((c) => c.cardId);
|
|
136
|
+
expect(cardIds).toContain('card-1');
|
|
137
|
+
expect(cardIds).toContain('card-2');
|
|
138
|
+
expect(cardIds).toContain('card-3');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('aggregation mode: MAX', () => {
|
|
143
|
+
it('uses maximum score from any generator', async () => {
|
|
144
|
+
const gen1 = new MockGenerator();
|
|
145
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
146
|
+
|
|
147
|
+
const gen2 = new MockGenerator();
|
|
148
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
149
|
+
|
|
150
|
+
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.MAX);
|
|
151
|
+
const result = await composite.getWeightedCards(10);
|
|
152
|
+
|
|
153
|
+
expect(result).toHaveLength(1);
|
|
154
|
+
expect(result[0].score).toBe(0.9);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('aggregation mode: AVERAGE', () => {
|
|
159
|
+
it('averages scores from multiple generators', async () => {
|
|
160
|
+
const gen1 = new MockGenerator();
|
|
161
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.8, 'new')]);
|
|
162
|
+
|
|
163
|
+
const gen2 = new MockGenerator();
|
|
164
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
165
|
+
|
|
166
|
+
const composite = new CompositeGenerator([gen1, gen2], AggregationMode.AVERAGE);
|
|
167
|
+
const result = await composite.getWeightedCards(10);
|
|
168
|
+
|
|
169
|
+
expect(result).toHaveLength(1);
|
|
170
|
+
expect(result[0].score).toBeCloseTo(0.7); // (0.8 + 0.6) / 2
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('averages scores from three generators', async () => {
|
|
174
|
+
const gen1 = new MockGenerator();
|
|
175
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
176
|
+
|
|
177
|
+
const gen2 = new MockGenerator();
|
|
178
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
179
|
+
|
|
180
|
+
const gen3 = new MockGenerator();
|
|
181
|
+
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
182
|
+
|
|
183
|
+
const composite = new CompositeGenerator([gen1, gen2, gen3], AggregationMode.AVERAGE);
|
|
184
|
+
const result = await composite.getWeightedCards(10);
|
|
185
|
+
|
|
186
|
+
expect(result).toHaveLength(1);
|
|
187
|
+
expect(result[0].score).toBeCloseTo(0.7); // (0.9 + 0.6 + 0.6) / 3
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('aggregation mode: FREQUENCY_BOOST (default)', () => {
|
|
192
|
+
it('applies frequency boost for cards appearing in multiple generators', async () => {
|
|
193
|
+
const gen1 = new MockGenerator();
|
|
194
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.8, 'new')]);
|
|
195
|
+
|
|
196
|
+
const gen2 = new MockGenerator();
|
|
197
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
198
|
+
|
|
199
|
+
const composite = new CompositeGenerator([gen1, gen2]); // default mode
|
|
200
|
+
const result = await composite.getWeightedCards(10);
|
|
201
|
+
|
|
202
|
+
expect(result).toHaveLength(1);
|
|
203
|
+
// avg = (0.8 + 0.6) / 2 = 0.7
|
|
204
|
+
// boost = 1 + 0.1 * (2 - 1) = 1.1
|
|
205
|
+
// final = 0.7 * 1.1 = 0.77
|
|
206
|
+
expect(result[0].score).toBeCloseTo(0.77);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('applies larger boost for cards in three generators', async () => {
|
|
210
|
+
const gen1 = new MockGenerator();
|
|
211
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
212
|
+
|
|
213
|
+
const gen2 = new MockGenerator();
|
|
214
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
215
|
+
|
|
216
|
+
const gen3 = new MockGenerator();
|
|
217
|
+
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.6, 'new')]);
|
|
218
|
+
|
|
219
|
+
const composite = new CompositeGenerator([gen1, gen2, gen3]);
|
|
220
|
+
const result = await composite.getWeightedCards(10);
|
|
221
|
+
|
|
222
|
+
expect(result).toHaveLength(1);
|
|
223
|
+
// avg = 0.6
|
|
224
|
+
// boost = 1 + 0.1 * (3 - 1) = 1.2
|
|
225
|
+
// final = 0.6 * 1.2 = 0.72
|
|
226
|
+
expect(result[0].score).toBeCloseTo(0.72);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('does not boost cards appearing in only one generator', async () => {
|
|
230
|
+
const gen1 = new MockGenerator();
|
|
231
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.8, 'new')]);
|
|
232
|
+
|
|
233
|
+
const gen2 = new MockGenerator();
|
|
234
|
+
gen2.setWeightedCards([makeWeightedCard('card-2', 'course-1', 0.6, 'new')]);
|
|
235
|
+
|
|
236
|
+
const composite = new CompositeGenerator([gen1, gen2]);
|
|
237
|
+
const result = await composite.getWeightedCards(10);
|
|
238
|
+
|
|
239
|
+
expect(result).toHaveLength(2);
|
|
240
|
+
// No boost for single-generator cards
|
|
241
|
+
const card1 = result.find((c) => c.cardId === 'card-1');
|
|
242
|
+
const card2 = result.find((c) => c.cardId === 'card-2');
|
|
243
|
+
expect(card1!.score).toBe(0.8);
|
|
244
|
+
expect(card2!.score).toBe(0.6);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('score clamping', () => {
|
|
249
|
+
it('clamps boosted scores to maximum of 1.0', async () => {
|
|
250
|
+
const gen1 = new MockGenerator();
|
|
251
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
252
|
+
|
|
253
|
+
const gen2 = new MockGenerator();
|
|
254
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 0.9, 'new')]);
|
|
255
|
+
|
|
256
|
+
const composite = new CompositeGenerator([gen1, gen2]);
|
|
257
|
+
const result = await composite.getWeightedCards(10);
|
|
258
|
+
|
|
259
|
+
expect(result).toHaveLength(1);
|
|
260
|
+
// avg = 0.9, boost = 1.1, result = 0.99 (would be 0.99, not clamped)
|
|
261
|
+
// But with higher scores:
|
|
262
|
+
expect(result[0].score).toBeLessThanOrEqual(1.0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('clamps to 1.0 when boosted score exceeds maximum', async () => {
|
|
266
|
+
const gen1 = new MockGenerator();
|
|
267
|
+
gen1.setWeightedCards([makeWeightedCard('card-1', 'course-1', 1.0, 'new')]);
|
|
268
|
+
|
|
269
|
+
const gen2 = new MockGenerator();
|
|
270
|
+
gen2.setWeightedCards([makeWeightedCard('card-1', 'course-1', 1.0, 'new')]);
|
|
271
|
+
|
|
272
|
+
const gen3 = new MockGenerator();
|
|
273
|
+
gen3.setWeightedCards([makeWeightedCard('card-1', 'course-1', 1.0, 'new')]);
|
|
274
|
+
|
|
275
|
+
const composite = new CompositeGenerator([gen1, gen2, gen3]);
|
|
276
|
+
const result = await composite.getWeightedCards(10);
|
|
277
|
+
|
|
278
|
+
expect(result).toHaveLength(1);
|
|
279
|
+
// avg = 1.0, boost = 1.2, result would be 1.2 but clamped to 1.0
|
|
280
|
+
expect(result[0].score).toBe(1.0);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('sorting and limiting', () => {
|
|
285
|
+
it('returns cards sorted by score descending', async () => {
|
|
286
|
+
const gen1 = new MockGenerator();
|
|
287
|
+
gen1.setWeightedCards([
|
|
288
|
+
makeWeightedCard('card-low', 'course-1', 0.3, 'new'),
|
|
289
|
+
makeWeightedCard('card-high', 'course-1', 0.9, 'new'),
|
|
290
|
+
makeWeightedCard('card-med', 'course-1', 0.6, 'new'),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
const composite = new CompositeGenerator([gen1]);
|
|
294
|
+
const result = await composite.getWeightedCards(10);
|
|
295
|
+
|
|
296
|
+
expect(result).toHaveLength(3);
|
|
297
|
+
expect(result[0].cardId).toBe('card-high');
|
|
298
|
+
expect(result[1].cardId).toBe('card-med');
|
|
299
|
+
expect(result[2].cardId).toBe('card-low');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('boosts cards appearing in multiple generators to top of list', async () => {
|
|
303
|
+
const gen1 = new MockGenerator();
|
|
304
|
+
gen1.setWeightedCards([
|
|
305
|
+
makeWeightedCard('card-boosted', 'course-1', 0.5, 'new'),
|
|
306
|
+
makeWeightedCard('card-single', 'course-1', 0.6, 'new'),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
const gen2 = new MockGenerator();
|
|
310
|
+
gen2.setWeightedCards([
|
|
311
|
+
makeWeightedCard('card-boosted', 'course-1', 0.5, 'new'),
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
const composite = new CompositeGenerator([gen1, gen2]);
|
|
315
|
+
const result = await composite.getWeightedCards(10);
|
|
316
|
+
|
|
317
|
+
expect(result).toHaveLength(2);
|
|
318
|
+
// card-boosted: avg=0.5, boost=1.1, final=0.55
|
|
319
|
+
// card-single: 0.6 (no boost)
|
|
320
|
+
expect(result[0].cardId).toBe('card-single'); // 0.6 > 0.55
|
|
321
|
+
expect(result[1].cardId).toBe('card-boosted');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
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
|
+
});
|