799 lines
22 KiB
TypeScript
799 lines
22 KiB
TypeScript
import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
|
|
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
|
|
import {
|
|
createDashboardTileCache,
|
|
dashboardTileCacheKey,
|
|
handleDashboardEventsRoute,
|
|
handleDashboardTilesRoute,
|
|
handleDashboardTileRoute,
|
|
loadDashboardResponse,
|
|
loadDashboardTilesResponse,
|
|
loadDashboardTileResponse,
|
|
type DashboardTileResolutionLogEvent,
|
|
} from "./dashboard";
|
|
|
|
describe("dashboard API route", () => {
|
|
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
|
|
|
afterEach(() => {
|
|
consoleInfo.mockClear();
|
|
});
|
|
|
|
afterAll(() => {
|
|
consoleInfo.mockRestore();
|
|
});
|
|
|
|
test("returns the ready dashboard shell without hydrating live datasources", async () => {
|
|
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
|
|
new Error("live datasource fetch should not run for the shell response"),
|
|
);
|
|
|
|
const response = await loadDashboardResponse({
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
});
|
|
|
|
expect(response.state).toBe("ready");
|
|
if (response.state !== "ready") throw new Error("expected ready dashboard");
|
|
expect(response.document.metadata.title).toBe(
|
|
dimensionLabDashboardFixture.metadata.title,
|
|
);
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
|
|
fetch.mockRestore();
|
|
});
|
|
|
|
test("reports when client-side live hydration is disabled", async () => {
|
|
const previous = process.env.DISABLE_LIVE_DATASOURCES;
|
|
process.env.DISABLE_LIVE_DATASOURCES = "1";
|
|
|
|
try {
|
|
const response = await loadDashboardResponse({
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
});
|
|
|
|
expect(response.state).toBe("ready");
|
|
if (response.state !== "ready") throw new Error("expected ready dashboard");
|
|
expect(response.liveDatasourceHydration).toEqual({ enabled: false });
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.DISABLE_LIVE_DATASOURCES;
|
|
} else {
|
|
process.env.DISABLE_LIVE_DATASOURCES = previous;
|
|
}
|
|
}
|
|
});
|
|
|
|
test("hydrates a telemetry tile independently from the dashboard shell", async () => {
|
|
const fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
|
|
if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
|
|
return jsonResponse({
|
|
status: "success",
|
|
data: {
|
|
result: [
|
|
{
|
|
metric: { host: "linux-infra" },
|
|
values: [
|
|
[1771430000, "10"],
|
|
[1771430060, "20"],
|
|
[1771430120, "42"],
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
if (url.startsWith("https://prometheus.example/api/v1/query")) {
|
|
return jsonResponse({
|
|
status: "success",
|
|
data: {
|
|
result: [
|
|
{
|
|
metric: { host: "linux-infra" },
|
|
value: [1771430400, "42"],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unhandled test request: ${url}`);
|
|
});
|
|
|
|
const response = await loadDashboardTileResponse(
|
|
{ kind: "telemetry", id: "infra-ram" },
|
|
{
|
|
fetch,
|
|
prometheusBaseUrl: "https://prometheus.example",
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
},
|
|
);
|
|
|
|
expect(response.state).toBe("ready");
|
|
if (response.state !== "ready") throw new Error("expected ready tile");
|
|
expect(response.tile).toEqual({ kind: "telemetry", id: "infra-ram" });
|
|
expect(response.item).toMatchObject({
|
|
id: "infra-ram",
|
|
value: { kind: "percent", value: 42 },
|
|
severity: "ok",
|
|
detail: "linux-infra",
|
|
sparkline: [10, 20, 42],
|
|
});
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/v1/query?"),
|
|
expect.objectContaining({ cache: "no-store" }),
|
|
);
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/v1/query_range?"),
|
|
expect.objectContaining({ cache: "no-store" }),
|
|
);
|
|
});
|
|
|
|
test("hydrates a service tile with its service group identity", async () => {
|
|
const fetch = vi.fn(async () =>
|
|
jsonResponse({
|
|
status: "UP",
|
|
ping: 42,
|
|
}),
|
|
);
|
|
|
|
const response = await loadDashboardTileResponse(
|
|
{ kind: "service", groupId: "essentials", id: "vaultwarden" },
|
|
{
|
|
fetch,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
},
|
|
);
|
|
|
|
expect(response.state).toBe("ready");
|
|
if (response.state !== "ready") throw new Error("expected ready tile");
|
|
expect(response.tile).toEqual({
|
|
kind: "service",
|
|
groupId: "essentials",
|
|
id: "vaultwarden",
|
|
});
|
|
expect(response.item).toMatchObject({
|
|
id: "vaultwarden",
|
|
severity: "ok",
|
|
detail: "42 ms",
|
|
});
|
|
});
|
|
|
|
test("caches ready tile responses until the tile ttl expires", async () => {
|
|
const cache = createDashboardTileCache();
|
|
let now = 1_000;
|
|
const fetch = vi.fn(async () => {
|
|
now += 7;
|
|
return jsonResponse({
|
|
status: "UP",
|
|
ping: 42,
|
|
});
|
|
});
|
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
|
|
const first = await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
const second = await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now + 29_999,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
now += 30_001;
|
|
const third = await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
|
|
expect(first).toEqual(second);
|
|
expect(third.state).toBe("ready");
|
|
expect(fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test("logs tile duration and cache hit or miss metadata", async () => {
|
|
let now = 1_000;
|
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
const service = dimensionLabDashboardFixture.serviceGroups
|
|
.flatMap((group) => group.services)
|
|
.find((item) => item.id === tile.id);
|
|
if (!service) throw new Error("missing service fixture");
|
|
|
|
let cacheCalls = 0;
|
|
const tileCache = {
|
|
async resolve() {
|
|
cacheCalls += 1;
|
|
if (cacheCalls === 1) now += 7;
|
|
const cacheState = cacheCalls === 1 ? "miss" as const : "hit" as const;
|
|
|
|
return {
|
|
cache: cacheState,
|
|
coalesced: false,
|
|
response: {
|
|
state: "ready" as const,
|
|
tile,
|
|
item: service,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
await loadDashboardTileResponse(tile, {
|
|
logTileResolution: (event) => logs.push(event),
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache,
|
|
});
|
|
now += 10;
|
|
await loadDashboardTileResponse(tile, {
|
|
logTileResolution: (event) => logs.push(event),
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache,
|
|
});
|
|
|
|
expect(logs).toEqual([
|
|
expect.objectContaining({
|
|
cache: "miss",
|
|
coalesced: false,
|
|
durationMs: 7,
|
|
status: "ready",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
expect.objectContaining({
|
|
cache: "hit",
|
|
coalesced: false,
|
|
durationMs: 0,
|
|
status: "ready",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
test("keeps telemetry tiles cached for fifteen seconds", async () => {
|
|
const cache = createDashboardTileCache();
|
|
let now = 1_000;
|
|
const fetch = telemetryFetch();
|
|
const tile = { kind: "telemetry", id: "infra-ram" } as const;
|
|
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
prometheusBaseUrl: "https://prometheus.example",
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now + 14_999,
|
|
prometheusBaseUrl: "https://prometheus.example",
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
now += 15_001;
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
prometheusBaseUrl: "https://prometheus.example",
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
test("keeps weather module tiles cached for ten minutes", async () => {
|
|
const cache = createDashboardTileCache();
|
|
let now = 1_000;
|
|
const fetch = vi.fn(async () =>
|
|
jsonResponse({
|
|
current: {
|
|
apparent_temperature: 19,
|
|
temperature_2m: 20,
|
|
weather_code: 0,
|
|
wind_speed_10m: 11,
|
|
},
|
|
}),
|
|
);
|
|
const tile = { kind: "module", id: "weather-amsterdam" } as const;
|
|
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now + 599_999,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
now += 600_001;
|
|
await loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
now: () => now,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test("coalesces concurrent tile requests for the same cache key", async () => {
|
|
const cache = createDashboardTileCache();
|
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
|
let resolveFetch: ((response: Response) => void) | undefined;
|
|
const fetch = vi.fn(() =>
|
|
new Promise<Response>((resolve) => {
|
|
resolveFetch = resolve;
|
|
})
|
|
);
|
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
|
|
const first = loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
logTileResolution: (event) => logs.push(event),
|
|
now: () => 1_000,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
const second = loadDashboardTileResponse(tile, {
|
|
fetch,
|
|
logTileResolution: (event) => logs.push(event),
|
|
now: () => 1_000,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
});
|
|
|
|
await Promise.resolve();
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
|
|
resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
|
|
expect(await first).toEqual(await second);
|
|
expect(logs).toEqual([
|
|
expect.objectContaining({
|
|
cache: "miss",
|
|
coalesced: false,
|
|
status: "ready",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
expect.objectContaining({
|
|
cache: "miss",
|
|
coalesced: true,
|
|
status: "ready",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
test("logs coalesced metadata when shared tile requests fail", async () => {
|
|
const cache = createDashboardTileCache();
|
|
let rejectLoad: ((error: Error) => void) | undefined;
|
|
const load = vi.fn(() =>
|
|
new Promise<never>((_resolve, reject) => {
|
|
rejectLoad = reject;
|
|
})
|
|
);
|
|
|
|
const first = cache.resolve("tile-a", 30_000, 1_000, load);
|
|
const second = cache.resolve("tile-a", 30_000, 1_000, load);
|
|
|
|
await Promise.resolve();
|
|
expect(load).toHaveBeenCalledTimes(1);
|
|
|
|
rejectLoad?.(new TypeError("upstream failed"));
|
|
await expect(first).rejects.toThrow("upstream failed");
|
|
await expect(second).rejects.toMatchObject({
|
|
cache: "miss",
|
|
coalesced: true,
|
|
cause: expect.any(TypeError),
|
|
});
|
|
});
|
|
|
|
test("logs coalesced metadata for failed tile cache resolution", async () => {
|
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
|
|
await expect(
|
|
loadDashboardTileResponse(tile, {
|
|
logTileResolution: (event) => logs.push(event),
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: {
|
|
async resolve() {
|
|
throw {
|
|
cache: "miss",
|
|
cause: new TypeError("coalesced cache failed"),
|
|
coalesced: true,
|
|
};
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toThrow("coalesced cache failed");
|
|
|
|
expect(logs).toEqual([
|
|
expect.objectContaining({
|
|
cache: "miss",
|
|
coalesced: true,
|
|
errorCategory: "TypeError",
|
|
status: "error",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
test("logs error categories for failed tile cache resolution", async () => {
|
|
const logs: DashboardTileResolutionLogEvent[] = [];
|
|
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
|
|
|
|
await expect(
|
|
loadDashboardTileResponse(tile, {
|
|
logTileResolution: (event) => logs.push(event),
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: {
|
|
async resolve() {
|
|
throw new TypeError("cache failed");
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toThrow("cache failed");
|
|
|
|
expect(logs).toEqual([
|
|
expect.objectContaining({
|
|
cache: "miss",
|
|
coalesced: false,
|
|
errorCategory: "TypeError",
|
|
status: "error",
|
|
tileKey: dashboardTileCacheKey(tile),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
test("uses structured tile cache keys when identifiers contain delimiters", () => {
|
|
expect(
|
|
dashboardTileCacheKey({ kind: "service", groupId: "a:b", id: "c" }),
|
|
).not.toBe(
|
|
dashboardTileCacheKey({ kind: "service", groupId: "a", id: "b:c" }),
|
|
);
|
|
expect(
|
|
dashboardTileCacheKey({ kind: "status", stripId: "a:b", id: "c" }),
|
|
).not.toBe(
|
|
dashboardTileCacheKey({ kind: "status", stripId: "a", id: "b:c" }),
|
|
);
|
|
});
|
|
|
|
test("uses thirty-second status aggregate and five-minute static status ttl buckets", async () => {
|
|
const cache = createDashboardTileCache();
|
|
const fetch = vi.fn(async () => jsonResponse({ status: "UP", ping: 42 }));
|
|
const serviceCheckCount = dimensionLabDashboardFixture.serviceGroups
|
|
.flatMap((group) => group.services).length;
|
|
|
|
await loadDashboardTileResponse(
|
|
{ kind: "status", stripId: "footer-status", id: "system-status" },
|
|
{
|
|
fetch,
|
|
now: () => 1_000,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
},
|
|
);
|
|
await loadDashboardTileResponse(
|
|
{ kind: "status", stripId: "footer-status", id: "system-status" },
|
|
{
|
|
fetch,
|
|
now: () => 31_001,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
},
|
|
);
|
|
|
|
await loadDashboardTileResponse(
|
|
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
{
|
|
fetch,
|
|
now: () => 1_000,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
},
|
|
);
|
|
await loadDashboardTileResponse(
|
|
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
{
|
|
fetch,
|
|
now: () => 300_999,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
},
|
|
);
|
|
await loadDashboardTileResponse(
|
|
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
|
|
{
|
|
fetch,
|
|
now: () => 301_001,
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: cache,
|
|
},
|
|
);
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(serviceCheckCount * 2);
|
|
});
|
|
|
|
test("serves tile route responses with short private cache headers", async () => {
|
|
const response = await handleDashboardTileRoute(
|
|
"/api/dashboard/tile/service/essentials/vaultwarden",
|
|
{
|
|
fetch: vi.fn(async () => jsonResponse({ status: "UP", ping: 42 })),
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: createDashboardTileCache(),
|
|
},
|
|
);
|
|
|
|
expect(response.headers.get("cache-control")).toBe(
|
|
"private, max-age=5, stale-while-revalidate=30",
|
|
);
|
|
});
|
|
|
|
test("serves batch tile route responses", async () => {
|
|
const response = await handleDashboardTilesRoute(
|
|
new Request("https://example.test/api/dashboard/tiles", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
tiles: [
|
|
{
|
|
kind: "status",
|
|
stripId: "footer-status",
|
|
id: "auto-refresh",
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
{
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: createDashboardTileCache(),
|
|
},
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("cache-control")).toBe(
|
|
"private, max-age=5, stale-while-revalidate=30",
|
|
);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
state: "ready",
|
|
tiles: [
|
|
{
|
|
state: "ready",
|
|
tile: {
|
|
kind: "status",
|
|
stripId: "footer-status",
|
|
id: "auto-refresh",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("streams ready dashboard tile events", async () => {
|
|
const controller = new AbortController();
|
|
const response = await handleDashboardEventsRoute({
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
signal: controller.signal,
|
|
tileCache: {
|
|
async resolve(key) {
|
|
controller.abort();
|
|
const tile = JSON.parse(key);
|
|
|
|
return {
|
|
cache: "miss",
|
|
coalesced: false,
|
|
response: {
|
|
state: "ready",
|
|
tile,
|
|
item: {
|
|
id: tile.id,
|
|
label: "Status",
|
|
severity: "ok",
|
|
value: "ok",
|
|
},
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.headers.get("content-type")).toBe(
|
|
"text/event-stream; charset=utf-8",
|
|
);
|
|
expect(response.headers.get("cache-control")).toBe("no-cache");
|
|
const body = await response.text();
|
|
expect(body).toContain("event: dashboard-tile");
|
|
expect(body).toContain('"state":"ready"');
|
|
expect(body).toContain('"tile"');
|
|
});
|
|
|
|
test("rejects invalid batch tile requests", async () => {
|
|
const response = await handleDashboardTilesRoute(
|
|
new Request("https://example.test/api/dashboard/tiles", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
tiles: [{ kind: "service", id: "missing-group" }],
|
|
}),
|
|
}),
|
|
);
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
test("resolves batch tile responses with server concurrency capped at six", async () => {
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
const started: string[] = [];
|
|
const releases = new Map<string, () => void>();
|
|
const service = dimensionLabDashboardFixture.serviceGroups
|
|
.flatMap((group) => group.services)[0];
|
|
if (!service) throw new Error("missing service fixture");
|
|
const tiles = Array.from({ length: 7 }, (_, index) => ({
|
|
kind: "service" as const,
|
|
groupId: "essentials",
|
|
id: `service-${index}`,
|
|
}));
|
|
|
|
const batch = loadDashboardTilesResponse(tiles, {
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
tileCache: {
|
|
async resolve(key) {
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
started.push(key);
|
|
await new Promise<void>((resolve) => releases.set(key, resolve));
|
|
active -= 1;
|
|
|
|
return {
|
|
cache: "miss",
|
|
coalesced: false,
|
|
response: {
|
|
state: "ready",
|
|
tile: JSON.parse(key),
|
|
item: service,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
await waitFor(() => started.length === 6);
|
|
expect(maxActive).toBe(6);
|
|
releases.get(started[0])?.();
|
|
await waitFor(() => started.length === 7);
|
|
for (const release of releases.values()) release();
|
|
|
|
await expect(batch).resolves.toMatchObject({
|
|
state: "ready",
|
|
tiles: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
state: "ready",
|
|
tile: tiles[0],
|
|
}),
|
|
]),
|
|
});
|
|
expect(maxActive).toBe(6);
|
|
});
|
|
|
|
test("does not hydrate tile routes when live datasources are disabled", async () => {
|
|
const previous = process.env.DISABLE_LIVE_DATASOURCES;
|
|
process.env.DISABLE_LIVE_DATASOURCES = "1";
|
|
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
|
|
new Error("live datasource fetch should not run when disabled"),
|
|
);
|
|
|
|
try {
|
|
const response = await loadDashboardTileResponse(
|
|
{ kind: "telemetry", id: "infra-ram" },
|
|
{
|
|
refreshSeedDocument: true,
|
|
seedIfEmpty: true,
|
|
},
|
|
);
|
|
|
|
expect(response.state).toBe("disabled");
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
} finally {
|
|
fetch.mockRestore();
|
|
if (previous === undefined) {
|
|
delete process.env.DISABLE_LIVE_DATASOURCES;
|
|
} else {
|
|
process.env.DISABLE_LIVE_DATASOURCES = previous;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
function jsonResponse(payload: unknown): Response {
|
|
return new Response(JSON.stringify(payload), {
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
async function waitFor(predicate: () => boolean) {
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
if (predicate()) return;
|
|
await Promise.resolve();
|
|
}
|
|
|
|
throw new Error("condition was not met");
|
|
}
|
|
|
|
function telemetryFetch() {
|
|
return vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
|
|
if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
|
|
return jsonResponse({
|
|
status: "success",
|
|
data: {
|
|
result: [
|
|
{
|
|
metric: { host: "linux-infra" },
|
|
values: [
|
|
[1771430000, "10"],
|
|
[1771430060, "20"],
|
|
[1771430120, "42"],
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
if (url.startsWith("https://prometheus.example/api/v1/query")) {
|
|
return jsonResponse({
|
|
status: "success",
|
|
data: {
|
|
result: [
|
|
{
|
|
metric: { host: "linux-infra" },
|
|
value: [1771430400, "42"],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unhandled test request: ${url}`);
|
|
});
|
|
}
|