Files
shadcn__ui/apps/v4/lib/registry-health/monitor.test.ts
T
shadcn 352f2094bd feat(registry): add health monitoring (#11616)
* feat(registry): add health monitoring

* fix(registry): cache health directory route

* fix(registry): harden health state transitions
2026-08-25 15:49:59 +04:00

614 lines
18 KiB
TypeScript

import { describe, expect, it, vi } from "vitest"
import type { RegistryDirectoryEntry } from "../registry-directory"
import { runRegistryMonitor } from "./monitor"
import type { RegistryMonitorEntryState, RegistryMonitorState } from "./schema"
const NOW = new Date("2026-08-24T12:00:00.000Z")
const DIRECTORY: RegistryDirectoryEntry[] = [
{
name: "@acme",
homepage: "https://acme.example.com",
url: "https://acme.example.com/r/{name}.json",
description: "Acme components",
logo: "https://acme.example.com/logo.svg",
},
]
function createEntryState(overrides: Partial<RegistryMonitorEntryState> = {}) {
return {
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-24T10:00:00.000Z",
status: "healthy",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
itemCursor: 0,
itemNames: [],
recentIndex: [],
recentDryRuns: [],
daily: [],
latestHygiene: {
contentTypeJson: null,
noDuplicateNames: null,
nameMatches: null,
},
...overrides,
} satisfies RegistryMonitorEntryState
}
function createState(entry = createEntryState()) {
return {
schemaVersion: 1,
scoreVersion: 1,
updatedAt: "2026-08-24T10:00:00.000Z",
registries: { "@acme": entry },
} satisfies RegistryMonitorState
}
function createSuccessfulFetch(itemCount = 1) {
return {
ok: true as const,
json: {
name: "acme",
homepage: "https://acme.example.com",
items: Array.from({ length: itemCount }, (_, index) => ({
name: `item-${index}`,
type: "registry:ui",
files: [],
})),
},
status: 200,
durationMs: 120,
responseSize: 500,
contentType: "application/json; charset=utf-8",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
}
}
describe("runRegistryMonitor", () => {
it("records completion separately from the observation time", async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2026-08-24T12:05:00.000Z"))
try {
const result = await runRegistryMonitor({
directory: DIRECTORY,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
expect(result.run.startedAt).toBe(NOW.toISOString())
expect(result.run.completedAt).toBe("2026-08-24T12:05:00.000Z")
} finally {
vi.useRealTimers()
}
})
it("records a successful hourly index observation", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch(2)),
runDryRun: vi.fn(),
})
expect(result.run.totals).toMatchObject({
registries: 1,
reachable: 1,
unavailable: 0,
challenges: 0,
})
expect(result.state.registries["@acme"].itemNames).toEqual([
"item-0",
"item-1",
])
expect(result.snapshot.registries["@acme"]).toMatchObject({
status: "observing",
monitoringLimited: false,
checkedAt: NOW.toISOString(),
})
})
it("starts an outage from the last successful index check", async () => {
const lastSuccessfulCheck = "2026-08-23T11:00:00.000Z"
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck,
consecutiveSuccessfulIndexes: 24,
})
),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "timeout",
reachable: false,
botChallenge: false,
durationMs: 100,
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"]).toMatchObject({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 1,
availabilityOutageSince: lastSuccessfulCheck,
availabilityRecoveryRequired: true,
})
expect(result.snapshot.registries["@acme"].status).toBe("unavailable")
})
it("clears availability recovery after two valid index checks", async () => {
const outageSince = "2026-08-16T00:00:00.000Z"
const recentIndex = Array.from({ length: 24 }, (_, index) => ({
checkedAt: new Date(
NOW.getTime() - (24 - index) * 60 * 60 * 1000
).toISOString(),
outcome: "unreachable" as const,
durationMs: 100,
redirectCount: 0,
}))
const previousState = createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: "2026-08-15T23:00:00.000Z",
status: "unavailable",
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 24,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: true,
recentIndex,
})
)
const first = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
const recovering = first.state.registries["@acme"]
expect(recovering).toMatchObject({
consecutiveSuccessfulIndexes: 1,
consecutiveIndexFailures: 0,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: true,
})
expect(first.snapshot.registries["@acme"]).toMatchObject({
status: "unavailable",
hidden: true,
})
const second = await runRegistryMonitor({
directory: DIRECTORY,
previousState: first.state,
mode: "hourly",
now: new Date("2026-08-24T13:00:00.000Z"),
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
const recovered = second.state.registries["@acme"]
expect(recovered).toMatchObject({
consecutiveSuccessfulIndexes: 2,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
})
expect(recovered).not.toHaveProperty("availabilityOutageSince")
expect(second.snapshot.registries["@acme"].status).toBe("healthy")
})
it("records a challenge without lowering availability", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
const bucket = result.state.registries["@acme"].daily[0]
const state = result.state.registries["@acme"]
expect(bucket.challengeObservations).toBe(1)
expect(bucket.availabilityObservations).toBe(0)
expect(state).toMatchObject({
consecutiveSuccessfulIndexes: 0,
consecutiveIndexFailures: 0,
availabilityRecoveryRequired: false,
})
expect(state).not.toHaveProperty("availabilityOutageSince")
expect(result.snapshot.registries["@acme"].monitoringLimited).toBe(true)
})
it("allows an existing outage to become unavailable during a challenge", async () => {
const outageSince = "2026-08-23T11:00:00.000Z"
const previousState = createState(
createEntryState({
firstObservedAt: "2026-08-01T00:00:00.000Z",
lastSuccessfulCheck: outageSince,
consecutiveIndexFailures: 1,
availabilityOutageSince: outageSince,
recentIndex: [
{
checkedAt: outageSince,
outcome: "unreachable",
durationMs: 100,
redirectCount: 0,
},
],
})
)
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/registry.json",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"]).toMatchObject({
consecutiveIndexFailures: 1,
availabilityOutageSince: outageSince,
availabilityRecoveryRequired: false,
})
expect(result.snapshot.registries["@acme"]).toMatchObject({
status: "unavailable",
monitoringLimited: true,
})
})
it("refreshes hygiene after a reachable invalid index", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
latestHygiene: {
contentTypeJson: true,
noDuplicateNames: true,
nameMatches: true,
},
})
),
mode: "hourly",
now: NOW,
fetchJson: vi.fn(async () => ({
...createSuccessfulFetch(),
json: { invalid: true },
contentType: "text/plain",
})),
runDryRun: vi.fn(),
})
expect(result.state.registries["@acme"].latestHygiene).toEqual({
contentTypeJson: false,
noDuplicateNames: null,
nameMatches: null,
})
})
it("rotates enough daily items to cover a catalog in thirty days", async () => {
const itemNames = Array.from({ length: 61 }, (_, index) => `item-${index}`)
const fetchJson = vi.fn(async (url: string) => {
const item = url.match(/item-\d+/)?.[0] ?? "item-0"
return {
...createSuccessfulFetch(),
json: { name: item, type: "registry:ui", files: [] },
finalUrl: url,
}
})
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(createEntryState({ itemNames })),
mode: "daily",
now: NOW,
fetchJson,
runDryRun: vi.fn(),
})
expect(fetchJson).toHaveBeenCalledTimes(3)
expect(result.state.registries["@acme"].itemCursor).toBe(3)
expect(result.run.totals.itemChecks).toBe(3)
expect(result.run.results["@acme"].items).toHaveLength(3)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
})
it("excludes item challenges from validation rates", async () => {
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
itemNames: ["button"],
daily: [
{
date: "2026-08-24",
availabilitySuccesses: 0,
availabilityObservations: 0,
challengeObservations: 0,
schemaSuccesses: 0,
schemaObservations: 0,
itemSuccesses: 8,
itemObservations: 9,
dryRunSuccesses: 0,
dryRunObservations: 0,
},
],
})
),
mode: "daily",
now: NOW,
fetchJson: vi.fn(async () => ({
ok: false as const,
failureCode: "bot_challenge",
reachable: false,
botChallenge: true,
status: 403,
durationMs: 80,
responseSize: 200,
contentType: "text/html",
redirectCount: 0,
finalUrl: "https://acme.example.com/r/button.json",
})),
runDryRun: vi.fn(),
})
const bucket = result.state.registries["@acme"].daily[0]
expect(bucket.itemObservations).toBe(9)
expect(bucket.itemSuccesses).toBe(8)
expect(result.run.totals).toMatchObject({
challenges: 1,
itemChecks: 1,
})
})
it("runs one rotating weekly dry-run item", async () => {
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({ itemNames: ["button", "dialog"] })
),
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(runDryRun).toHaveBeenCalledWith({
namespace: "@acme",
item: "button",
registryUrl: DIRECTORY[0].url,
})
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.registries["@acme"].recentDryRuns).toHaveLength(1)
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("continues weekly rotation after dry-run history is trimmed", async () => {
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const recentDryRuns = Array.from({ length: 12 }, (_, index) => ({
checkedAt: `2026-08-${String(index + 1).padStart(2, "0")}T12:00:00.000Z`,
item: index === 11 ? "dialog" : "button",
success: true,
durationMs: 250,
}))
await runRegistryMonitor({
directory: DIRECTORY,
previousState: createState(
createEntryState({
itemNames: ["button", "dialog", "tooltip"],
recentDryRuns,
})
),
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(runDryRun).toHaveBeenCalledWith({
namespace: "@acme",
item: "tooltip",
registryUrl: DIRECTORY[0].url,
})
})
it("runs delayed auto checks using their last run timestamps", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-23T15:00:00.000Z",
lastWeeklyRunAt: "2026-08-18T12:00:00.000Z",
}
const fetchJson = vi.fn(async (url: string) =>
url.endsWith("/registry.json")
? createSuccessfulFetch()
: {
...createSuccessfulFetch(),
json: { name: "item-0", type: "registry:ui", files: [] },
finalUrl: url,
}
)
const runDryRun = vi.fn(async () => ({
success: true,
durationMs: 250,
}))
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson,
runDryRun,
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("skips auto checks until their elapsed intervals", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-23T17:00:00.000Z",
lastWeeklyRunAt: "2026-08-19T12:00:00.000Z",
}
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(),
})
expect(result.run.totals.itemChecks).toBe(0)
expect(result.run.totals.dryRuns).toBe(0)
expect(result.state.lastDailyRunAt).toBe(previousState.lastDailyRunAt)
expect(result.state.lastWeeklyRunAt).toBe(previousState.lastWeeklyRunAt)
})
it("runs auto checks for state without last run timestamps", async () => {
const previousState = createState(
createEntryState({ itemNames: ["button"] })
)
const fetchJson = vi.fn(async (url: string) =>
url.endsWith("/registry.json")
? createSuccessfulFetch()
: {
...createSuccessfulFetch(),
json: { name: "item-0", type: "registry:ui", files: [] },
finalUrl: url,
}
)
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson,
runDryRun: vi.fn(async () => ({
success: true,
durationMs: 250,
})),
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("runs auto checks when last run timestamps are in the future", async () => {
const previousState = {
...createState(createEntryState({ itemNames: ["button"] })),
lastDailyRunAt: "2026-08-25T12:00:00.000Z",
lastWeeklyRunAt: "2026-08-25T12:00:00.000Z",
}
const result = await runRegistryMonitor({
directory: DIRECTORY,
previousState,
mode: "auto",
now: NOW,
fetchJson: vi.fn(async () => createSuccessfulFetch()),
runDryRun: vi.fn(async () => ({
success: true,
durationMs: 250,
})),
})
expect(result.run.totals.itemChecks).toBe(1)
expect(result.run.totals.dryRuns).toBe(1)
expect(result.state.lastDailyRunAt).toBe(NOW.toISOString())
expect(result.state.lastWeeklyRunAt).toBe(NOW.toISOString())
})
it("limits concurrent CLI dry runs to four", async () => {
const directory = Array.from({ length: 6 }, (_, index) => ({
name: `@registry${index}`,
homepage: `https://registry${index}.example.com`,
url: `https://registry${index}.example.com/r/{name}.json`,
description: `Registry ${index}`,
logo: `https://registry${index}.example.com/logo.svg`,
}))
const previousState: RegistryMonitorState = {
schemaVersion: 1,
scoreVersion: 1,
updatedAt: "2026-08-24T10:00:00.000Z",
registries: Object.fromEntries(
directory.map((entry) => [
entry.name,
createEntryState({ itemNames: ["button"] }),
])
),
}
let active = 0
let maximumActive = 0
const runDryRun = vi.fn(async () => {
active += 1
maximumActive = Math.max(maximumActive, active)
await new Promise((resolve) => setTimeout(resolve, 20))
active -= 1
return { success: true, durationMs: 20 }
})
const result = await runRegistryMonitor({
directory,
previousState,
mode: "weekly",
now: NOW,
fetchJson: vi.fn(),
runDryRun,
})
expect(result.run.totals.dryRuns).toBe(6)
expect(maximumActive).toBe(4)
})
})