394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
|
|
import type {
|
|
RegistryHealthDailyBucket,
|
|
RegistryIndexObservation,
|
|
RegistryMonitorEntryState,
|
|
} from "./schema"
|
|
import {
|
|
calculateRegistryHealth,
|
|
DEFAULT_GLOBAL_MEANS,
|
|
getRegistryHealthSignals,
|
|
} from "./score"
|
|
|
|
const NOW = new Date("2026-08-24T12:00:00.000Z")
|
|
|
|
function createDailyBucket(overrides: Partial<RegistryHealthDailyBucket> = {}) {
|
|
return {
|
|
date: "2026-08-24",
|
|
availabilitySuccesses: 0,
|
|
availabilityObservations: 0,
|
|
challengeObservations: 0,
|
|
schemaSuccesses: 0,
|
|
schemaObservations: 0,
|
|
itemSuccesses: 0,
|
|
itemObservations: 0,
|
|
dryRunSuccesses: 0,
|
|
dryRunObservations: 0,
|
|
...overrides,
|
|
} satisfies RegistryHealthDailyBucket
|
|
}
|
|
|
|
function createIndexObservations(
|
|
count = 25,
|
|
outcome: RegistryIndexObservation["outcome"] = "reachable"
|
|
) {
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
checkedAt: new Date(
|
|
NOW.getTime() - (count - index) * 60 * 60 * 1000
|
|
).toISOString(),
|
|
outcome,
|
|
durationMs: 100,
|
|
redirectCount: 0,
|
|
schemaValid: outcome === "reachable",
|
|
})) satisfies RegistryIndexObservation[]
|
|
}
|
|
|
|
function createState(overrides: Partial<RegistryMonitorEntryState> = {}) {
|
|
return {
|
|
firstObservedAt: "2026-08-22T10:00:00.000Z",
|
|
lastSuccessfulCheck: "2026-08-24T11:00:00.000Z",
|
|
status: "healthy",
|
|
consecutiveSuccessfulIndexes: 25,
|
|
consecutiveIndexFailures: 0,
|
|
availabilityRecoveryRequired: false,
|
|
itemCursor: 0,
|
|
itemNames: [],
|
|
recentIndex: createIndexObservations(),
|
|
recentDryRuns: [],
|
|
daily: [
|
|
createDailyBucket({
|
|
availabilitySuccesses: 25,
|
|
availabilityObservations: 25,
|
|
schemaSuccesses: 25,
|
|
schemaObservations: 25,
|
|
}),
|
|
],
|
|
latestHygiene: {
|
|
contentTypeJson: null,
|
|
noDuplicateNames: null,
|
|
nameMatches: null,
|
|
},
|
|
...overrides,
|
|
} satisfies RegistryMonitorEntryState
|
|
}
|
|
|
|
describe("calculateRegistryHealth", () => {
|
|
it("derives the published total from rounded components", () => {
|
|
const health = calculateRegistryHealth({
|
|
state: createState(),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.score).toBe(
|
|
health.breakdown.reliability +
|
|
health.breakdown.correctness +
|
|
health.breakdown.installability +
|
|
health.breakdown.hygiene
|
|
)
|
|
expect(health.score).toBeGreaterThanOrEqual(0)
|
|
expect(health.score).toBeLessThanOrEqual(100)
|
|
expect(health.statusReason).toEqual({
|
|
code: "healthy_thresholds",
|
|
message: "Recent checks are within healthy thresholds",
|
|
})
|
|
})
|
|
|
|
it("explains when baseline observations are still being collected", () => {
|
|
const health = calculateRegistryHealth({
|
|
state: createState({ recentIndex: createIndexObservations(3) }),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.status).toBe("observing")
|
|
expect(health.statusReason).toEqual({
|
|
code: "collecting_baseline",
|
|
message: "Collecting baseline data (3 of 24 checks)",
|
|
})
|
|
})
|
|
|
|
it("gives unknown hygiene signals half credit", () => {
|
|
const health = calculateRegistryHealth({
|
|
state: createState(),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.breakdown.hygiene).toBe(6.25)
|
|
})
|
|
|
|
it("uses a cadence-scaled prior for weekly dry runs", () => {
|
|
const state = createState({
|
|
daily: [
|
|
createDailyBucket({
|
|
availabilitySuccesses: 25,
|
|
availabilityObservations: 25,
|
|
schemaSuccesses: 25,
|
|
schemaObservations: 25,
|
|
dryRunSuccesses: 1,
|
|
dryRunObservations: 1,
|
|
}),
|
|
],
|
|
})
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.breakdown.installability).toBe(18.5)
|
|
})
|
|
|
|
it("excludes challenge observations from availability", () => {
|
|
const state = createState({
|
|
recentIndex: [
|
|
...createIndexObservations(),
|
|
{
|
|
checkedAt: NOW.toISOString(),
|
|
outcome: "bot_challenge",
|
|
durationMs: 100,
|
|
redirectCount: 0,
|
|
},
|
|
],
|
|
})
|
|
const signals = getRegistryHealthSignals(
|
|
state,
|
|
"https://example.com/{name}.json",
|
|
NOW
|
|
)
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(signals.availability7d).toEqual({
|
|
successes: 25,
|
|
observations: 25,
|
|
})
|
|
expect(health.monitoringLimited).toBe(true)
|
|
expect(health.status).toBe("healthy")
|
|
})
|
|
|
|
it("hides a registry only after seven days unavailable", () => {
|
|
const state = createState({
|
|
firstObservedAt: "2026-08-01T00:00:00.000Z",
|
|
lastSuccessfulCheck: "2026-08-16T00:00:00.000Z",
|
|
status: "unavailable",
|
|
consecutiveSuccessfulIndexes: 0,
|
|
consecutiveIndexFailures: 8,
|
|
availabilityOutageSince: "2026-08-16T00:00:00.000Z",
|
|
availabilityRecoveryRequired: true,
|
|
recentIndex: createIndexObservations(8, "unreachable"),
|
|
})
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.status).toBe("unavailable")
|
|
expect(health.statusReason).toEqual({
|
|
code: "index_unavailable",
|
|
message: "No successful index check in the last 24 hours",
|
|
})
|
|
expect(health.hidden).toBe(true)
|
|
})
|
|
|
|
it("keeps an unavailable registry visible before seven days", () => {
|
|
const state = createState({
|
|
firstObservedAt: "2026-08-01T00:00:00.000Z",
|
|
lastSuccessfulCheck: "2026-08-20T00:00:00.000Z",
|
|
status: "unavailable",
|
|
consecutiveSuccessfulIndexes: 0,
|
|
consecutiveIndexFailures: 8,
|
|
availabilityOutageSince: "2026-08-20T00:00:00.000Z",
|
|
availabilityRecoveryRequired: true,
|
|
recentIndex: createIndexObservations(8, "unreachable"),
|
|
})
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.status).toBe("unavailable")
|
|
expect(health.hidden).toBe(false)
|
|
})
|
|
|
|
it("keeps a long outage hidden through the first recovery success", () => {
|
|
const recovery = createIndexObservations(1)[0]
|
|
const state = createState({
|
|
firstObservedAt: "2026-08-01T00:00:00.000Z",
|
|
lastSuccessfulCheck: recovery.checkedAt,
|
|
status: "unavailable",
|
|
consecutiveSuccessfulIndexes: 1,
|
|
consecutiveIndexFailures: 0,
|
|
availabilityOutageSince: "2026-08-16T00:00:00.000Z",
|
|
availabilityRecoveryRequired: true,
|
|
recentIndex: [...createIndexObservations(24, "unreachable"), recovery],
|
|
})
|
|
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.status).toBe("unavailable")
|
|
expect(health.statusReason?.code).toBe("recovery_pending")
|
|
expect(health.hidden).toBe(true)
|
|
})
|
|
|
|
it("requires two successful checks to recover from availability degradation", () => {
|
|
const history = createIndexObservations()
|
|
const failures = createIndexObservations(3, "unreachable")
|
|
const successes = createIndexObservations(2)
|
|
const state = createState({
|
|
status: "degraded",
|
|
consecutiveSuccessfulIndexes: 1,
|
|
consecutiveIndexFailures: 0,
|
|
availabilityOutageSince: failures[0].checkedAt,
|
|
availabilityRecoveryRequired: true,
|
|
recentIndex: [...history, ...failures, successes[0]],
|
|
})
|
|
|
|
const recovering = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
const recovered = calculateRegistryHealth({
|
|
state: {
|
|
...state,
|
|
consecutiveSuccessfulIndexes: 2,
|
|
availabilityOutageSince: undefined,
|
|
availabilityRecoveryRequired: false,
|
|
recentIndex: [...state.recentIndex, successes[1]],
|
|
},
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(recovering.status).toBe("degraded")
|
|
expect(recovering.statusReason).toEqual({
|
|
code: "recovery_pending",
|
|
message: "Waiting for a second successful recovery check",
|
|
})
|
|
expect(recovered.status).toBe("healthy")
|
|
})
|
|
|
|
it("explains each degraded status condition", () => {
|
|
const failures = calculateRegistryHealth({
|
|
state: createState({
|
|
consecutiveSuccessfulIndexes: 0,
|
|
consecutiveIndexFailures: 3,
|
|
availabilityOutageSince: "2026-08-24T09:00:00.000Z",
|
|
availabilityRecoveryRequired: true,
|
|
recentIndex: [
|
|
...createIndexObservations(),
|
|
...createIndexObservations(3, "unreachable"),
|
|
],
|
|
}),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
const observations = createIndexObservations()
|
|
const invalidSchema = calculateRegistryHealth({
|
|
state: createState({
|
|
recentIndex: [
|
|
...observations.slice(0, -1),
|
|
{ ...observations.at(-1)!, schemaValid: false },
|
|
],
|
|
}),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
const invalidItems = calculateRegistryHealth({
|
|
state: createState({
|
|
daily: [
|
|
createDailyBucket({
|
|
availabilitySuccesses: 25,
|
|
availabilityObservations: 25,
|
|
schemaSuccesses: 25,
|
|
schemaObservations: 25,
|
|
itemSuccesses: 8,
|
|
itemObservations: 10,
|
|
}),
|
|
],
|
|
}),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
const dryRuns = calculateRegistryHealth({
|
|
state: createState({
|
|
recentDryRuns: [
|
|
{
|
|
checkedAt: "2026-08-17T12:00:00.000Z",
|
|
item: "button",
|
|
success: false,
|
|
durationMs: 100,
|
|
},
|
|
{
|
|
checkedAt: "2026-08-24T12:00:00.000Z",
|
|
item: "button",
|
|
success: false,
|
|
durationMs: 100,
|
|
},
|
|
],
|
|
}),
|
|
registryUrl: "https://example.com/r/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(failures.statusReason).toEqual({
|
|
code: "consecutive_index_failures",
|
|
message: "The registry index failed 3 consecutive checks",
|
|
})
|
|
expect(invalidSchema.statusReason).toEqual({
|
|
code: "index_schema_invalid",
|
|
message: "The registry index failed schema validation",
|
|
})
|
|
expect(invalidItems.statusReason).toEqual({
|
|
code: "item_validation_failures",
|
|
message: "Sampled registry items are failing validation",
|
|
})
|
|
expect(dryRuns.statusReason).toEqual({
|
|
code: "dry_run_failures",
|
|
message: "The last two CLI dry-run checks failed",
|
|
})
|
|
})
|
|
|
|
it("recovers correctness degradation when its condition clears", () => {
|
|
const state = createState({
|
|
status: "degraded",
|
|
recentIndex: createIndexObservations(),
|
|
})
|
|
|
|
const health = calculateRegistryHealth({
|
|
state,
|
|
registryUrl: "https://example.com/{name}.json",
|
|
globalMeans: DEFAULT_GLOBAL_MEANS,
|
|
now: NOW,
|
|
})
|
|
|
|
expect(health.status).toBe("healthy")
|
|
})
|
|
})
|