diff --git a/snapshot/src/core/checks.ts b/snapshot/src/core/checks.ts new file mode 100644 index 0000000..d2fc97b --- /dev/null +++ b/snapshot/src/core/checks.ts @@ -0,0 +1,36 @@ +export interface CheckInput { + relaysQueried: number + relaysResponded: number + eventCount: number + minEvents: number + lastKnownGoodCount: number | undefined + newDeletionsCount: number + allowShrink: boolean +} + +export function runChecks(input: CheckInput): void { + const quorum = Math.ceil(input.relaysQueried * 0.6) + if (input.relaysResponded < quorum) { + throw new Error( + `Relay-Quorum nicht erreicht: ${input.relaysResponded}/${input.relaysQueried} ` + + `(brauche mindestens ${quorum})`, + ) + } + if (input.eventCount < input.minEvents) { + throw new Error( + `Event-Count ${input.eventCount} unter min-events ${input.minEvents}`, + ) + } + if (input.lastKnownGoodCount !== undefined && !input.allowShrink) { + const drop = input.lastKnownGoodCount - input.eventCount + const dropPct = drop / input.lastKnownGoodCount + if (dropPct > 0.2 && drop > input.newDeletionsCount) { + throw new Error( + `Event-Count-Drop ${drop} (${(dropPct * 100).toFixed(0)}%) gegenueber ` + + `last-known-good ${input.lastKnownGoodCount}, ` + + `nur ${input.newDeletionsCount} korrespondierende kind:5. ` + + `Override mit --allow-shrink falls bewusst.`, + ) + } + } +} diff --git a/snapshot/tests/checks.test.ts b/snapshot/tests/checks.test.ts new file mode 100644 index 0000000..65547c0 --- /dev/null +++ b/snapshot/tests/checks.test.ts @@ -0,0 +1,59 @@ +import { assertEquals, assertThrows } from '@std/assert' +import { runChecks } from '../src/core/checks.ts' + +Deno.test('runChecks: weniger als 60% relays geantwortet -> hard-fail', () => { + assertThrows( + () => runChecks({ + relaysQueried: 5, relaysResponded: 2, + eventCount: 27, minEvents: 1, lastKnownGoodCount: undefined, + newDeletionsCount: 0, allowShrink: false, + }), + Error, 'Relay-Quorum', + ) +}) + +Deno.test('runChecks: event-count unter min-events -> hard-fail', () => { + assertThrows( + () => runChecks({ + relaysQueried: 5, relaysResponded: 5, + eventCount: 0, minEvents: 1, lastKnownGoodCount: undefined, + newDeletionsCount: 0, allowShrink: false, + }), + Error, 'min-events', + ) +}) + +Deno.test('runChecks: drop > 20% ohne kind:5 -> hard-fail', () => { + assertThrows( + () => runChecks({ + relaysQueried: 5, relaysResponded: 5, + eventCount: 20, minEvents: 1, lastKnownGoodCount: 27, + newDeletionsCount: 0, allowShrink: false, + }), + Error, 'Event-Count-Drop', + ) +}) + +Deno.test('runChecks: drop > 20% mit korrespondierenden kind:5 -> ok', () => { + runChecks({ + relaysQueried: 5, relaysResponded: 5, + eventCount: 20, minEvents: 1, lastKnownGoodCount: 27, + newDeletionsCount: 7, allowShrink: false, + }) +}) + +Deno.test('runChecks: --allow-shrink umgeht drop-check', () => { + runChecks({ + relaysQueried: 5, relaysResponded: 5, + eventCount: 1, minEvents: 1, lastKnownGoodCount: 27, + newDeletionsCount: 0, allowShrink: true, + }) +}) + +Deno.test('runChecks: erstlauf ohne cache + min-events=1 -> ok', () => { + runChecks({ + relaysQueried: 5, relaysResponded: 5, + eventCount: 1, minEvents: 1, lastKnownGoodCount: undefined, + newDeletionsCount: 0, allowShrink: false, + }) +})