feat(FN-1465): merge fusion/fn-1465

This commit is contained in:
gsxdsm
2026-04-12 07:00:49 -07:00
parent 86f1f93de9
commit 8f0bcf7f00
58 changed files with 3890 additions and 1696 deletions

View File

@@ -104,6 +104,98 @@ describe("isTaskStuck", () => {
const task = createTask({ updatedAt: boundary });
expect(isTaskStuck(task, 600000)).toBe(false);
});
describe("dataAsOfMs parameter (freshness-aware stuck detection)", () => {
it("uses dataAsOfMs instead of Date.now() when provided", () => {
// Task updatedAt is 11 minutes ago
const taskUpdatedAt = new Date(Date.now() - 11 * 60 * 1000).toISOString();
const task = createTask({ updatedAt: taskUpdatedAt });
// dataAsOfMs is 5 minutes ago (task was fresh 5 minutes ago)
const dataAsOfMs = Date.now() - 5 * 60 * 1000;
// 10 minute timeout
// With dataAsOfMs: 5 min - 11 min = -6 min < 10 min → NOT stuck
// Without dataAsOfMs: 0 min - 11 min = -11 min > 10 min → stuck
expect(isTaskStuck(task, 600000, dataAsOfMs)).toBe(false);
});
it("falls back to Date.now() when dataAsOfMs is undefined", () => {
// Task updatedAt is 5 minutes ago
const taskUpdatedAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
const task = createTask({ updatedAt: taskUpdatedAt });
// Without dataAsOfMs, should use Date.now() → NOT stuck (within 10 min timeout)
expect(isTaskStuck(task, 600000)).toBe(false);
});
it("correctly identifies a task that would be stuck with Date.now() but not with dataAsOfMs", () => {
// Scenario: Tab was in background for 20 minutes
// Task was updated 10 minutes ago (relative to dataAsOfMs)
// dataAsOfMs represents "10 minutes ago" (when we fetched fresh data)
// Date.now() is "now" (20 minutes after the fetch)
//
// This simulates the background tab scenario:
// - User opened tab at T=0, fetched tasks
// - Tab went to background at T=0
// - User came back at T=20
// - dataAsOfMs = T=0 (when we last had fresh data)
// - Task was updated at T=-10 (10 minutes before fetch)
// - task.updatedAt represents T=-10
//
// Check: dataAsOfMs - updatedAt = 0 - (-10) = 10 min < 10 min timeout → NOT stuck
// Without dataAsOfMs: Date.now() - updatedAt = 20 - (-10) = 30 min > 10 min → STUCK (false positive!)
// In fake timers, we set Date.now() to a fixed point
// Let's say Date.now() = 1000 (representing "now")
// dataAsOfMs = 0 (representing 20 minutes before "now" in fake time)
// task.updatedAt = -600 (representing 10 minutes before dataAsOfMs)
vi.setSystemTime(new Date(1000)); // Date.now() = 1000
const dataAsOfMs = 0; // 20 minutes before Date.now() in this scenario
const taskUpdatedAt = new Date(-600000).toISOString(); // 10 minutes before dataAsOfMs
const task = createTask({ updatedAt: taskUpdatedAt });
// With dataAsOfMs: 0 - (-600000) = 600000ms = 10 min = timeout → NOT stuck (boundary)
// Without dataAsOfMs: 1000 - (-600000) = 601000ms > 10 min → STUCK
// The key test: with dataAsOfMs it should NOT be stuck even though Date.now() would say it is
expect(isTaskStuck(task, 600000, dataAsOfMs)).toBe(false);
});
it("prevents false positive when tab was in background", () => {
// Simulate: Tab in background, data fetched 15 min ago
// Task.updatedAt is 12 min ago (stale from server perspective)
// taskStuckTimeoutMs = 10 min
// With fresh data (15 min ago): 15 - 12 = 3 min < 10 min → NOT stuck
// With stale Date.now(): 0 - 12 = 12 min > 10 min → STUCK (FALSE POSITIVE)
vi.setSystemTime(new Date(0)); // Date.now() = 0
const dataAsOfMs = -900000; // 15 minutes ago (in fake time)
const taskUpdatedAt = new Date(-720000).toISOString(); // 12 minutes ago (in fake time)
const task = createTask({ updatedAt: taskUpdatedAt });
// With dataAsOfMs: -900000 - (-720000) = -180000ms = -3 min < 10 min → NOT stuck
// Without dataAsOfMs: 0 - (-720000) = 720000ms = 12 min > 10 min → STUCK
expect(isTaskStuck(task, 600000, dataAsOfMs)).toBe(false);
});
it("correctly identifies genuinely stuck tasks even with dataAsOfMs", () => {
// Task really is stuck: updatedAt is 15 min ago, timeout is 10 min
// With dataAsOfMs of 2 min ago: 2 - 15 = -13 min < 10 min → NOT stuck (hmm, this is a problem)
// Actually, dataAsOfMs should represent when we last got FRESH data from the server
// If dataAsOfMs = 2 min ago and task.updatedAt = 15 min ago, the task was stale
// even when we fetched it, because 2 - 15 = -13 min > 10 min timeout
vi.setSystemTime(new Date(0));
const dataAsOfMs = -120000; // 2 minutes ago
const taskUpdatedAt = new Date(-900000).toISOString(); // 15 minutes ago
const task = createTask({ updatedAt: taskUpdatedAt });
// With dataAsOfMs: -120000 - (-900000) = 780000ms = 13 min > 10 min → STUCK
expect(isTaskStuck(task, 600000, dataAsOfMs)).toBe(true);
});
});
});
describe("countStuckTasks", () => {
@@ -152,4 +244,27 @@ describe("countStuckTasks", () => {
];
expect(countStuckTasks(tasks, 600000)).toBe(2);
});
describe("dataAsOfMs parameter (freshness-aware stuck detection)", () => {
it("passes dataAsOfMs through to isTaskStuck", () => {
// Task would be stuck with Date.now() but not with dataAsOfMs
vi.setSystemTime(new Date(0));
const dataAsOfMs = -900000; // 15 minutes ago
const taskUpdatedAt = new Date(-720000).toISOString(); // 12 minutes ago
const tasks = [createTask({ updatedAt: taskUpdatedAt })];
// With dataAsOfMs: -900000 - (-720000) = -180000ms = -3 min < 10 min → NOT stuck
expect(countStuckTasks(tasks, 600000, dataAsOfMs)).toBe(0);
});
it("counts tasks that are genuinely stuck even with dataAsOfMs", () => {
vi.setSystemTime(new Date(0));
const dataAsOfMs = -120000; // 2 minutes ago
const taskUpdatedAt = new Date(-900000).toISOString(); // 15 minutes ago
const tasks = [createTask({ updatedAt: taskUpdatedAt })];
// With dataAsOfMs: -120000 - (-900000) = 780000ms = 13 min > 10 min → STUCK
expect(countStuckTasks(tasks, 600000, dataAsOfMs)).toBe(1);
});
});
});