mirror of
https://github.com/Xevion/dotfiles.git
synced 2026-01-31 04:24:10 -06:00
Previous implementation had discontinuities at day boundaries. New cycle-based approach (2 AM sleep start) provides continuous, testable active hour tracking.
294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import {
|
|
SLEEP_START_HOUR,
|
|
WAKE_HOUR,
|
|
SLEEP_HOURS_PER_DAY,
|
|
ACTIVE_HOURS_PER_DAY,
|
|
offsetInCycle,
|
|
getCycleStart,
|
|
activeHoursInPartialCycle,
|
|
elapsedActiveHoursBetween,
|
|
} from "../home/dot_local/bin/executable_claude-usage";
|
|
|
|
/** Helper: create a Date for a specific day/hour/minute */
|
|
function makeDate(
|
|
year: number,
|
|
month: number,
|
|
day: number,
|
|
hour: number,
|
|
minute: number = 0,
|
|
second: number = 0
|
|
): Date {
|
|
return new Date(year, month - 1, day, hour, minute, second);
|
|
}
|
|
|
|
describe("constants", () => {
|
|
test("active + sleep = 24 hours", () => {
|
|
expect(ACTIVE_HOURS_PER_DAY + SLEEP_HOURS_PER_DAY).toBe(24);
|
|
});
|
|
|
|
test("wake hour = sleep start + sleep duration", () => {
|
|
expect(WAKE_HOUR).toBe(SLEEP_START_HOUR + SLEEP_HOURS_PER_DAY);
|
|
});
|
|
});
|
|
|
|
describe("offsetInCycle", () => {
|
|
test("2:00 AM is offset 0", () => {
|
|
expect(offsetInCycle(makeDate(2026, 1, 15, 2, 0))).toBe(0);
|
|
});
|
|
|
|
test("9:00 AM is offset 7 (sleep duration)", () => {
|
|
expect(offsetInCycle(makeDate(2026, 1, 15, 9, 0))).toBe(SLEEP_HOURS_PER_DAY);
|
|
});
|
|
|
|
test("midnight is offset 22", () => {
|
|
expect(offsetInCycle(makeDate(2026, 1, 15, 0, 0))).toBe(22);
|
|
});
|
|
|
|
test("1:59 AM is offset 23 + 59/60", () => {
|
|
const offset = offsetInCycle(makeDate(2026, 1, 15, 1, 59));
|
|
expect(offset).toBeCloseTo(23 + 59 / 60, 10);
|
|
});
|
|
|
|
test("offset is monotonically increasing from 2 AM through to next 1:59 AM", () => {
|
|
let prev = -1;
|
|
// Walk minute by minute from 2:00 AM to 1:59 AM next day
|
|
for (let minuteOffset = 0; minuteOffset < 24 * 60; minuteOffset++) {
|
|
const d = new Date(makeDate(2026, 1, 15, 2, 0).getTime() + minuteOffset * 60_000);
|
|
const offset = offsetInCycle(d);
|
|
expect(offset).toBeGreaterThan(prev);
|
|
prev = offset;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("getCycleStart", () => {
|
|
test("at 3 AM, cycle start is 2 AM same day", () => {
|
|
const result = getCycleStart(makeDate(2026, 1, 15, 3, 0));
|
|
expect(result.getHours()).toBe(2);
|
|
expect(result.getDate()).toBe(15);
|
|
});
|
|
|
|
test("at 1 AM, cycle start is 2 AM previous day", () => {
|
|
const result = getCycleStart(makeDate(2026, 1, 15, 1, 0));
|
|
expect(result.getHours()).toBe(2);
|
|
expect(result.getDate()).toBe(14);
|
|
});
|
|
|
|
test("at exactly 2 AM, cycle start is 2 AM same day", () => {
|
|
const result = getCycleStart(makeDate(2026, 1, 15, 2, 0));
|
|
expect(result.getHours()).toBe(2);
|
|
expect(result.getDate()).toBe(15);
|
|
});
|
|
});
|
|
|
|
describe("activeHoursInPartialCycle", () => {
|
|
test("offset 0 (sleep start) returns 0", () => {
|
|
expect(activeHoursInPartialCycle(0)).toBe(0);
|
|
});
|
|
|
|
test("offset during sleep returns 0", () => {
|
|
expect(activeHoursInPartialCycle(3)).toBe(0);
|
|
expect(activeHoursInPartialCycle(6.99)).toBe(0);
|
|
});
|
|
|
|
test("offset at sleep boundary returns 0", () => {
|
|
expect(activeHoursInPartialCycle(SLEEP_HOURS_PER_DAY)).toBe(0);
|
|
});
|
|
|
|
test("offset just after wake returns small positive", () => {
|
|
const result = activeHoursInPartialCycle(SLEEP_HOURS_PER_DAY + 0.01);
|
|
expect(result).toBeCloseTo(0.01, 10);
|
|
});
|
|
|
|
test("offset at end of cycle returns ACTIVE_HOURS_PER_DAY", () => {
|
|
expect(activeHoursInPartialCycle(24)).toBe(ACTIVE_HOURS_PER_DAY);
|
|
});
|
|
|
|
test("mid-active returns correct value", () => {
|
|
// offset 15 = 8 hours into active period
|
|
expect(activeHoursInPartialCycle(15)).toBe(15 - SLEEP_HOURS_PER_DAY);
|
|
});
|
|
});
|
|
|
|
describe("elapsedActiveHoursBetween", () => {
|
|
test("same timestamp returns 0", () => {
|
|
const t = makeDate(2026, 1, 15, 12, 0);
|
|
expect(elapsedActiveHoursBetween(t, t)).toBe(0);
|
|
});
|
|
|
|
test("1 hour during active period returns 1", () => {
|
|
const start = makeDate(2026, 1, 15, 12, 0);
|
|
const end = makeDate(2026, 1, 15, 13, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(1, 10);
|
|
});
|
|
|
|
test("full sleep period returns 0 active hours", () => {
|
|
const start = makeDate(2026, 1, 15, 2, 0);
|
|
const end = makeDate(2026, 1, 15, 9, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(0, 10);
|
|
});
|
|
|
|
test("full active period (9 AM to 2 AM) returns 17 hours", () => {
|
|
const start = makeDate(2026, 1, 15, 9, 0);
|
|
const end = makeDate(2026, 1, 16, 2, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(ACTIVE_HOURS_PER_DAY, 10);
|
|
});
|
|
|
|
test("full 24-hour cycle returns 17 active hours", () => {
|
|
const start = makeDate(2026, 1, 15, 2, 0);
|
|
const end = makeDate(2026, 1, 16, 2, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(ACTIVE_HOURS_PER_DAY, 10);
|
|
});
|
|
|
|
test("7 full days returns 7 * 17 = 119 active hours", () => {
|
|
const start = makeDate(2026, 1, 10, 2, 0);
|
|
const end = makeDate(2026, 1, 17, 2, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(7 * ACTIVE_HOURS_PER_DAY, 10);
|
|
});
|
|
|
|
test("across midnight is continuous (11 PM to 1 AM = 2 active hours)", () => {
|
|
const start = makeDate(2026, 1, 15, 23, 0);
|
|
const end = makeDate(2026, 1, 16, 1, 0);
|
|
expect(elapsedActiveHoursBetween(start, end)).toBeCloseTo(2, 10);
|
|
});
|
|
});
|
|
|
|
describe("no discontinuities at critical boundaries", () => {
|
|
/**
|
|
* Walk minute-by-minute through a full 7-day period and verify:
|
|
* 1. The elapsed active hours never decrease (monotonically non-decreasing)
|
|
* 2. The maximum jump between consecutive minutes is small (no sudden jumps)
|
|
*/
|
|
test("elapsed active hours is monotonically non-decreasing (minute granularity, 7 days)", () => {
|
|
const periodStart = makeDate(2026, 1, 10, 14, 30); // arbitrary start mid-afternoon
|
|
const totalMinutes = 7 * 24 * 60;
|
|
let prevValue = 0;
|
|
let maxJump = 0;
|
|
|
|
for (let m = 0; m <= totalMinutes; m++) {
|
|
const now = new Date(periodStart.getTime() + m * 60_000);
|
|
const elapsed = elapsedActiveHoursBetween(periodStart, now);
|
|
|
|
// Must never decrease
|
|
expect(elapsed).toBeGreaterThanOrEqual(prevValue);
|
|
|
|
// Track maximum jump
|
|
const jump = elapsed - prevValue;
|
|
if (jump > maxJump) maxJump = jump;
|
|
|
|
prevValue = elapsed;
|
|
}
|
|
|
|
// Maximum jump in 1 minute should be ~1/60 hour (during active) or 0 (during sleep)
|
|
// Allow small floating point margin
|
|
expect(maxJump).toBeLessThanOrEqual(1 / 60 + 1e-9);
|
|
});
|
|
|
|
test("no jump at 2:00 AM boundary (sleep start)", () => {
|
|
const periodStart = makeDate(2026, 1, 10, 12, 0);
|
|
|
|
// Check the minute before and after 2:00 AM
|
|
const before = makeDate(2026, 1, 15, 1, 59);
|
|
const at = makeDate(2026, 1, 15, 2, 0);
|
|
const after = makeDate(2026, 1, 15, 2, 1);
|
|
|
|
const valBefore = elapsedActiveHoursBetween(periodStart, before);
|
|
const valAt = elapsedActiveHoursBetween(periodStart, at);
|
|
const valAfter = elapsedActiveHoursBetween(periodStart, after);
|
|
|
|
// 1:59 AM to 2:00 AM: still active, should increase by ~1/60
|
|
expect(valAt - valBefore).toBeCloseTo(1 / 60, 4);
|
|
// 2:00 AM to 2:01 AM: now sleeping, should be flat
|
|
expect(valAfter - valAt).toBeCloseTo(0, 10);
|
|
});
|
|
|
|
test("no jump at 9:00 AM boundary (wake start)", () => {
|
|
const periodStart = makeDate(2026, 1, 10, 12, 0);
|
|
|
|
const before = makeDate(2026, 1, 15, 8, 59);
|
|
const at = makeDate(2026, 1, 15, 9, 0);
|
|
const after = makeDate(2026, 1, 15, 9, 1);
|
|
|
|
const valBefore = elapsedActiveHoursBetween(periodStart, before);
|
|
const valAt = elapsedActiveHoursBetween(periodStart, at);
|
|
const valAfter = elapsedActiveHoursBetween(periodStart, after);
|
|
|
|
// 8:59 AM to 9:00 AM: still sleeping, should be flat
|
|
expect(valAt - valBefore).toBeCloseTo(0, 10);
|
|
// 9:00 AM to 9:01 AM: now active, should increase by ~1/60
|
|
expect(valAfter - valAt).toBeCloseTo(1 / 60, 4);
|
|
});
|
|
|
|
test("no jump at midnight boundary", () => {
|
|
const periodStart = makeDate(2026, 1, 10, 12, 0);
|
|
|
|
const before = makeDate(2026, 1, 14, 23, 59);
|
|
const at = makeDate(2026, 1, 15, 0, 0);
|
|
const after = makeDate(2026, 1, 15, 0, 1);
|
|
|
|
const valBefore = elapsedActiveHoursBetween(periodStart, before);
|
|
const valAt = elapsedActiveHoursBetween(periodStart, at);
|
|
const valAfter = elapsedActiveHoursBetween(periodStart, after);
|
|
|
|
// All active times - should increase smoothly
|
|
const jump1 = valAt - valBefore;
|
|
const jump2 = valAfter - valAt;
|
|
|
|
expect(jump1).toBeCloseTo(1 / 60, 4);
|
|
expect(jump2).toBeCloseTo(1 / 60, 4);
|
|
});
|
|
|
|
test("flat during sleep hours (2 AM - 9 AM)", () => {
|
|
const periodStart = makeDate(2026, 1, 10, 12, 0);
|
|
|
|
const sleepStart = elapsedActiveHoursBetween(periodStart, makeDate(2026, 1, 15, 2, 0));
|
|
const sleepMid = elapsedActiveHoursBetween(periodStart, makeDate(2026, 1, 15, 5, 30));
|
|
const sleepEnd = elapsedActiveHoursBetween(periodStart, makeDate(2026, 1, 15, 9, 0));
|
|
|
|
expect(sleepMid).toBeCloseTo(sleepStart, 10);
|
|
expect(sleepEnd).toBeCloseTo(sleepStart, 10);
|
|
});
|
|
|
|
test("monotonic with arbitrary period start times", () => {
|
|
// Test with various period start times to catch alignment issues
|
|
const startTimes = [
|
|
makeDate(2026, 1, 10, 0, 0), // midnight
|
|
makeDate(2026, 1, 10, 1, 30), // during sleep (before 2 AM boundary)
|
|
makeDate(2026, 1, 10, 2, 0), // exactly at sleep start
|
|
makeDate(2026, 1, 10, 5, 0), // mid-sleep
|
|
makeDate(2026, 1, 10, 9, 0), // exactly at wake
|
|
makeDate(2026, 1, 10, 15, 45), // mid-afternoon
|
|
makeDate(2026, 1, 10, 23, 59), // just before midnight
|
|
];
|
|
|
|
for (const periodStart of startTimes) {
|
|
let prevValue = 0;
|
|
// Check every 10 minutes for 7 days
|
|
const totalSteps = 7 * 24 * 6;
|
|
for (let step = 0; step <= totalSteps; step++) {
|
|
const now = new Date(periodStart.getTime() + step * 10 * 60_000);
|
|
const elapsed = elapsedActiveHoursBetween(periodStart, now);
|
|
expect(elapsed).toBeGreaterThanOrEqual(prevValue);
|
|
prevValue = elapsed;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("pace calculation sanity", () => {
|
|
test("at period midpoint during active hours, elapsed should be roughly half total", () => {
|
|
// 3.5 days into period, at noon (active time)
|
|
const start = makeDate(2026, 1, 10, 12, 0);
|
|
const midpoint = makeDate(2026, 1, 14, 0, 0); // 3.5 days later
|
|
|
|
const elapsed = elapsedActiveHoursBetween(start, midpoint);
|
|
const totalActive = 7 * ACTIVE_HOURS_PER_DAY;
|
|
|
|
// Should be roughly 50% through, give or take
|
|
const ratio = elapsed / totalActive;
|
|
expect(ratio).toBeGreaterThan(0.35);
|
|
expect(ratio).toBeLessThan(0.65);
|
|
});
|
|
});
|