478 lines
13 KiB
TypeScript
478 lines
13 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
||
import {
|
||
computeCacheTTL,
|
||
TTL_HIGH_ACTIVITY,
|
||
TTL_MODERATE_ACTIVITY,
|
||
TTL_LOW_ACTIVITY,
|
||
} from "../../src/modules/algorithms/computeCacheTTL";
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** Fixed reference point so tests are deterministic. */
|
||
const NOW = new Date("2026-03-02T12:00:00Z");
|
||
|
||
/** Return a Date offset from NOW by `days` (negative = past, positive = future). */
|
||
const daysFromNow = (days: number): Date =>
|
||
new Date(NOW.getTime() + days * 24 * 60 * 60 * 1000);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rule 1a — Closed records older than 30 days should not be cached
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — Rule 1a: Closed records (>30 days)", () => {
|
||
test("returns null when closedFlag is true and closedDate is null", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBeNull();
|
||
});
|
||
|
||
test("returns null when closedFlag is true and closedDate is 60 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-60),
|
||
expectedCloseDate: daysFromNow(-1),
|
||
lastUpdated: daysFromNow(-1),
|
||
now: NOW,
|
||
}),
|
||
).toBeNull();
|
||
});
|
||
|
||
test("returns null when closedFlag is true and closedDate is 31 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: new Date(NOW.getTime() - 31 * 24 * 60 * 60 * 1000),
|
||
expectedCloseDate: daysFromNow(2),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rule 1b — Recently closed (within 30 days) → 15 minutes
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — Rule 1b: Recently closed (≤30 days)", () => {
|
||
test("returns 15min when closed 1 day ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-1),
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when closed 15 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-15),
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when closed exactly 30 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-30),
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when closed today even with recent activity dates", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: NOW,
|
||
expectedCloseDate: daysFromNow(-1),
|
||
lastUpdated: NOW,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("just past 30-day boundary returns null", () => {
|
||
const justPast30Days = new Date(
|
||
NOW.getTime() - 30 * 24 * 60 * 60 * 1000 - 1,
|
||
);
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: justPast30Days,
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rule 2 — High activity (within 5 days) → 30 seconds
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — Rule 2: High activity (≤5 days)", () => {
|
||
test("returns 30s when lastUpdated is today", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: NOW,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when lastUpdated is 3 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-3),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when lastUpdated is exactly 5 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-5),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when expectedCloseDate is 2 days in the future", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(2),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when expectedCloseDate is 5 days in the future", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(5),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when expectedCloseDate is 4 days ago (recently passed)", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(-4),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when either date is within 5 days (lastUpdated wins)", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(-30),
|
||
lastUpdated: daysFromNow(-2),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("returns 30s when either date is within 5 days (expectedCloseDate wins)", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(3),
|
||
lastUpdated: daysFromNow(-30),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rule 3 — Moderate activity (within 14 days but > 5 days) → 60 seconds
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — Rule 3: Moderate activity (6–14 days)", () => {
|
||
test("returns 60s when lastUpdated is 6 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-6),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("returns 60s when lastUpdated is 10 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-10),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("returns 60s when lastUpdated is exactly 14 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-14),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("returns 60s when expectedCloseDate is 8 days in the future", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(8),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("returns 60s when expectedCloseDate is 14 days in the future", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(14),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("returns 60s when expectedCloseDate is 12 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(-12),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Rule 4 — Low activity (older than 14 days) → 15 minutes
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — Rule 4: Low activity (>14 days)", () => {
|
||
test("returns 15min when lastUpdated is 15 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-15),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when lastUpdated is 60 days ago", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-60),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when expectedCloseDate is 20 days in the future", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(20),
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when both dates are null", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("returns 15min when both dates are far in the past", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(-100),
|
||
lastUpdated: daysFromNow(-90),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Edge cases
|
||
// ---------------------------------------------------------------------------
|
||
describe("computeCacheTTL — edge cases", () => {
|
||
test("defaults `now` to current time when omitted", () => {
|
||
// Open, no dates → should return LOW_ACTIVITY (15min)
|
||
const result = computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: null,
|
||
});
|
||
expect(result).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("5-day boundary is inclusive", () => {
|
||
// Exactly 5 days should match high activity
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-5),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("just past 5-day boundary falls to moderate", () => {
|
||
// 5 days + 1 millisecond past → moderate
|
||
const justPast5Days = new Date(NOW.getTime() - 5 * 24 * 60 * 60 * 1000 - 1);
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: justPast5Days,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("14-day boundary is inclusive", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: daysFromNow(-14),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_MODERATE_ACTIVITY);
|
||
});
|
||
|
||
test("just past 14-day boundary falls to low activity", () => {
|
||
const justPast14Days = new Date(
|
||
NOW.getTime() - 14 * 24 * 60 * 60 * 1000 - 1,
|
||
);
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: null,
|
||
lastUpdated: justPast14Days,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
|
||
test("higher-priority rule wins when both dates span different tiers", () => {
|
||
// expectedCloseDate in 5-day window, lastUpdated in 14-day window → 30s
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: false,
|
||
closedDate: null,
|
||
expectedCloseDate: daysFromNow(3),
|
||
lastUpdated: daysFromNow(-10),
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_HIGH_ACTIVITY);
|
||
});
|
||
|
||
test("closed >30 days always returns null regardless of other dates", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-60),
|
||
expectedCloseDate: NOW,
|
||
lastUpdated: NOW,
|
||
now: NOW,
|
||
}),
|
||
).toBeNull();
|
||
});
|
||
|
||
test("recently closed always returns 15min regardless of activity dates", () => {
|
||
expect(
|
||
computeCacheTTL({
|
||
closedFlag: true,
|
||
closedDate: daysFromNow(-5),
|
||
expectedCloseDate: NOW,
|
||
lastUpdated: NOW,
|
||
now: NOW,
|
||
}),
|
||
).toBe(TTL_LOW_ACTIVITY);
|
||
});
|
||
});
|