96 lines
4.2 KiB
TypeScript
96 lines
4.2 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { computeStreakFromDays } from "../src/v1/curator/streak-utils.js";
|
||
|
||
/**
|
||
* Test: Streak policies — current/longest, same-day duplicate, gap reset,
|
||
* recovery (resume after gap), and seven-day eligibility.
|
||
*
|
||
* Uses the pure computeStreakFromDays function with a fixed "today" so tests
|
||
* are deterministic.
|
||
*/
|
||
|
||
const TODAY = "2026-07-10";
|
||
|
||
// ── 1. Current and longest from a contiguous run ending today ────────────────
|
||
{
|
||
const days = ["2026-07-10", "2026-07-09", "2026-07-08"];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
assert.equal(streak.current_days, 3, "current streak should be 3 for 3 consecutive days ending today");
|
||
assert.equal(streak.longest_days, 3, "longest should match current when run is unbroken");
|
||
assert.equal(streak.last_completed_on, "2026-07-10");
|
||
}
|
||
|
||
// ── 2. Same-day duplicate must not inflate the streak ────────────────────────
|
||
// computeStreakFromDays requires deduplicated days (the SQL GROUP BY ensures
|
||
// this in production). Passing a duplicate should not double-count.
|
||
{
|
||
const days = ["2026-07-10", "2026-07-10", "2026-07-09"];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
// With a duplicate, the descending list has two "today" entries. The while
|
||
// loop checks `days.includes(cursor)` which is set-based, so current stays 2.
|
||
// But the longest loop iterates ALL entries including the dup, which could
|
||
// inflate longest to 3. The correct behavior: duplicates must not inflate.
|
||
assert.equal(streak.current_days, 2, "duplicate day must not inflate current streak");
|
||
assert.equal(
|
||
streak.longest_days,
|
||
2,
|
||
"duplicate day must not inflate longest streak — duplicates should be deduped before counting",
|
||
);
|
||
}
|
||
|
||
// ── 3. Gap resets the current streak ─────────────────────────────────────────
|
||
{
|
||
// Completed today and 3 days ago — gap breaks current streak
|
||
const days = ["2026-07-10", "2026-07-07"];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
assert.equal(streak.current_days, 1, "gap resets current streak to 1 (only today)");
|
||
assert.equal(streak.longest_days, 1, "longest should also be 1 with only isolated days");
|
||
}
|
||
|
||
// ── 4. Recovery: longest captures a past longer run even if current is broken ─
|
||
{
|
||
// 5-day run last week, then a gap, then completed today
|
||
const days = [
|
||
"2026-07-10", // today (current restart)
|
||
"2026-07-05", // past run: Jul 1–5
|
||
"2026-07-04",
|
||
"2026-07-03",
|
||
"2026-07-02",
|
||
"2026-07-01",
|
||
];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
assert.equal(streak.current_days, 1, "current is 1 after a gap even if a longer past run exists");
|
||
assert.equal(streak.longest_days, 5, "longest should capture the 5-day past run");
|
||
}
|
||
|
||
// ── 5. Seven-day eligibility: a 7-day streak counts as eligible ──────────────
|
||
{
|
||
const days = [
|
||
"2026-07-10", "2026-07-09", "2026-07-08",
|
||
"2026-07-07", "2026-07-06", "2026-07-05", "2026-07-04",
|
||
];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
assert.equal(streak.current_days, 7, "7 consecutive days should yield current=7");
|
||
assert.equal(streak.longest_days, 7, "7 consecutive days should yield longest=7");
|
||
assert.ok(streak.current_days >= 7, "7-day streak eligibility threshold met");
|
||
}
|
||
|
||
// ── 6. Empty days yields zero streak ─────────────────────────────────────────
|
||
{
|
||
const streak = computeStreakFromDays([], TODAY);
|
||
assert.equal(streak.current_days, 0);
|
||
assert.equal(streak.longest_days, 0);
|
||
assert.equal(streak.last_completed_on, null);
|
||
}
|
||
|
||
// ── 7. Yesterday keeps the current streak alive until today's IST day closes ─
|
||
{
|
||
const days = ["2026-07-09"];
|
||
const streak = computeStreakFromDays(days, TODAY);
|
||
assert.equal(streak.current_days, 1, "yesterday remains current during the one-day grace window");
|
||
assert.equal(streak.longest_days, 1, "longest should still count the 1-day run");
|
||
}
|
||
|
||
console.log("streak-policy tests passed");
|
||
process.exit(0);
|