snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
746
apps/v4/lib/registry-health/monitor.test.ts
Normal file
746
apps/v4/lib/registry-health/monitor.test.ts
Normal file
@@ -0,0 +1,746 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import type { RegistryDirectoryEntry } from "../registry-directory"
|
||||
import { runRegistryMonitor } from "./monitor"
|
||||
import {
|
||||
REGISTRY_DRY_RUN_VERSION,
|
||||
type RegistryMonitorEntryState,
|
||||
type 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,
|
||||
dryRunVersion: REGISTRY_DRY_RUN_VERSION,
|
||||
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("discards invalid pre-tsconfig dry runs and reruns the weekly check", async () => {
|
||||
const now = new Date("2026-09-02T12:00:00.000Z")
|
||||
const recentIndex = Array.from({ length: 24 }, (_, index) => ({
|
||||
checkedAt: new Date(
|
||||
now.getTime() - (24 - index) * 60 * 60 * 1000
|
||||
).toISOString(),
|
||||
outcome: "reachable" as const,
|
||||
status: 200,
|
||||
durationMs: 100,
|
||||
redirectCount: 0,
|
||||
schemaValid: true,
|
||||
}))
|
||||
const previousState = {
|
||||
...createState(
|
||||
createEntryState({
|
||||
itemNames: ["button"],
|
||||
recentIndex,
|
||||
recentDryRuns: [
|
||||
{
|
||||
checkedAt: "2026-08-25T12:04:27.443Z",
|
||||
item: "button",
|
||||
success: false,
|
||||
failureCode: "exit_1",
|
||||
durationMs: 1500,
|
||||
},
|
||||
{
|
||||
checkedAt: "2026-08-31T12:38:36.084Z",
|
||||
item: "dialog",
|
||||
success: false,
|
||||
failureCode: "exit_1",
|
||||
durationMs: 1700,
|
||||
},
|
||||
],
|
||||
daily: [
|
||||
{
|
||||
date: "2026-08-25",
|
||||
availabilitySuccesses: 0,
|
||||
availabilityObservations: 0,
|
||||
challengeObservations: 0,
|
||||
schemaSuccesses: 0,
|
||||
schemaObservations: 0,
|
||||
itemSuccesses: 0,
|
||||
itemObservations: 0,
|
||||
dryRunSuccesses: 0,
|
||||
dryRunObservations: 1,
|
||||
},
|
||||
{
|
||||
date: "2026-08-31",
|
||||
availabilitySuccesses: 0,
|
||||
availabilityObservations: 0,
|
||||
challengeObservations: 0,
|
||||
schemaSuccesses: 0,
|
||||
schemaObservations: 0,
|
||||
itemSuccesses: 0,
|
||||
itemObservations: 0,
|
||||
dryRunSuccesses: 0,
|
||||
dryRunObservations: 1,
|
||||
},
|
||||
],
|
||||
latestHygiene: {
|
||||
contentTypeJson: true,
|
||||
noDuplicateNames: true,
|
||||
nameMatches: true,
|
||||
},
|
||||
})
|
||||
),
|
||||
dryRunVersion: 0,
|
||||
lastDailyRunAt: "2026-09-02T11:00:00.000Z",
|
||||
lastWeeklyRunAt: "2026-08-31T12:38:36.084Z",
|
||||
}
|
||||
const runDryRun = vi.fn(async () => ({
|
||||
success: true,
|
||||
durationMs: 250,
|
||||
}))
|
||||
|
||||
const result = await runRegistryMonitor({
|
||||
directory: DIRECTORY,
|
||||
previousState,
|
||||
mode: "auto",
|
||||
now,
|
||||
fetchJson: vi.fn(async () => createSuccessfulFetch()),
|
||||
runDryRun,
|
||||
})
|
||||
|
||||
expect(runDryRun).toHaveBeenCalledOnce()
|
||||
expect(result.state.registries["@acme"].recentDryRuns).toEqual([
|
||||
{
|
||||
checkedAt: now.toISOString(),
|
||||
item: "item-0",
|
||||
success: true,
|
||||
durationMs: 250,
|
||||
},
|
||||
])
|
||||
expect(result.state.registries["@acme"].daily.slice(0, 2)).toMatchObject([
|
||||
{ date: "2026-08-25", dryRunSuccesses: 0, dryRunObservations: 0 },
|
||||
{ date: "2026-08-31", dryRunSuccesses: 0, dryRunObservations: 0 },
|
||||
])
|
||||
expect(result.state.lastWeeklyRunAt).toBe(now.toISOString())
|
||||
expect(result.run.diagnostics).toEqual([
|
||||
"Discarded 2 invalid CLI dry-run observations from the pre-tsconfig scaffold.",
|
||||
])
|
||||
expect(result.snapshot.registries["@acme"]).toMatchObject({
|
||||
status: "healthy",
|
||||
breakdown: { installability: 18.5 },
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it("resolves {style} urls with the default style", async () => {
|
||||
const fetchJson = vi.fn(async () => createSuccessfulFetch())
|
||||
await runRegistryMonitor({
|
||||
directory: [
|
||||
{
|
||||
...DIRECTORY[0],
|
||||
url: "https://acme.example.com/r/{style}/{name}.json",
|
||||
},
|
||||
],
|
||||
mode: "daily",
|
||||
now: NOW,
|
||||
previousState: createState(createEntryState({ itemNames: ["button"] })),
|
||||
fetchJson,
|
||||
runDryRun: vi.fn(),
|
||||
})
|
||||
|
||||
expect(fetchJson).toHaveBeenCalledWith(
|
||||
"https://acme.example.com/r/radix-vega/button.json"
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user