136 lines
5.6 KiB
TypeScript
136 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import { formatRollout, safeToProceed, verifyRollout, type Target } from '../src/index.js'
|
|
|
|
const shipped = (name: string, sha = 'a1b2c3d'): Target =>
|
|
({ name, expected: sha, observed: sha, via: 'artifact-probe', where: `https://${name}/__version` })
|
|
|
|
describe('a green pipeline is not a deployed artifact', () => {
|
|
it('CATCHES THE SKIPPED DEPLOY: deploy skipped, pipeline still green', () => {
|
|
// An unpinned linter failed, the deploy job's dependency was
|
|
// unsatisfied, and it was SKIPPED. A skipped job does not fail a
|
|
// pipeline, so nothing ships behind a green tick.
|
|
const t: Target = {
|
|
name: 'api',
|
|
expected: 'a1b2c3d',
|
|
observed: 'a1b2c3d',
|
|
via: 'artifact-probe',
|
|
where: 'gitlab pipelines/123',
|
|
pipelineStatus: 'success',
|
|
jobsRun: ['build', 'test', 'image-lint'],
|
|
}
|
|
const r = verifyRollout([t])
|
|
const f = r.findings.find(x => x.id === 'api/skipped-deploy')!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.severity).toBe('critical')
|
|
expect(f.detail).toContain('build, test, image-lint')
|
|
expect(overall(r)).toBe('fail')
|
|
})
|
|
|
|
it('does not fire when the deploy job actually ran', () => {
|
|
const t: Target = { ...shipped('api'), pipelineStatus: 'success', jobsRun: ['build', 'deploy'] }
|
|
expect(verifyRollout([t]).findings.some(f => f.id.endsWith('/skipped-deploy'))).toBe(false)
|
|
expect(overall(verifyRollout([t]))).toBe('pass')
|
|
})
|
|
|
|
it('honours a custom deploy job name', () => {
|
|
const t: Target = { ...shipped('api'), pipelineStatus: 'success', jobsRun: ['build', 'ship'] }
|
|
expect(verifyRollout([t], { deployJob: 'ship' }).findings.some(f => f.id.endsWith('/skipped-deploy'))).toBe(false)
|
|
expect(verifyRollout([t], { deployJob: 'deploy' }).findings.some(f => f.id.endsWith('/skipped-deploy'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('a 200 is not your build', () => {
|
|
it('refuses to accept an HTTP status as a deployment check', () => {
|
|
const t: Target = {
|
|
name: 'web', expected: 'a1b2c3d', observed: 'up', via: 'http-status',
|
|
httpStatus: 200, where: 'https://web.example.com/healthz',
|
|
}
|
|
const r = verifyRollout([t])
|
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
|
expect(r.findings[0]!.detail).toContain('passes on HTTP 500')
|
|
expect(overall(r)).toBe('not-assessed')
|
|
})
|
|
|
|
it('refuses to accept a pipeline status as a deployment check', () => {
|
|
const t: Target = {
|
|
name: 'worker', expected: 'a1b2c3d', observed: 'success', via: 'pipeline-status',
|
|
pipelineStatus: 'success', where: 'gitlab pipelines/9',
|
|
}
|
|
expect(verifyRollout([t]).findings[0]!.determination).toBe('not-assessed')
|
|
expect(formatRollout([t])).toContain('A green pipeline is not a deployed artifact')
|
|
})
|
|
|
|
it('CATCHES THE HEALTHY OLD BUILD — up, 200, and the previous version', () => {
|
|
const t: Target = {
|
|
name: 'billing', expected: 'a1b2c3d', observed: '9f8e7d6', via: 'artifact-probe',
|
|
where: 'https://billing.example.com/__version',
|
|
}
|
|
const f = verifyRollout([t]).findings[0]!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.summary).toContain('serving 9f8e7d6, expected a1b2c3d')
|
|
expect(f.detail).toContain('up, healthy, and running the previous build')
|
|
})
|
|
|
|
it('passes only on an artifact probe that matches', () => {
|
|
expect(overall(verifyRollout([shipped('api')]))).toBe('pass')
|
|
})
|
|
})
|
|
|
|
describe('an untouched target reports its previous success', () => {
|
|
it('reports a not-yet-started target as unassessed, not green', () => {
|
|
// Querying status for a repo not yet merged returns the PRE-rollout run.
|
|
const t: Target = {
|
|
name: 'search', expected: 'a1b2c3d', observed: 'success', via: 'pipeline-status',
|
|
pipelineStatus: 'success', where: 'gitlab pipelines/1', started: false,
|
|
}
|
|
const f = verifyRollout([t]).findings[0]!
|
|
expect(f.determination).toBe('not-assessed')
|
|
expect(f.detail).toContain('green because nothing has happened yet')
|
|
})
|
|
|
|
it('does not count an unstarted target as confirmed', () => {
|
|
const t: Target = { ...shipped('search'), started: false }
|
|
expect(formatRollout([t])).toContain('0 of 1 target(s) confirmed')
|
|
})
|
|
})
|
|
|
|
describe('safeToProceed blocks on unverified, not only on failed', () => {
|
|
it('proceeds when every target is confirmed by probe', () => {
|
|
const r = safeToProceed([shipped('a'), shipped('b')])
|
|
expect(r.ok).toBe(true)
|
|
expect(r.reason).toContain('All 2 target(s) confirmed')
|
|
})
|
|
|
|
it('blocks on a failure', () => {
|
|
expect(safeToProceed([shipped('a'), { name: 'b', expected: 'x', observed: 'y', via: 'artifact-probe', where: 'w' }]).ok).toBe(false)
|
|
})
|
|
|
|
it('blocks on unverified — "not checked" is not evidence it worked', () => {
|
|
const r = safeToProceed([
|
|
shipped('a'),
|
|
{ name: 'b', expected: 'x', observed: 'up', via: 'http-status', where: 'w' },
|
|
])
|
|
expect(r.ok).toBe(false)
|
|
expect(r.reason).toContain('is not evidence that it did')
|
|
})
|
|
|
|
it('blocks on an empty target list', () => {
|
|
expect(safeToProceed([]).ok).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('an empty rollout is not-assessed, never clean', () => {
|
|
expect(overall(verifyRollout([]))).toBe('not-assessed')
|
|
expect(formatRollout([])).not.toContain('✓')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(verifyRollout([
|
|
shipped('a'),
|
|
{ name: 'b', expected: 'x', observed: 'y', via: 'artifact-probe', where: 'w', pipelineStatus: 'success', jobsRun: ['build'] },
|
|
]))).toEqual([])
|
|
})
|
|
})
|