157 lines
4.7 KiB
TypeScript
157 lines
4.7 KiB
TypeScript
|
|
import { test } from 'node:test'
|
||
|
|
import assert from 'node:assert'
|
||
|
|
import Fastify from 'fastify'
|
||
|
|
import * as fc from 'fast-check'
|
||
|
|
import apophisPlugin from '../index.js'
|
||
|
|
import { createChaosEventArbitrary } from '../quality/chaos-v3.js'
|
||
|
|
import { FlakeDetector } from '../quality/flake.js'
|
||
|
|
|
||
|
|
process.env.NODE_ENV = 'test'
|
||
|
|
|
||
|
|
test('chaos: no events means no disruption', async (t) => {
|
||
|
|
const fastify = Fastify()
|
||
|
|
await fastify.register(apophisPlugin, { runtime: 'off' })
|
||
|
|
|
||
|
|
fastify.get('/ping', {
|
||
|
|
schema: {
|
||
|
|
'x-category': 'utility',
|
||
|
|
'x-ensures': ['status:200', 'response_body(this).pong == true'],
|
||
|
|
response: { 200: { type: 'object', properties: { pong: { type: 'boolean' } } } }
|
||
|
|
}
|
||
|
|
}, async () => {
|
||
|
|
return { pong: true }
|
||
|
|
})
|
||
|
|
|
||
|
|
await fastify.ready()
|
||
|
|
t.after(() => fastify.close())
|
||
|
|
|
||
|
|
const result = await (fastify as any).apophis.contract({
|
||
|
|
runs: 5,
|
||
|
|
chaos: { probability: 0 }
|
||
|
|
})
|
||
|
|
assert.strictEqual(result.summary.failed, 0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: error injection breaks contracts', async (t) => {
|
||
|
|
const fastify = Fastify()
|
||
|
|
await fastify.register(apophisPlugin, { runtime: 'off' })
|
||
|
|
|
||
|
|
fastify.post('/critical', {
|
||
|
|
schema: {
|
||
|
|
'x-category': 'constructor',
|
||
|
|
'x-ensures': ['status:201', 'response_body(this).id != null'],
|
||
|
|
body: { type: 'object', properties: { x: { type: 'number' } } },
|
||
|
|
response: { 201: { type: 'object', properties: { id: { type: 'string' } } } }
|
||
|
|
}
|
||
|
|
}, async (req, reply) => {
|
||
|
|
reply.status(201)
|
||
|
|
return { id: 'c-1' }
|
||
|
|
})
|
||
|
|
|
||
|
|
await fastify.ready()
|
||
|
|
t.after(() => fastify.close())
|
||
|
|
|
||
|
|
const result = await (fastify as any).apophis.contract({
|
||
|
|
runs: 10,
|
||
|
|
chaos: {
|
||
|
|
probability: 1,
|
||
|
|
error: { probability: 1, statusCode: 503 }
|
||
|
|
}
|
||
|
|
})
|
||
|
|
assert.ok(result.summary.failed > 0, 'chaos error injection should cause contract failures')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: createChaosEventArbitrary determinism with fixed seed', async () => {
|
||
|
|
const arb = createChaosEventArbitrary(
|
||
|
|
{
|
||
|
|
probability: 0.5,
|
||
|
|
error: { probability: 0.5, statusCode: 503 },
|
||
|
|
delay: { probability: 0.5, minMs: 0, maxMs: 100 },
|
||
|
|
},
|
||
|
|
['test.contract']
|
||
|
|
)
|
||
|
|
|
||
|
|
const events1 = fc.sample(arb, { numRuns: 1, seed: 42 })[0]!
|
||
|
|
const events2 = fc.sample(arb, { numRuns: 1, seed: 42 })[0]!
|
||
|
|
assert.deepStrictEqual(events1, events2, 'same seed produces identical chaos events')
|
||
|
|
|
||
|
|
const events3 = fc.sample(arb, { numRuns: 1, seed: 43 })[0]!
|
||
|
|
assert.deepStrictEqual(events1, events2)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: FlakeDetector classifies deterministic failures as not flaky', async () => {
|
||
|
|
let callCount = 0
|
||
|
|
const rerunFn = async (seed?: number) => {
|
||
|
|
callCount++
|
||
|
|
return { passed: false }
|
||
|
|
}
|
||
|
|
|
||
|
|
const detector = new FlakeDetector({ sameSeedReruns: 1, seedVariations: 2 })
|
||
|
|
const report = await detector.detectFlake(
|
||
|
|
{ ok: false, name: 'test', id: 1 },
|
||
|
|
rerunFn,
|
||
|
|
42
|
||
|
|
)
|
||
|
|
assert.strictEqual(report.isFlaky, false)
|
||
|
|
assert.strictEqual(report.confidence, 'high')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: FlakeDetector with varying seeds detects intermittent failures', async () => {
|
||
|
|
let flip = false
|
||
|
|
const rerunFn = async (seed?: number) => {
|
||
|
|
flip = !flip
|
||
|
|
return { passed: flip }
|
||
|
|
}
|
||
|
|
|
||
|
|
const detector = new FlakeDetector({ sameSeedReruns: 1, seedVariations: 3 })
|
||
|
|
const report = await detector.detectFlake(
|
||
|
|
{ ok: false, name: 'test', id: 1 },
|
||
|
|
rerunFn,
|
||
|
|
42
|
||
|
|
)
|
||
|
|
assert.strictEqual(report.isFlaky, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: chaos events with no strategies produce only none events', async () => {
|
||
|
|
const arb = createChaosEventArbitrary(
|
||
|
|
{ probability: 0.5 },
|
||
|
|
['svc.users']
|
||
|
|
)
|
||
|
|
const events = fc.sample(arb, { numRuns: 1, seed: 1 })[0]!
|
||
|
|
assert.strictEqual(events.filter((e: any) => e.type !== 'none').length, 0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: chaos events with error strategy generates non-none events', async () => {
|
||
|
|
const arb = createChaosEventArbitrary(
|
||
|
|
{ probability: 1, error: { probability: 1, statusCode: 503 } },
|
||
|
|
['svc.orders']
|
||
|
|
)
|
||
|
|
const samples = fc.sample(arb, { numRuns: 10, seed: 123 })
|
||
|
|
const activeEvents = samples.flat().filter((e: any) => e.type !== 'none')
|
||
|
|
assert.ok(activeEvents.length > 0, 'error strategy should produce chaos events')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('chaos: contract runner with chaos no-ops ignores none events', async (t) => {
|
||
|
|
const fastify = Fastify()
|
||
|
|
await fastify.register(apophisPlugin, { runtime: 'off' })
|
||
|
|
|
||
|
|
fastify.get('/stable', {
|
||
|
|
schema: {
|
||
|
|
'x-category': 'observer',
|
||
|
|
'x-ensures': ['status:200', 'response_body(this).stable == true'],
|
||
|
|
response: { 200: { type: 'object', properties: { stable: { type: 'boolean' } } } }
|
||
|
|
}
|
||
|
|
}, async () => {
|
||
|
|
return { stable: true }
|
||
|
|
})
|
||
|
|
|
||
|
|
await fastify.ready()
|
||
|
|
t.after(() => fastify.close())
|
||
|
|
|
||
|
|
const result = await (fastify as any).apophis.contract({
|
||
|
|
runs: 10,
|
||
|
|
chaos: { probability: 0 }
|
||
|
|
})
|
||
|
|
assert.strictEqual(result.summary.failed, 0)
|
||
|
|
})
|