134 lines
5.4 KiB
TypeScript
134 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import { ambiguousAttribution, formatPane, unify, type Signal } from '../src/index.js'
|
|
|
|
const NOW = '2026-08-01T12:00:00Z'
|
|
const fresh = '2026-08-01T11:58:00Z'
|
|
const old = '2026-08-01T09:00:00Z'
|
|
|
|
const sig = (p: Partial<Signal> = {}): Signal => ({
|
|
source: 'gatus',
|
|
subject: 'crm',
|
|
metric: 'availability',
|
|
value: 100,
|
|
health: 'up',
|
|
observedAt: fresh,
|
|
where: 'https://status.example.com',
|
|
...p,
|
|
})
|
|
|
|
describe('silence is not health', () => {
|
|
it('a stale panel is NOT ASSESSED, not green', () => {
|
|
// An error board can show zero events because ingestion stopped, not
|
|
// because nothing went wrong.
|
|
const r = unify([sig({ observedAt: old })], { now: NOW })
|
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
|
expect(overall(r)).toBe('not-assessed')
|
|
const text = formatPane([sig({ observedAt: old })], { now: NOW })
|
|
expect(text).toContain('Silence is not health')
|
|
expect(text).toContain('showing its last reading')
|
|
expect(text).not.toContain('✓')
|
|
})
|
|
|
|
it('passes a reading inside the freshness window', () => {
|
|
expect(overall(unify([sig()], { now: NOW }))).toBe('pass')
|
|
})
|
|
|
|
it('honours a custom freshness window', () => {
|
|
// 2 minutes old, with a 60-second window.
|
|
expect(overall(unify([sig()], { now: NOW, maxAgeSeconds: 60 }))).toBe('not-assessed')
|
|
})
|
|
|
|
it('treats an unstamped reading as indistinguishable from a stale one', () => {
|
|
const r = unify([sig({ observedAt: undefined })], { now: NOW })
|
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
|
expect(r.findings[0]!.detail).toContain('may be current; nothing here establishes that')
|
|
})
|
|
|
|
it('reports an expected subject with no signal at all', () => {
|
|
const r = unify([sig({ subject: 'crm' })], { now: NOW, expectedSubjects: ['crm', 'hr', 'desk'] })
|
|
const missing = r.findings.filter(f => f.id.endsWith('/missing'))
|
|
expect(missing).toHaveLength(2)
|
|
expect(missing[0]!.detail).toContain('green because nothing was ever wired to it')
|
|
})
|
|
})
|
|
|
|
describe('shared identity is not per-subject evidence', () => {
|
|
// Several products reporting through ONE error-tracking project. A
|
|
// per-product search returning nothing means "not separable", not "no errors".
|
|
const shared = [
|
|
sig({ source: 'sentry', subject: 'crm', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
|
sig({ source: 'sentry', subject: 'hr', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
|
sig({ source: 'sentry', subject: 'desk', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
|
]
|
|
|
|
it('names the entangled subjects rather than saying "ambiguous"', () => {
|
|
const groups = ambiguousAttribution(shared)
|
|
expect(groups).toHaveLength(1)
|
|
expect(groups[0]!.subjects).toEqual(['crm', 'desk', 'hr'])
|
|
})
|
|
|
|
it('refuses to read a shared-identity zero as a per-subject pass', () => {
|
|
const r = unify(shared, { now: NOW })
|
|
expect(overall(r)).toBe('not-assessed')
|
|
for (const f of r.findings.filter(f => f.id.startsWith('crm/'))) {
|
|
expect(f.determination).toBe('not-assessed')
|
|
}
|
|
})
|
|
|
|
it('says what a search returning nothing actually means', () => {
|
|
const text = formatPane(shared, { now: NOW })
|
|
expect(text).toContain('absence of attributed events is not absence of events')
|
|
expect(text).toContain('crm, desk, hr')
|
|
})
|
|
|
|
it('leaves a uniquely-attributed signal alone', () => {
|
|
const distinct = [
|
|
sig({ source: 'sentry', subject: 'crm', attributedTo: 'crm-prod' }),
|
|
sig({ source: 'sentry', subject: 'hr', attributedTo: 'hr-prod' }),
|
|
]
|
|
expect(ambiguousAttribution(distinct)).toEqual([])
|
|
expect(overall(unify(distinct, { now: NOW }))).toBe('pass')
|
|
})
|
|
|
|
it('does not entangle the same identity across different sources', () => {
|
|
// gatus/"prod" and sentry/"prod" are unrelated namespaces.
|
|
const r = ambiguousAttribution([
|
|
sig({ source: 'gatus', subject: 'crm', attributedTo: 'prod' }),
|
|
sig({ source: 'sentry', subject: 'hr', attributedTo: 'prod' }),
|
|
])
|
|
expect(r).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('health states', () => {
|
|
it('reports down as critical and degraded as high', () => {
|
|
expect(unify([sig({ health: 'down' })], { now: NOW }).findings[0]!.severity).toBe('critical')
|
|
expect(unify([sig({ health: 'degraded' })], { now: NOW }).findings[0]!.severity).toBe('high')
|
|
expect(overall(unify([sig({ health: 'down' })], { now: NOW }))).toBe('fail')
|
|
})
|
|
|
|
it('reports an uncomputable value as not-assessed, not zero', () => {
|
|
const r = unify([sig({ value: null, health: undefined })], { now: NOW })
|
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
|
})
|
|
|
|
it('carries the sample count into the citation for a null value', () => {
|
|
const text = formatPane([sig({ value: null, health: undefined })], { now: NOW, evidence: 'inline' })
|
|
expect(text).toContain('not computable')
|
|
expect(text).toContain('0 samples')
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('an empty board is not-assessed, never healthy', () => {
|
|
const r = unify([], { now: NOW })
|
|
expect(overall(r)).toBe('not-assessed')
|
|
expect(formatPane([], { now: NOW })).toContain('An empty board is not a healthy one')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(unify([sig(), sig({ subject: 'hr', health: 'down' })], { now: NOW }))).toEqual([])
|
|
})
|
|
})
|