feat(FN-4608): complete Step 2 — handle empty-sample reliability days
Fusion-Task-Id: FN-4608 Fusion-Task-Lineage: 6cabc509-38a1-4cb8-be6f-51c53d5753ff
This commit is contained in:
@@ -4,6 +4,7 @@ import type { ActivityLogEntry, RunAuditEvent } from "@fusion/core";
|
||||
|
||||
import {
|
||||
bucketByDay,
|
||||
dayHasSamples,
|
||||
fileScopeInvariantFailuresPerDay,
|
||||
inReviewDurationMetrics,
|
||||
inReviewFailureRate7d,
|
||||
@@ -136,4 +137,52 @@ describe("reliability-metrics", () => {
|
||||
expect(inReviewFailureRate7d({ "2026-05-13": 10 }, { "2026-05-13": 2 }, endMs)).toEqual({ value: 0.2 });
|
||||
expect(inReviewFailureRate7d({}, {}, endMs)).toEqual({ value: null, reason: "no-in-review-entries" });
|
||||
});
|
||||
|
||||
it("returns no-in-review-entries when all seven days are empty", () => {
|
||||
const endMs = Date.parse("2026-05-13T00:00:00.000Z");
|
||||
expect(inReviewFailureRate7d({ "2026-05-13": 0, "2026-05-12": 0 }, { "2026-05-13": 0 }, endMs)).toEqual({
|
||||
value: null,
|
||||
reason: "no-in-review-entries",
|
||||
});
|
||||
});
|
||||
|
||||
it("filters task movement counts by start/end window", () => {
|
||||
const activity: ActivityLogEntry[] = [
|
||||
moved("2026-05-10T23:59:59.000Z", "FN-1", "todo", "in-review"),
|
||||
moved("2026-05-11T00:00:00.000Z", "FN-2", "todo", "in-review"),
|
||||
moved("2026-05-12T00:00:00.000Z", "FN-3", "in-review", "in-progress"),
|
||||
];
|
||||
|
||||
const start = Date.parse("2026-05-11T00:00:00.000Z");
|
||||
const end = Date.parse("2026-05-12T00:00:00.000Z");
|
||||
|
||||
expect(tasksEnteredInReviewPerDay(activity, start, end)).toEqual({ "2026-05-11": 1 });
|
||||
expect(tasksBouncedToInProgressPerDay(activity, start, end)).toEqual({ "2026-05-12": 1 });
|
||||
});
|
||||
|
||||
it("reports hasSamples semantics for per-day rows", () => {
|
||||
expect(dayHasSamples({
|
||||
tasksEnteredInReview: 0,
|
||||
tasksBouncedToInProgress: 0,
|
||||
postMergeAuditFailures: null,
|
||||
fileScopeInvariantFailures: null,
|
||||
recoverAlreadyMergedReviewTasksRecoveries: null,
|
||||
})).toBe(false);
|
||||
|
||||
expect(dayHasSamples({
|
||||
tasksEnteredInReview: 0,
|
||||
tasksBouncedToInProgress: 1,
|
||||
postMergeAuditFailures: null,
|
||||
fileScopeInvariantFailures: null,
|
||||
recoverAlreadyMergedReviewTasksRecoveries: null,
|
||||
})).toBe(true);
|
||||
|
||||
expect(dayHasSamples({
|
||||
tasksEnteredInReview: 0,
|
||||
tasksBouncedToInProgress: 0,
|
||||
postMergeAuditFailures: { block: 0, warn: 1, off: 0 },
|
||||
fileScopeInvariantFailures: 0,
|
||||
recoverAlreadyMergedReviewTasksRecoveries: 0,
|
||||
})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,14 @@ export interface InReviewDurationMetric {
|
||||
reason?: NullMetricReason;
|
||||
}
|
||||
|
||||
export interface ReliabilityPerDayCounts {
|
||||
tasksEnteredInReview: number;
|
||||
tasksBouncedToInProgress: number;
|
||||
postMergeAuditFailures: { block: number; warn: number; off: number } | null;
|
||||
fileScopeInvariantFailures: number | null;
|
||||
recoverAlreadyMergedReviewTasksRecoveries: number | null;
|
||||
}
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
export function bucketByDay(timestamp: string): string {
|
||||
@@ -183,6 +191,21 @@ export function mergeAttemptsPerMergedTask(events: RunAuditEvent[], activity: Ac
|
||||
};
|
||||
}
|
||||
|
||||
export function dayHasSamples(counts: ReliabilityPerDayCounts): boolean {
|
||||
if (counts.tasksEnteredInReview > 0 || counts.tasksBouncedToInProgress > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (counts.postMergeAuditFailures) {
|
||||
const { block, warn, off } = counts.postMergeAuditFailures;
|
||||
if (block + warn + off > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (counts.fileScopeInvariantFailures ?? 0) > 0 || (counts.recoverAlreadyMergedReviewTasksRecoveries ?? 0) > 0;
|
||||
}
|
||||
|
||||
export function inReviewFailureRate7d(enteredByDay: Record<string, number>, bouncedByDay: Record<string, number>, endMs: number): NullableMetric<number> {
|
||||
let entered = 0;
|
||||
let bounced = 0;
|
||||
|
||||
@@ -61,6 +61,7 @@ import { createAuthMiddleware, authenticateUpgradeRequest, getDaemonToken } from
|
||||
import { validateRemoteAuthToken } from "./remote-auth.js";
|
||||
import { getCliPackageVersion } from "./cli-package-version.js";
|
||||
import {
|
||||
dayHasSamples,
|
||||
fileScopeInvariantFailuresPerDay,
|
||||
inReviewDurationMetrics,
|
||||
inReviewFailureRate7d,
|
||||
@@ -1167,11 +1168,13 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
const postMergeAuditFailures = postMergeByDay.value ? (postMergeByDay.value[day] ?? { block: 0, warn: 0, off: 0 }) : null;
|
||||
const fileScopeInvariantFailures = fileScopeByDay.value ? (fileScopeByDay.value[day] ?? 0) : null;
|
||||
const recoverAlreadyMergedReviewTasksRecoveries = recoveriesByDay.value ? (recoveriesByDay.value[day] ?? 0) : null;
|
||||
const hasSamples = tasksEnteredInReview > 0
|
||||
|| tasksBouncedToInProgress > 0
|
||||
|| (postMergeAuditFailures ? postMergeAuditFailures.block + postMergeAuditFailures.warn + postMergeAuditFailures.off > 0 : false)
|
||||
|| (typeof fileScopeInvariantFailures === "number" && fileScopeInvariantFailures > 0)
|
||||
|| (typeof recoverAlreadyMergedReviewTasksRecoveries === "number" && recoverAlreadyMergedReviewTasksRecoveries > 0);
|
||||
const hasSamples = dayHasSamples({
|
||||
tasksEnteredInReview,
|
||||
tasksBouncedToInProgress,
|
||||
postMergeAuditFailures,
|
||||
fileScopeInvariantFailures,
|
||||
recoverAlreadyMergedReviewTasksRecoveries,
|
||||
});
|
||||
|
||||
perDay.push({
|
||||
date: day,
|
||||
|
||||
Reference in New Issue
Block a user