|
1 | 1 | //! db_flashcards 单测(内存库;AAA 模式)。 |
2 | 2 |
|
| 3 | +use crate::commands_flashcards::review_card_inner; |
3 | 4 | use crate::db::Db; |
4 | 5 | use crate::db_flashcards::NewFlashcard; |
| 6 | +use crate::scheduler::{schedule, CardState, Rating}; |
5 | 7 | use crate::types::NewNoteGroup; |
6 | 8 |
|
7 | 9 | /// 内存库。 |
@@ -224,3 +226,69 @@ fn update_card_back_roundtrip() { |
224 | 226 | assert_eq!(fetched.back, new_back); |
225 | 227 | assert!(!db.update_card_back(9999, "x").expect("miss")); |
226 | 228 | } |
| 229 | + |
| 230 | +/// 复习过的卡状态 JSON(`reps=0` ⇒ `review_card` 内部按新卡态调度:间隔与 now 无关)。 |
| 231 | +fn reviewed_state(last_review_ms: i64) -> String { |
| 232 | + format!( |
| 233 | + r#"{{"stability":0.0,"difficulty":0.0,"reps":0,"lapses":0,"lastReviewMs":{}}}"#, |
| 234 | + last_review_ms |
| 235 | + ) |
| 236 | +} |
| 237 | + |
| 238 | +#[test] |
| 239 | +fn interval_days_new_card_is_zero_and_degraded_inputs_safe() { |
| 240 | + // Arrange:新卡(state_json="{}" ⇒ lastReviewMs 缺键 ⇒ 0) |
| 241 | + let db = mem_db(); |
| 242 | + let gid = make_group(&db); |
| 243 | + // Act |
| 244 | + let created = db.create_card(&card(gid, "新卡", 1000)).expect("create"); |
| 245 | + let fetched = db.get_card(created.id).expect("get").expect("exists"); |
| 246 | + // Assert:新卡无间隔——两路都 0.0(不发明 due_at/86400000 这类数字) |
| 247 | + assert_eq!(created.interval_days, 0.0); |
| 248 | + assert_eq!(fetched.interval_days, 0.0); |
| 249 | + // Act/Assert(劣化输入):lastReviewMs > due_at ⇒ 0.0;due_at 为负 ⇒ 0.0;均不 panic |
| 250 | + db.update_card_schedule(created.id, &reviewed_state(9_000_000), 1000).expect("u1"); |
| 251 | + assert_eq!(db.get_card(created.id).expect("g1").expect("e1").interval_days, 0.0); |
| 252 | + db.update_card_schedule(created.id, &reviewed_state(5000), -1).expect("u2"); |
| 253 | + assert_eq!(db.get_card(created.id).expect("g2").expect("e2").interval_days, 0.0); |
| 254 | +} |
| 255 | + |
| 256 | +#[test] |
| 257 | +fn interval_days_row_derived_is_whole_days() { |
| 258 | + // Arrange:上次复习 T0、到期 T0+31 天(夹具构造;不读 stateJson 语义) |
| 259 | + let db = mem_db(); |
| 260 | + let gid = make_group(&db); |
| 261 | + let t0 = 1_700_000_000_000i64; |
| 262 | + let mut new = card(gid, "极限", t0 + 31 * 86_400_000); |
| 263 | + new.state_json = reviewed_state(t0); |
| 264 | + db.create_card(&new).expect("create"); |
| 265 | + // Act:队列路径(5 个查询方法共用 row_to_card 供值) |
| 266 | + let due = db.list_due_cards(None, i64::MAX, 10).expect("due"); |
| 267 | + // Assert:整天粒度 = round((due_at − lastReviewMs)/86400000).max(1) |
| 268 | + assert_eq!(due.len(), 1); |
| 269 | + assert_eq!(due[0].interval_days, 31.0); |
| 270 | +} |
| 271 | + |
| 272 | +#[test] |
| 273 | +fn review_card_returns_precise_interval_not_stale_row_value() { |
| 274 | + // Arrange:行派生值刻意构造成 31 天(陈旧值)——本次评分必不等于它 |
| 275 | + let db = mem_db(); |
| 276 | + let gid = make_group(&db); |
| 277 | + let t0 = 1_700_000_000_000i64; |
| 278 | + let mut new = card(gid, "极限", t0 + 31 * 86_400_000); |
| 279 | + new.state_json = reviewed_state(t0); |
| 280 | + let id = db.create_card(&new).expect("create").id; |
| 281 | + let stale = db.get_card(id).expect("get").expect("exists"); |
| 282 | + assert_eq!(stale.interval_days, 31.0, "②域:行派生整天粒度"); |
| 283 | + // Act:复习编排(命令壳只解引用 State ⇒ inner 等价于测全命令) |
| 284 | + let returned = review_card_inner(&db, id, "good").expect("review"); |
| 285 | + // Assert:①域 = 当次 ScheduleOutcome.interval_days(reps=0 ⇒ 新卡态 ⇒ 与 now 无关) |
| 286 | + assert_eq!(returned.interval_days, schedule(None, Rating::Good, 0).interval_days); |
| 287 | + assert_ne!(returned.interval_days, stale.interval_days, "`..card` 不得漏出旧值"); |
| 288 | + // Assert:返回的间隔就是驱动 dueAt 的那一个(scheduler.rs:100 恒等式) |
| 289 | + let next: CardState = serde_json::from_str(&returned.state_json).expect("next"); |
| 290 | + assert_eq!( |
| 291 | + returned.due_at, |
| 292 | + next.last_review_ms as i64 + (returned.interval_days.round() as i64).max(1) * 86_400_000 |
| 293 | + ); |
| 294 | +} |
0 commit comments