| 362 |
362 |
|
assert!(repo.get_by_id(problem.id, user_id).await.unwrap().is_none());
|
| 363 |
363 |
|
assert!(!repo.delete(problem.id, user_id).await.unwrap());
|
| 364 |
364 |
|
}
|
|
365 |
+ |
|
|
366 |
+ |
/// Insert a project directly, so the test controls its status and the instant
|
|
367 |
+ |
/// it entered it. Returns the id to attribute a problem to.
|
|
368 |
+ |
async fn project_with_status(
|
|
369 |
+ |
pool: &sqlx::SqlitePool,
|
|
370 |
+ |
user_id: UserId,
|
|
371 |
+ |
status: &str,
|
|
372 |
+ |
status_changed_at: chrono::DateTime<Utc>,
|
|
373 |
+ |
) -> String {
|
|
374 |
+ |
let id = uuid::Uuid::new_v4().to_string();
|
|
375 |
+ |
sqlx::query(
|
|
376 |
+ |
"INSERT INTO projects (id, user_id, name, description, project_type, status, created_at, status_changed_at) \
|
|
377 |
+ |
VALUES (?, ?, 'shelf', '', 'SideProject', ?, ?, ?)",
|
|
378 |
+ |
)
|
|
379 |
+ |
.bind(&id)
|
|
380 |
+ |
.bind(user_id.to_string())
|
|
381 |
+ |
.bind(status)
|
|
382 |
+ |
.bind((Utc::now() - Duration::days(400)).format("%Y-%m-%d %H:%M:%S").to_string())
|
|
383 |
+ |
.bind(status_changed_at.format("%Y-%m-%d %H:%M:%S").to_string())
|
|
384 |
+ |
.execute(pool)
|
|
385 |
+ |
.await
|
|
386 |
+ |
.expect("insert project");
|
|
387 |
+ |
id
|
|
388 |
+ |
}
|
|
389 |
+ |
|
|
390 |
+ |
/// The dormancy join is what stops a shelved project's backlog climbing the
|
|
391 |
+ |
/// painhours ranking. It is a LEFT JOIN on a non-Active status, so this pins
|
|
392 |
+ |
/// both arms: a shelved project supplies the anchor, an Active one supplies
|
|
393 |
+ |
/// NULL, and an unattributed problem still reads.
|
|
394 |
+ |
#[tokio::test]
|
|
395 |
+ |
async fn a_shelved_project_freezes_its_problems_through_the_join() {
|
|
396 |
+ |
let pool = common::setup_test_db().await;
|
|
397 |
+ |
let user_id = common::create_test_user(&pool).await;
|
|
398 |
+ |
|
|
399 |
+ |
// Reported 40 days ago, shelved 33 days ago: the shelved one accrues the
|
|
400 |
+ |
// 7 days it spent in a live project and then stops, while its twin keeps
|
|
401 |
+ |
// climbing. Both stay short of the score's saturation point, so the
|
|
402 |
+ |
// comparison measures the freeze rather than two clipped 100s.
|
|
403 |
+ |
let shelved_at = Utc::now() - Duration::days(33);
|
|
404 |
+ |
let on_hold = project_with_status(&pool, user_id, "OnHold", shelved_at).await;
|
|
405 |
+ |
let active = project_with_status(&pool, user_id, "Active", shelved_at).await;
|
|
406 |
+ |
|
|
407 |
+ |
let repo = SqliteProblemRepository::new(pool);
|
|
408 |
+ |
|
|
409 |
+ |
// Same age, same factors: the only difference is the project's status.
|
|
410 |
+ |
let reported = Utc::now() - Duration::days(40);
|
|
411 |
+ |
let mut shelved_input = new_problem("shelved");
|
|
412 |
+ |
shelved_input.created_at = reported;
|
|
413 |
+ |
shelved_input.project_id = Some(uuid::Uuid::parse_str(&on_hold).unwrap().into());
|
|
414 |
+ |
let mut live_input = new_problem("live");
|
|
415 |
+ |
live_input.created_at = reported;
|
|
416 |
+ |
live_input.project_id = Some(uuid::Uuid::parse_str(&active).unwrap().into());
|
|
417 |
+ |
|
|
418 |
+ |
let shelved = repo.ingest(user_id, shelved_input).await.unwrap();
|
|
419 |
+ |
let live = repo.ingest(user_id, live_input).await.unwrap();
|
|
420 |
+ |
let orphan = repo.ingest(user_id, new_problem("orphan")).await.unwrap();
|
|
421 |
+ |
|
|
422 |
+ |
assert_eq!(
|
|
423 |
+ |
shelved.dormant_since.map(|d| d.date_naive()),
|
|
424 |
+ |
Some(shelved_at.date_naive())
|
|
425 |
+ |
);
|
|
426 |
+ |
assert!(shelved.is_dormant());
|
|
427 |
+ |
assert!(live.dormant_since.is_none(), "Active must not join");
|
|
428 |
+ |
assert!(!live.is_dormant());
|
|
429 |
+ |
assert!(
|
|
430 |
+ |
orphan.dormant_since.is_none(),
|
|
431 |
+ |
"unattributed must still read"
|
|
432 |
+ |
);
|
|
433 |
+ |
|
|
434 |
+ |
assert!(
|
|
435 |
+ |
shelved.painhours() < live.painhours(),
|
|
436 |
+ |
"shelved {} should rank below live {}",
|
|
437 |
+ |
shelved.painhours(),
|
|
438 |
+ |
live.painhours()
|
|
439 |
+ |
);
|
|
440 |
+ |
|
|
441 |
+ |
// And it survives the round trip through every read path.
|
|
442 |
+ |
let listed = repo.list(user_id, &ProblemFilter::default()).await.unwrap();
|
|
443 |
+ |
let from_list = listed.iter().find(|p| p.source_ref == "shelved").unwrap();
|
|
444 |
+ |
assert!(from_list.is_dormant(), "list must carry dormancy");
|
|
445 |
+ |
let by_id = repo.get_by_id(shelved.id, user_id).await.unwrap().unwrap();
|
|
446 |
+ |
assert!(by_id.is_dormant(), "get_by_id must carry dormancy");
|
|
447 |
+ |
}
|
|
448 |
+ |
|
|
449 |
+ |
/// The demotion tier. A shelved problem keeps its honest score and still sorts
|
|
450 |
+ |
/// below every live one, which is what makes the ranking usable again the day
|
|
451 |
+ |
/// the project comes back: nothing is recomputed, one bool flips.
|
|
452 |
+ |
#[tokio::test]
|
|
453 |
+ |
async fn dormant_problems_sort_below_live_ones_whatever_the_score() {
|
|
454 |
+ |
let pool = common::setup_test_db().await;
|
|
455 |
+ |
let user_id = common::create_test_user(&pool).await;
|
|
456 |
+ |
|
|
457 |
+ |
let shelved_at = Utc::now() - Duration::days(1);
|
|
458 |
+ |
let on_hold = project_with_status(&pool, user_id, "Archived", shelved_at).await;
|
|
459 |
+ |
|
|
460 |
+ |
let repo = SqliteProblemRepository::new(pool);
|
|
461 |
+ |
|
|
462 |
+ |
// The shelved one is deliberately the more painful, older problem: it
|
|
463 |
+ |
// outranks the live one on every axis the score knows about.
|
|
464 |
+ |
let mut big = new_problem("shelved-and-severe");
|
|
465 |
+ |
big.pain = 5;
|
|
466 |
+ |
big.scale = 5;
|
|
467 |
+ |
big.created_at = Utc::now() - Duration::days(300);
|
|
468 |
+ |
big.project_id = Some(uuid::Uuid::parse_str(&on_hold).unwrap().into());
|
|
469 |
+ |
|
|
470 |
+ |
let mut small = new_problem("live-and-minor");
|
|
471 |
+ |
small.pain = 1;
|
|
472 |
+ |
small.scale = 1;
|
|
473 |
+ |
small.created_at = Utc::now() - Duration::days(1);
|
|
474 |
+ |
|
|
475 |
+ |
let shelved = repo.ingest(user_id, big).await.unwrap();
|
|
476 |
+ |
let live = repo.ingest(user_id, small).await.unwrap();
|
|
477 |
+ |
|
|
478 |
+ |
assert!(
|
|
479 |
+ |
shelved.painhours() > live.painhours(),
|
|
480 |
+ |
"fixture must have the shelved problem scoring higher, else this proves nothing"
|
|
481 |
+ |
);
|
|
482 |
+ |
|
|
483 |
+ |
let listed = repo.list(user_id, &ProblemFilter::default()).await.unwrap();
|
|
484 |
+ |
let order: Vec<&str> = listed.iter().map(|p| p.source_ref.as_str()).collect();
|
|
485 |
+ |
assert_eq!(
|
|
486 |
+ |
order,
|
|
487 |
+ |
vec!["live-and-minor", "shelved-and-severe"],
|
|
488 |
+ |
"live work sorts first regardless of score"
|
|
489 |
+ |
);
|
|
490 |
+ |
|
|
491 |
+ |
// And the demoted problem did not lose its score on the way down.
|
|
492 |
+ |
assert_eq!(listed[1].painhours(), shelved.painhours());
|
|
493 |
+ |
}
|