First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user