155 lines
6.0 KiB
TypeScript
155 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import {
|
|
assessStream,
|
|
formatStream,
|
|
keyVerdict,
|
|
transit,
|
|
type LinkPolicy,
|
|
type Message,
|
|
} from '../src/index.js'
|
|
|
|
const msg = (p: Partial<Message> = {}): Message => ({
|
|
id: 'm1',
|
|
sender: 'probe-a',
|
|
sequence: 1,
|
|
originAt: '2026-08-01T10:00:00Z',
|
|
receivedAt: '2026-08-01T10:20:00Z',
|
|
keyId: 'k1',
|
|
where: 'downlink.log',
|
|
...p,
|
|
})
|
|
|
|
const policy: LinkPolicy = { keys: [{ id: 'k1' }] }
|
|
|
|
describe('expiry has two ends and one check cannot express both', () => {
|
|
const expiring: LinkPolicy = { keys: [{ id: 'k1', notAfter: '2026-08-01T10:10:00Z' }] }
|
|
|
|
it('calls a key that expired IN FLIGHT its own thing', () => {
|
|
// Sent 10:00 (valid), arrived 10:20 (expired at 10:10).
|
|
expect(keyVerdict(msg(), expiring)).toBe('expired-in-flight')
|
|
})
|
|
|
|
it('ACCEPTS it, because rejecting would drop legitimate traffic', () => {
|
|
const { apply, report } = assessStream([msg()], expiring)
|
|
expect(apply).toHaveLength(1)
|
|
const f = report.findings.find(x => x.id.endsWith('/key-flight'))!
|
|
expect(f.determination).toBe('not-applicable')
|
|
expect(f.detail).toContain('rejecting it would drop legitimate traffic')
|
|
})
|
|
|
|
it('still records it, because the key is no longer trusted for anything new', () => {
|
|
expect(formatStream([msg()], expiring)).toContain('expired while in flight')
|
|
})
|
|
|
|
it('rejects a key that was already invalid at origin', () => {
|
|
const late = msg({ originAt: '2026-08-01T10:15:00Z', receivedAt: '2026-08-01T10:30:00Z' })
|
|
expect(keyVerdict(late, expiring)).toBe('invalid-at-origin')
|
|
expect(assessStream([late], expiring).reject).toHaveLength(1)
|
|
})
|
|
|
|
it('revocation ignores origin time entirely', () => {
|
|
// The key was usually compromised before anyone noticed.
|
|
const revoked: LinkPolicy = { keys: [{ id: 'k1', revokedAt: '2026-08-01T23:00:00Z' }] }
|
|
expect(keyVerdict(msg(), revoked)).toBe('revoked')
|
|
const r = assessStream([msg()], revoked)
|
|
expect(r.reject).toHaveLength(1)
|
|
expect(r.report.findings[0]!.detail).toContain('Revocation is not a schedule')
|
|
})
|
|
|
|
it('rejects an unrecognised or absent key as unassessed', () => {
|
|
expect(keyVerdict(msg({ keyId: undefined }), policy)).toBe('unknown')
|
|
expect(keyVerdict(msg({ keyId: 'nope' }), policy)).toBe('unknown')
|
|
const r = assessStream([msg({ keyId: undefined })], policy)
|
|
expect(r.report.findings[0]!.determination).toBe('not-assessed')
|
|
expect(r.reject).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('ordering is by sender sequence, never by arrival', () => {
|
|
it('rejects an older message that arrived after a newer one', () => {
|
|
// Applying in arrival order would overwrite newer state with older,
|
|
// and would look like it worked.
|
|
const out = [
|
|
msg({ id: 'm2', sequence: 2, originAt: '2026-08-01T10:05:00Z', receivedAt: '2026-08-01T10:10:00Z' }),
|
|
msg({ id: 'm1', sequence: 1 }),
|
|
]
|
|
const r = assessStream(out, policy)
|
|
// Sequence 1 sorts first, so 2 is applied and there is no regression.
|
|
expect(r.apply.map(m => m.sequence)).toEqual([1, 2])
|
|
})
|
|
|
|
it('returns apply order by sequence even when the input is shuffled', () => {
|
|
const shuffled = [
|
|
msg({ id: 'c', sequence: 3 }),
|
|
msg({ id: 'a', sequence: 1 }),
|
|
msg({ id: 'b', sequence: 2 }),
|
|
]
|
|
expect(assessStream(shuffled, policy).apply.map(m => m.sequence)).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('tracks sequences per sender, not globally', () => {
|
|
const two = [
|
|
msg({ id: 'a', sender: 'probe-a', sequence: 9 }),
|
|
msg({ id: 'b', sender: 'probe-b', sequence: 1 }),
|
|
]
|
|
expect(assessStream(two, policy).reject).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('a duplicate is the delivery guarantee working', () => {
|
|
const dup = [msg({ id: 'a', sequence: 1 }), msg({ id: 'b', sequence: 1 })]
|
|
|
|
it('fails a duplicate when the receiver is not idempotent', () => {
|
|
const r = assessStream(dup, policy)
|
|
const f = r.report.findings.find(x => x.id.endsWith('/duplicate'))!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.detail).toContain('the assumption of exactly-once is')
|
|
})
|
|
|
|
it('accepts it when the receiver declares idempotency', () => {
|
|
const r = assessStream(dup, { ...policy, receiverIsIdempotent: true })
|
|
const f = r.report.findings.find(x => x.id.endsWith('/duplicate'))!
|
|
expect(f.determination).toBe('not-applicable')
|
|
expect(r.reject).toHaveLength(0)
|
|
})
|
|
|
|
it('accepts it when the message itself is idempotent', () => {
|
|
const r = assessStream([msg({ id: 'a', idempotent: true }), msg({ id: 'b', idempotent: true })], policy)
|
|
expect(r.reject).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('transit', () => {
|
|
it('rejects a message too old to act on, and says to record it anyway', () => {
|
|
const r = assessStream([msg()], { ...policy, maxTransitSeconds: 300 })
|
|
const f = r.report.findings.find(x => x.id.endsWith('/stale'))!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.detail).toContain('Record it; do not act on it')
|
|
})
|
|
|
|
it('flags impossible clocks as unassessed, not as a fast message', () => {
|
|
const back = msg({ originAt: '2026-08-01T10:20:00Z', receivedAt: '2026-08-01T10:00:00Z' })
|
|
expect(transit(back).impossible).toBe(true)
|
|
const r = assessStream([back], policy)
|
|
expect(r.report.findings[0]!.determination).toBe('not-assessed')
|
|
expect(r.report.findings[0]!.detail).toContain('neither transit time nor any expiry decision')
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('an empty stream is not-assessed, and says what silence means', () => {
|
|
const r = assessStream([])
|
|
expect(overall(r.report)).toBe('not-assessed')
|
|
expect(formatStream([])).toContain('silence is not evidence that nothing was sent')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(assessStream([msg(), msg({ id: 'b', sequence: 2 })], policy).report)).toEqual([])
|
|
})
|
|
|
|
it('passes a clean stream', () => {
|
|
expect(overall(assessStream([msg()], policy).report)).toBe('pass')
|
|
})
|
|
})
|