chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
chooseBucketSeconds,
|
||||
groupRunStatus,
|
||||
RUN_STATUS_GROUPS,
|
||||
zeroFillGroupedSeries,
|
||||
zeroFillScalarSeries,
|
||||
} from "~/presenters/v3/activitySeries.server";
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
describe("chooseBucketSeconds", () => {
|
||||
it("uses fine buckets for sub-hour ranges (the 5-minute bug)", () => {
|
||||
// 5 minutes should NOT collapse to a single 1h bar.
|
||||
expect(chooseBucketSeconds(5 * MINUTE)).toBe(5); // 60 buckets
|
||||
expect(chooseBucketSeconds(1 * MINUTE)).toBe(1); // 60 buckets
|
||||
expect(chooseBucketSeconds(30 * MINUTE)).toBe(30); // 60 buckets
|
||||
});
|
||||
|
||||
it("scales the interval up for longer ranges", () => {
|
||||
expect(chooseBucketSeconds(1 * HOUR)).toBe(60);
|
||||
expect(chooseBucketSeconds(6 * HOUR)).toBe(300);
|
||||
expect(chooseBucketSeconds(7 * DAY)).toBe(7200);
|
||||
expect(chooseBucketSeconds(30 * DAY)).toBe(43200);
|
||||
});
|
||||
|
||||
it("never exceeds the bucket ceiling", () => {
|
||||
const ranges = [1 * MINUTE, 5 * MINUTE, 1 * HOUR, 24 * HOUR, 7 * DAY, 30 * DAY];
|
||||
for (const range of ranges) {
|
||||
const secs = chooseBucketSeconds(range);
|
||||
const count = range / 1000 / secs;
|
||||
expect(count).toBeLessThanOrEqual(120);
|
||||
expect(count).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to a computed interval for ranges beyond the ladder", () => {
|
||||
const huge = 2000 * DAY;
|
||||
const secs = chooseBucketSeconds(huge);
|
||||
const count = huge / 1000 / secs;
|
||||
expect(count).toBeLessThanOrEqual(120);
|
||||
});
|
||||
|
||||
it("honours a custom target", () => {
|
||||
// Smaller target => wider buckets => fewer bars.
|
||||
const wide = chooseBucketSeconds(1 * HOUR, { targetBuckets: 12 });
|
||||
const dense = chooseBucketSeconds(1 * HOUR, { targetBuckets: 72 });
|
||||
expect(wide).toBeGreaterThan(dense);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupRunStatus", () => {
|
||||
it("maps raw statuses to chart groups", () => {
|
||||
expect(groupRunStatus("COMPLETED_SUCCESSFULLY")).toBe("COMPLETED");
|
||||
expect(groupRunStatus("CRASHED")).toBe("FAILED");
|
||||
expect(groupRunStatus("EXPIRED")).toBe("CANCELED");
|
||||
expect(groupRunStatus("EXECUTING")).toBe("RUNNING");
|
||||
expect(groupRunStatus("SOMETHING_UNKNOWN")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("zeroFillGroupedSeries", () => {
|
||||
it("emits a contiguous, fully zero-filled series", () => {
|
||||
const from = new Date("2026-06-22T00:00:00.000Z");
|
||||
const to = new Date("2026-06-22T00:00:05.000Z"); // 5 seconds
|
||||
const bucketSeconds = 1;
|
||||
const at2s = Math.floor(new Date("2026-06-22T00:00:02.000Z").getTime() / 1000);
|
||||
|
||||
const points = zeroFillGroupedSeries({
|
||||
rows: [{ bucket: at2s, status: "COMPLETED_SUCCESSFULLY", val: 3 }],
|
||||
from,
|
||||
to,
|
||||
bucketSeconds,
|
||||
orderedKeys: RUN_STATUS_GROUPS,
|
||||
groupFn: groupRunStatus,
|
||||
fallbackKey: "RUNNING",
|
||||
});
|
||||
|
||||
expect(points).toHaveLength(5);
|
||||
// Every point has every key (stable legend).
|
||||
for (const p of points) {
|
||||
for (const key of RUN_STATUS_GROUPS) {
|
||||
expect(typeof p[key]).toBe("number");
|
||||
}
|
||||
}
|
||||
// The matching bucket carries the value; the rest are zero.
|
||||
const filled = points.find((p) => p.bucket === at2s * 1000);
|
||||
expect(filled?.COMPLETED).toBe(3);
|
||||
expect(points.filter((p) => p.COMPLETED > 0)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses identity grouping when no groupFn is provided", () => {
|
||||
const from = new Date("2026-06-22T00:00:00.000Z");
|
||||
const to = new Date("2026-06-22T00:00:02.000Z");
|
||||
const at0 = Math.floor(from.getTime() / 1000);
|
||||
|
||||
const points = zeroFillGroupedSeries({
|
||||
rows: [{ bucket: at0, status: "ACTIVE", val: 7 }],
|
||||
from,
|
||||
to,
|
||||
bucketSeconds: 1,
|
||||
orderedKeys: ["ACTIVE", "CLOSED", "EXPIRED"] as const,
|
||||
});
|
||||
|
||||
expect(points).toHaveLength(2);
|
||||
expect(points[0].ACTIVE).toBe(7);
|
||||
expect(points[0].CLOSED).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("zeroFillScalarSeries", () => {
|
||||
it("zero-fills a single series", () => {
|
||||
const from = new Date("2026-06-22T00:00:00.000Z");
|
||||
const to = new Date("2026-06-22T00:00:03.000Z");
|
||||
const at1 = Math.floor(new Date("2026-06-22T00:00:01.000Z").getTime() / 1000);
|
||||
|
||||
const points = zeroFillScalarSeries({
|
||||
rows: [{ bucket: at1, val: 42 }],
|
||||
from,
|
||||
to,
|
||||
bucketSeconds: 1,
|
||||
seriesKey: "cost",
|
||||
});
|
||||
|
||||
expect(points).toHaveLength(3);
|
||||
expect(points.find((p) => p.bucket === at1 * 1000)?.cost).toBe(42);
|
||||
expect(points.filter((p) => p.cost > 0)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user