192 lines
6.5 KiB
TypeScript
192 lines
6.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import {
|
|
codeOnly,
|
|
toReport,
|
|
compareImplementations,
|
|
extractFunction,
|
|
fingerprint,
|
|
formatDrift,
|
|
} from '../src/index.js'
|
|
|
|
const A = `
|
|
// header comment
|
|
function evaluateSendRules(c) {
|
|
if (!c.postalAddress) return { ok: false }
|
|
return { ok: true }
|
|
}
|
|
module.exports = { evaluateSendRules }
|
|
`
|
|
|
|
// Same behaviour, different comments and formatting — must NOT be drift.
|
|
const B = `
|
|
/* different doc */
|
|
function evaluateSendRules(c) {
|
|
// a comment the other copy does not have
|
|
if (!c.postalAddress) return { ok: false }
|
|
|
|
return { ok: true } // trailing note
|
|
}
|
|
`
|
|
|
|
// A real behavioural change — the postal-address check is gone.
|
|
const C = `
|
|
function evaluateSendRules(c) {
|
|
return { ok: true }
|
|
}
|
|
`
|
|
|
|
describe('extractFunction', () => {
|
|
it('extracts a function by brace matching', () => {
|
|
expect(extractFunction(A, 'evaluateSendRules')).toContain('postalAddress')
|
|
})
|
|
|
|
it('handles nested braces without stopping early', () => {
|
|
const src = 'function f() { if (x) { g({ a: 1 }) } return 2 }'
|
|
expect(extractFunction(src, 'f')).toBe(src)
|
|
})
|
|
|
|
it('finds async and exported declarations', () => {
|
|
expect(extractFunction('async function f() { return 1 }', 'f')).toBeTruthy()
|
|
expect(extractFunction('export function f() { return 1 }', 'f')).toBeTruthy()
|
|
})
|
|
|
|
it('returns null when the function is absent', () => {
|
|
expect(extractFunction(A, 'nope')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('codeOnly', () => {
|
|
it('ignores comments and whitespace so docs may differ', () => {
|
|
expect(codeOnly(extractFunction(A, 'evaluateSendRules')!))
|
|
.toBe(codeOnly(extractFunction(B, 'evaluateSendRules')!))
|
|
})
|
|
|
|
it('does NOT ignore a behavioural change', () => {
|
|
expect(codeOnly(extractFunction(A, 'evaluateSendRules')!))
|
|
.not.toBe(codeOnly(extractFunction(C, 'evaluateSendRules')!))
|
|
})
|
|
})
|
|
|
|
describe('compareImplementations', () => {
|
|
it('reports identical copies as in sync', () => {
|
|
const r = compareImplementations(
|
|
[{ label: 'a', source: A }, { label: 'b', source: B }],
|
|
'evaluateSendRules',
|
|
)
|
|
expect(r.inSync).toBe(true)
|
|
expect(r.groups).toHaveLength(1)
|
|
expect(r.vacuous).toBe(false)
|
|
})
|
|
|
|
it('catches the one-sided fix — the failure this exists for', () => {
|
|
// A CAN-SPAM guard removed in one repo only.
|
|
const r = compareImplementations(
|
|
[{ label: 'hosting', source: A }, { label: 'crm', source: C }],
|
|
'evaluateSendRules',
|
|
)
|
|
expect(r.inSync).toBe(false)
|
|
expect(r.groups).toHaveLength(2)
|
|
expect(formatDrift(r)).toContain('has drifted across')
|
|
})
|
|
|
|
it('groups three copies as 2-vs-1 and names which side is odd', () => {
|
|
const r = compareImplementations(
|
|
[{ label: 'a', source: A }, { label: 'b', source: B }, { label: 'c', source: C }],
|
|
'evaluateSendRules',
|
|
)
|
|
// Majority group first.
|
|
expect(r.groups[0]!.labels).toEqual(['a', 'b'])
|
|
expect(r.groups[1]!.labels).toEqual(['c'])
|
|
})
|
|
|
|
it('treats a MISSING function as not-found, never as passing', () => {
|
|
// Renamed or deleted is not "no drift" — a guard that cannot find its
|
|
// subject is not passing.
|
|
const r = compareImplementations(
|
|
[{ label: 'a', source: A }, { label: 'gone', source: 'function other() {}' }],
|
|
'evaluateSendRules',
|
|
)
|
|
expect(r.missing).toEqual(['gone'])
|
|
expect(r.inSync).toBe(false)
|
|
expect(formatDrift(r)).toContain('could not be found')
|
|
})
|
|
|
|
it('flags a single copy as NOT ASSESSED, not in sync', () => {
|
|
// One copy cannot disagree with itself; calling that a pass would let a
|
|
// deleted sibling read as clean.
|
|
const r = compareImplementations([{ label: 'a', source: A }], 'evaluateSendRules')
|
|
expect(r.vacuous).toBe(true)
|
|
const text = formatDrift(r)
|
|
expect(text).toContain('NOT ASSESSED')
|
|
expect(text).toContain('One copy cannot disagree with itself')
|
|
// The word it replaced needed a glossary.
|
|
expect(text).not.toContain('VACUOUS')
|
|
})
|
|
|
|
it('an empty input set is vacuous', () => {
|
|
expect(compareImplementations([], 'f').vacuous).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('fingerprint', () => {
|
|
it('is stable and differs on changed code', () => {
|
|
expect(fingerprint('abc')).toBe(fingerprint('abc'))
|
|
expect(fingerprint('abc')).not.toBe(fingerprint('abd'))
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
const A = 'function evaluateSendRules(c) {\n if (c.unsub) return false\n return true\n}'
|
|
const B = 'function evaluateSendRules(c) {\n return true\n}'
|
|
|
|
it('cites the file of every copy that disagrees', () => {
|
|
const r = compareImplementations([
|
|
{ label: 'crm', path: 'service-a/lib/send.js', source: A },
|
|
{ label: 'hosting', path: 'service-b/lib/send.js', source: B },
|
|
], 'evaluateSendRules')
|
|
const text = formatDrift(r, { evidence: 'full' })
|
|
expect(text).toContain('service-a/lib/send.js')
|
|
expect(text).toContain('service-b/lib/send.js')
|
|
expect(overall(toReport(r))).toBe('fail')
|
|
})
|
|
|
|
it('cites the copies behind an in-sync pass', () => {
|
|
// A pass that cannot name what it compared is an asserted result.
|
|
const r = compareImplementations([
|
|
{ label: 'crm', path: 'a/send.js', source: A },
|
|
{ label: 'hosting', path: 'b/send.js', source: A },
|
|
], 'evaluateSendRules')
|
|
expect(overall(toReport(r))).toBe('pass')
|
|
expect(formatDrift(r, { evidence: 'full' })).toContain('a/send.js')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
const r = compareImplementations([
|
|
{ label: 'crm', path: 'a/send.js', source: A },
|
|
{ label: 'hosting', path: 'b/send.js', source: B },
|
|
], 'evaluateSendRules')
|
|
expect(validateReport(toReport(r))).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('a missing subject is a finding, not an absence of one', () => {
|
|
it('stays assessed when one copy is present and one is missing', () => {
|
|
// `vacuous` counts LOCATED copies, so this pair is vacuous by that
|
|
// definition. Reporting it as NOT ASSESSED would hide a deleted or
|
|
// renamed sibling behind "nothing was compared" — the exact failure this
|
|
// library exists to catch.
|
|
const r = compareImplementations(
|
|
[
|
|
{ label: 'a', path: 'a/send.js', source: 'function evaluateSendRules(c) { return true }' },
|
|
{ label: 'gone', path: 'b/send.js', source: 'function other() {}' },
|
|
],
|
|
'evaluateSendRules',
|
|
)
|
|
expect(r.vacuous).toBe(true)
|
|
expect(overall(toReport(r))).toBe('fail')
|
|
expect(formatDrift(r)).toContain('could not be found')
|
|
expect(formatDrift(r)).not.toContain('NOT ASSESSED')
|
|
})
|
|
})
|