133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
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);
|
|
});
|
|
});
|