feat(FN-4452): complete Step 2 — hydrate stale paused review

Fusion-Task-Id: FN-4452
Fusion-Task-Lineage: 7d8b1c13-883b-4815-94ee-fdf18ad24e5f
This commit is contained in:
Fusion
2026-05-14 18:21:18 -07:00
committed by gsxdsm
parent 18695c860f
commit 0188c18f8b
2 changed files with 82 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { TaskStore } from "../store.js";
describe("TaskStore stalePausedReview hydration", () => {
let rootDir: string;
let globalDir: string;
let store: TaskStore;
beforeEach(async () => {
rootDir = await mkdtemp(join(tmpdir(), "store-stale-paused-review-"));
globalDir = join(rootDir, ".fusion-global-settings");
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
});
afterEach(async () => {
await store.close();
await rm(rootDir, { recursive: true, force: true });
});
async function seedTask(id: string, overrides: { paused?: boolean; ageMs?: number; column?: "in-review" | "todo"; mergeConfirmed?: boolean }) {
const now = Date.now();
const ageMs = overrides.ageMs ?? 24 * 60 * 60_000 + 1_000;
const movedAt = new Date(now - ageMs).toISOString();
const column = overrides.column ?? "in-review";
await store.createTaskWithReservedId(
{ description: id, column },
{ taskId: id, createdAt: movedAt, updatedAt: movedAt, applyDefaultWorkflowSteps: false },
);
const db = (store as unknown as { db: { prepare: (sql: string) => { run: (...params: unknown[]) => unknown } } }).db;
db.prepare(`UPDATE tasks
SET paused = ?, mergeDetails = ?, columnMovedAt = ?, updatedAt = ?
WHERE id = ?`).run(
overrides.paused ? 1 : 0,
JSON.stringify(overrides.mergeConfirmed ? { mergeConfirmed: true } : {}),
movedAt,
movedAt,
id,
);
}
it("hydrates stalePausedReview for paused in-review past threshold", async () => {
await seedTask("FN-4452-A", { paused: true });
const task = (await store.listTasks({ slim: true })).find((entry) => entry.id === "FN-4452-A");
expect(task?.stalePausedReview?.code).toBe("stale-paused-review");
});
it("omits stalePausedReview under threshold", async () => {
await seedTask("FN-4452-B", { paused: true, ageMs: 1_000 });
const task = (await store.listTasks({ slim: true })).find((entry) => entry.id === "FN-4452-B");
expect(task?.stalePausedReview).toBeUndefined();
});
it("omits stalePausedReview for non-paused tasks", async () => {
await seedTask("FN-4452-C", { paused: false });
const task = (await store.listTasks({ slim: true })).find((entry) => entry.id === "FN-4452-C");
expect(task?.stalePausedReview).toBeUndefined();
});
it("respects stalePausedReviewThresholdMs setting override", async () => {
await store.updateSettings({ stalePausedReviewThresholdMs: 2_000 });
await seedTask("FN-4452-D", { paused: true, ageMs: 2_500 });
const task = (await store.listTasks({ slim: true })).find((entry) => entry.id === "FN-4452-D");
expect(task?.stalePausedReview?.thresholdMs).toBe(2_000);
});
});

View File

@@ -24,6 +24,7 @@ import { BackwardCompat, ProjectRequiredError } from "./migration.js";
import { CentralCore } from "./central-core.js";
import { getTaskMergeBlocker, resolveTaskMergeTarget } from "./task-merge.js";
import { getInReviewStallReason } from "./in-review-stall.js";
import { getStalePausedReviewSignal } from "./stale-paused-review.js";
import { getTaskAgeStalenessSignal, type TaskAgeStalenessThresholds } from "./task-age-staleness.js";
import { ensureMemoryFileWithBackend } from "./project-memory.js";
import { runCommandAsync } from "./run-command.js";
@@ -3283,6 +3284,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const activeTasks = await Promise.all((rows as unknown as TaskRow[]).map(async (row) => {
const task = this.rowToTask(row);
task.inReviewStall = getInReviewStallReason(task, { now });
task.stalePausedReview = getStalePausedReviewSignal(task, {
now,
thresholdMs: settings.stalePausedReviewThresholdMs,
});
if (!disableAgeStalenessHydration) {
try {
task.ageStaleness = getTaskAgeStalenessSignal(task, { now, thresholds: staleThresholds });
@@ -3392,6 +3397,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const tasks = rows.slice(0, resolvedLimit).map((row) => {
const task = this.rowToTask(row);
task.inReviewStall = getInReviewStallReason(task, { now });
task.stalePausedReview = getStalePausedReviewSignal(task, {
now,
thresholdMs: settings.stalePausedReviewThresholdMs,
});
if (!disableAgeStalenessHydration) {
try {
task.ageStaleness = getTaskAgeStalenessSignal(task, { now, thresholds: staleThresholds });
@@ -3526,6 +3535,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const activeMatches = await Promise.all(rows.map(async (row) => {
const task = this.rowToTask(row);
task.inReviewStall = getInReviewStallReason(task, { now });
task.stalePausedReview = getStalePausedReviewSignal(task, {
now,
thresholdMs: settings.stalePausedReviewThresholdMs,
});
if (!disableAgeStalenessHydration) {
try {
task.ageStaleness = getTaskAgeStalenessSignal(task, { now, thresholds: staleThresholds });