Files
ycode/scripts/rea/compare-visual-pixels.mjs
T

591 lines
32 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { createRequire } from 'node:module'
import { spawn } from 'node:child_process'
import { pathToFileURL } from 'node:url'
const require = createRequire(import.meta.url)
const electronExecutable = require('electron')
const runtime = path.resolve('.tmp/rea-css-runtime/dist')
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
const evidenceIdPattern = /^ev_[a-f0-9]{64}$/
const electronHelperSource = String.raw`
const fs = require('node:fs')
const path = require('node:path')
const { app, nativeImage } = require('electron')
const jobPath = process.env.YCODE_PIXEL_JOB
const resultPath = process.env.YCODE_PIXEL_RESULT
const finish = (value, code = 0) => {
try { fs.writeFileSync(resultPath, JSON.stringify(value)) } catch {}
app.exit(code)
}
const fail = error => finish({ ok: false, error: String(error && (error.stack || error.message) || error) }, 1)
try {
const job = JSON.parse(fs.readFileSync(jobPath, 'utf8'))
const isolated = path.join(path.dirname(jobPath), 'electron-data')
for (const name of ['userData', 'sessionData', 'logs']) {
try { app.setPath(name, path.join(isolated, name)) } catch {}
}
app.disableHardwareAcceleration()
app.whenReady().then(() => {
try {
if (job.mode === 'fixtures') {
const createPng = (file, width, height, changed) => {
const bitmap = Buffer.alloc(width * height * 4)
for (let offset = 0; offset < bitmap.length; offset += 4) bitmap.set([20, 40, 60, 255], offset)
if (changed) bitmap[0] += 1
const image = nativeImage.createFromBitmap(bitmap, { width, height, scaleFactor: 1 })
if (image.isEmpty()) throw new Error('nativeImage could not encode self-test fixture')
fs.writeFileSync(file, image.toPNG())
}
createPng(job.files.reference, 2, 2, false)
createPng(job.files.changed, 2, 2, true)
createPng(job.files.wide, 3, 2, false)
finish({ ok: true })
return
}
if (job.mode !== 'compare') throw new Error('Unknown nativeImage helper mode')
const output = []
for (const pair of job.pairs) {
const reference = nativeImage.createFromBuffer(fs.readFileSync(pair.reference))
const candidate = nativeImage.createFromBuffer(fs.readFileSync(pair.candidate))
if (reference.isEmpty() || candidate.isEmpty()) throw new Error('A screenshot could not be decoded by nativeImage')
const beforeSize = reference.getSize(1)
const afterSize = candidate.getSize(1)
const base = {
name: pair.name,
before: beforeSize,
after: afterSize,
channel_tolerance: job.channelTolerance,
}
if (beforeSize.width !== afterSize.width || beforeSize.height !== afterSize.height) {
output.push({ ...base, status: 'dimension_mismatch', compared_pixels: 0, changed_pixels: null, changed_ratio: null, mean_absolute_channel_delta: null, maximum_channel_delta: null, diff_width: null, diff_height: null })
continue
}
const region = pair.region || { x: 0, y: 0, width: beforeSize.width, height: beforeSize.height }
if (!Number.isInteger(region.x) || !Number.isInteger(region.y) || !Number.isInteger(region.width) || !Number.isInteger(region.height) || region.x < 0 || region.y < 0 || region.width < 1 || region.height < 1 || region.x + region.width > beforeSize.width || region.y + region.height > beforeSize.height) {
throw new Error('Comparison region must be a positive integer rectangle inside both screenshots')
}
const beforeBitmap = reference.toBitmap({ scaleFactor: 1 })
const afterBitmap = candidate.toBitmap({ scaleFactor: 1 })
const expectedBeforeBytes = beforeSize.width * beforeSize.height * 4
const expectedAfterBytes = afterSize.width * afterSize.height * 4
if (beforeBitmap.length !== expectedBeforeBytes || afterBitmap.length !== expectedAfterBytes) throw new Error('nativeImage returned an unexpected bitmap byte count')
const diffBitmap = Buffer.alloc(region.width * region.height * 4)
const comparedPixels = region.width * region.height
let changedPixels = 0
let maximumChannelDelta = 0
let absoluteChannelDelta = 0
for (let y = 0; y < region.height; y += 1) {
for (let x = 0; x < region.width; x += 1) {
const beforeOffset = ((region.y + y) * beforeSize.width + region.x + x) * 4
const afterOffset = ((region.y + y) * afterSize.width + region.x + x) * 4
const diffOffset = (y * region.width + x) * 4
let pixelMaximum = 0
for (let channel = 0; channel < 4; channel += 1) {
const delta = Math.abs(beforeBitmap[beforeOffset + channel] - afterBitmap[afterOffset + channel])
absoluteChannelDelta += delta
pixelMaximum = Math.max(pixelMaximum, delta)
maximumChannelDelta = Math.max(maximumChannelDelta, delta)
}
if (pixelMaximum > job.channelTolerance) {
changedPixels += 1
diffBitmap.fill(255, diffOffset, diffOffset + 4)
}
}
}
const diffImage = nativeImage.createFromBitmap(diffBitmap, { width: region.width, height: region.height, scaleFactor: 1 })
if (diffImage.isEmpty()) throw new Error('nativeImage could not encode the diff bitmap')
fs.writeFileSync(pair.diffPath, diffImage.toPNG())
output.push({
...base,
region,
status: changedPixels === 0 ? 'identical' : 'different',
compared_pixels: comparedPixels,
changed_pixels: changedPixels,
changed_ratio: changedPixels / comparedPixels,
mean_absolute_channel_delta: absoluteChannelDelta / (comparedPixels * 4),
maximum_channel_delta: maximumChannelDelta,
diff_width: region.width,
diff_height: region.height,
})
}
finish({ ok: true, pairs: output })
} catch (error) {
fail(error)
}
}).catch(fail)
} catch (error) {
fail(error)
}
`
function parseManifest(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Manifest must be a JSON object')
const source = value.source ?? { kind: 'local-fixture' }
if (!source || typeof source !== 'object' || !['rea-workbuddy', 'local-fixture'].includes(source.kind)) throw new Error('source.kind must be rea-workbuddy or local-fixture')
if (!Array.isArray(value.requiredStates) || value.requiredStates.length === 0 || value.requiredStates.some(state => typeof state !== 'string' || !state.trim()) || new Set(value.requiredStates).size !== value.requiredStates.length) throw new Error('requiredStates must be a non-empty array of unique non-empty names')
if (!Array.isArray(value.pairs)) throw new Error('pairs must be an array')
const names = new Set()
const pairs = value.pairs.map((pair, index) => {
if (!pair || typeof pair !== 'object' || Array.isArray(pair)) throw new Error(`pairs[${index}] must be an object`)
if (typeof pair.name !== 'string' || !pair.name.trim() || names.has(pair.name)) throw new Error(`pairs[${index}].name must be unique and non-empty`)
names.add(pair.name)
if (typeof pair.reference !== 'string' || !pair.reference || typeof pair.candidate !== 'string' || !pair.candidate) throw new Error(`pairs[${index}] requires reference and candidate PNG paths`)
if (pair.region !== undefined) {
const { x, y, width, height } = pair.region ?? {}
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width < 1 || height < 1) throw new Error(`pairs[${index}].region must be a positive integer rectangle`)
}
if (source.kind === 'rea-workbuddy') {
const evidence = pair.referenceEvidence
if (!evidence || typeof evidence !== 'object' || !evidenceIdPattern.test(evidence.id) || typeof evidence.archive !== 'string' || !evidence.archive) throw new Error(`pairs[${index}] requires referenceEvidence with a valid Evidence id and archive`)
}
return { ...pair }
})
const channelTolerance = value.channelTolerance ?? 0
if (!Number.isInteger(channelTolerance) || channelTolerance < 0 || channelTolerance > 255) throw new Error('channelTolerance must be an integer from 0 to 255')
return { source, requiredStates: [...value.requiredStates], pairs, channelTolerance }
}
async function authenticateEvidence(evidencePath) {
const modulePath = pathToFileURL(path.join(runtime, 'domain/evidence.js'))
let parseEvidence
try {
({ parseEvidence } = await import(modulePath))
} catch (error) {
throw new Error(`Prepared REA runtime is required to authenticate Evidence: ${error.message}`)
}
return parseEvidence(evidencePath)
}
function screenshotArtifacts(evidence) {
const visuals = evidence.normalized_result?.visuals
const artifacts = []
if (visuals?.artifact) artifacts.push({ location: 'visuals.artifact', artifact: visuals.artifact })
for (let index = 0; index < (visuals?.probe?.frames?.length ?? 0); index += 1) {
const artifact = visuals.probe.frames[index]?.artifact
if (artifact) artifacts.push({ location: `visuals.probe.frames[${index}].artifact`, artifact })
}
return artifacts
}
function checkedArtifactHash(artifact) {
if (!artifact || artifact.media_type !== 'image/png' || typeof artifact.data_base64 !== 'string' || typeof artifact.sha256 !== 'string') return undefined
const bytes = Buffer.from(artifact.data_base64, 'base64')
if (!bytes.length || bytes.toString('base64') !== artifact.data_base64 || (artifact.bytes !== undefined && artifact.bytes !== bytes.length)) return undefined
const actualHash = hash(bytes)
return actualHash === artifact.sha256 ? actualHash : undefined
}
async function verifyReferenceEvidence(pair, manifestDirectory, referenceHash) {
const archive = path.resolve(manifestDirectory, pair.referenceEvidence.archive)
let raw
try {
raw = JSON.parse(await fs.readFile(archive, 'utf8'))
} catch (error) {
throw new Error(`Cannot read reference Evidence archive for ${pair.name}: ${error.message}`)
}
if (raw.evidence_id !== pair.referenceEvidence.id) throw new Error(`Evidence id does not match its archive for ${pair.name}`)
let evidence
try {
evidence = await authenticateEvidence(raw)
} catch (error) {
throw new Error(`Reference Evidence archive failed authentication for ${pair.name}: ${error.message}`)
}
if (evidence.operation !== 'inspect_electron_page') throw new Error(`Reference Evidence is not an Electron page inspection for ${pair.name}`)
if (evidence.provider?.id !== 'rea-cdp-electron') throw new Error(`Reference Evidence provider is not rea-cdp-electron for ${pair.name}`)
const targetPath = evidence.normalized_result?.target?.file_path
if (typeof targetPath !== 'string' || !/WorkBuddy[\\/]resources[\\/]app\.asar[\\/]renderer[\\/]index\.html$/i.test(targetPath)) {
throw new Error(`Reference Evidence target is not the WorkBuddy renderer for ${pair.name}`)
}
const visualProbe = evidence.parameters?.visual_probe
if (visualProbe !== undefined && (!visualProbe || typeof visualProbe !== 'object' || Array.isArray(visualProbe) || !Array.isArray(visualProbe.actions))) {
throw new Error(`Reference Evidence visual_probe.actions is malformed for ${pair.name}`)
}
const visualProbeActions = visualProbe?.actions ?? []
const hasVisualProbeActions = visualProbeActions.length > 0
const expectedAuthority = hasVisualProbeActions ? 'controlled-replay' : 'external-service'
if (evidence.authority !== expectedAuthority) throw new Error(`Reference Evidence authority does not match visual_probe.actions for ${pair.name}`)
const userAgent = evidence.normalized_result?.browser?.user_agent
const version = typeof userAgent === 'string' ? userAgent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1] ?? 'unobserved' : 'unobserved'
const matching = screenshotArtifacts(evidence).filter(({ artifact }) => checkedArtifactHash(artifact) === referenceHash)
if (!matching.length) throw new Error(`Reference PNG SHA is absent from authenticated REA baseline/probe artifacts for ${pair.name}`)
return {
id: evidence.evidence_id,
archive,
artifact_locations: matching.map(item => item.location),
sha256: referenceHash,
target_file_path: targetPath,
authority: evidence.authority,
version,
version_observed: version !== 'unobserved',
}
}
function safeDiffName(name) {
const stem = name.normalize('NFKD').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'state'
return `${stem}-${hash(Buffer.from(name, 'utf8')).slice(0, 12)}-diff.png`
}
async function runElectronHelper(job) {
const helperDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixel-helper-'))
const helperPath = path.join(helperDirectory, 'helper.cjs')
const packagePath = path.join(helperDirectory, 'package.json')
const jobPath = path.join(helperDirectory, 'job.json')
const resultPath = path.join(helperDirectory, 'result.json')
await fs.writeFile(helperPath, electronHelperSource, 'utf8')
await fs.writeFile(packagePath, JSON.stringify({ name: 'ycode-visual-pixel-helper', version: '1.0.0', main: 'helper.cjs' }), 'utf8')
await fs.writeFile(jobPath, JSON.stringify(job), 'utf8')
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'ELECTRON_RUN_AS_NODE'))
env.YCODE_PIXEL_JOB = jobPath
env.YCODE_PIXEL_RESULT = resultPath
try {
const completion = await new Promise((resolve, reject) => {
const child = spawn(electronExecutable, ['--no-sandbox', '--disable-gpu', '--disable-extensions', helperDirectory], {
cwd: helperDirectory,
env,
windowsHide: true,
stdio: ['ignore', 'ignore', 'pipe'],
})
let stderr = ''
let settled = false
const timer = setTimeout(() => {
child.kill()
if (!settled) {
settled = true
reject(new Error('Hidden Electron nativeImage helper timed out'))
}
}, 45000)
timer.unref()
child.stderr.on('data', chunk => { stderr += chunk.toString() })
child.once('error', error => {
clearTimeout(timer)
if (!settled) { settled = true; reject(error) }
})
child.once('close', (code, signal) => {
clearTimeout(timer)
if (settled) return
settled = true
resolve({ code, signal, stderr })
})
})
let result
try { result = JSON.parse(await fs.readFile(resultPath, 'utf8')) } catch {
throw new Error(`Hidden Electron nativeImage helper exited without a result (code ${completion.code}${completion.stderr ? `): ${completion.stderr.trim()}` : ')'}`)
}
if (!result.ok || completion.code !== 0) throw new Error(result.error || `Hidden Electron nativeImage helper failed (code ${completion.code})`)
return result
} finally {
await fs.rm(helperDirectory, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 })
}
}
async function compareManifest(manifest, manifestDirectory, outputDirectory) {
const parsed = parseManifest(manifest)
const prepared = []
for (const pair of parsed.pairs) {
const referencePath = path.resolve(manifestDirectory, pair.reference)
const candidatePath = path.resolve(manifestDirectory, pair.candidate)
let referenceBytes, candidateBytes
try {
[referenceBytes, candidateBytes] = await Promise.all([fs.readFile(referencePath), fs.readFile(candidatePath)])
} catch (error) {
throw new Error(`Cannot read screenshot pair ${pair.name}: ${error.message}`)
}
const referenceSha = hash(referenceBytes)
const candidateSha = hash(candidateBytes)
const referenceEvidence = parsed.source.kind === 'rea-workbuddy'
? await verifyReferenceEvidence(pair, manifestDirectory, referenceSha)
: undefined
prepared.push({
name: pair.name,
reference: referencePath,
candidate: candidatePath,
reference_sha256: referenceSha,
candidate_sha256: candidateSha,
region: pair.region,
reference_evidence: referenceEvidence,
})
}
await fs.mkdir(outputDirectory, { recursive: true })
const helperPairs = prepared.map(pair => ({
name: pair.name,
reference: pair.reference,
candidate: pair.candidate,
region: pair.region,
diffPath: path.join(outputDirectory, safeDiffName(pair.name)),
}))
const nativeResult = await runElectronHelper({ mode: 'compare', channelTolerance: parsed.channelTolerance, pairs: helperPairs })
const nativeByName = new Map(nativeResult.pairs.map(pair => [pair.name, pair]))
const results = prepared.map(pair => {
const compared = nativeByName.get(pair.name)
if (!compared) throw new Error(`nativeImage helper omitted comparison ${pair.name}`)
const dimensions = { width: compared.before.width, height: compared.before.height }
const afterDimensions = { width: compared.after.width, height: compared.after.height }
const diffPath = compared.diff_width === null ? undefined : helperPairs.find(item => item.name === pair.name).diffPath
return {
name: pair.name,
status: compared.status,
reference: { path: pair.reference, sha256: pair.reference_sha256, dimensions },
candidate: { path: pair.candidate, sha256: pair.candidate_sha256, dimensions: afterDimensions },
...(compared.region ? { region: compared.region } : {}),
channel_tolerance: compared.channel_tolerance,
compared_pixels: compared.compared_pixels,
changed_pixels: compared.changed_pixels,
changed_ratio: compared.changed_ratio,
mean_absolute_channel_delta: compared.mean_absolute_channel_delta,
maximum_channel_delta: compared.maximum_channel_delta,
diff_png: diffPath ? { path: diffPath, sha256: undefined, dimensions: { width: compared.diff_width, height: compared.diff_height } } : null,
...(pair.reference_evidence ? { reference_evidence: pair.reference_evidence } : {}),
}
})
for (const pair of results) {
if (pair.diff_png) pair.diff_png.sha256 = hash(await fs.readFile(pair.diff_png.path))
}
const missingRequiredStates = parsed.requiredStates.filter(name => !parsed.pairs.some(pair => pair.name === name))
const status = missingRequiredStates.length
? 'incomplete'
: results.some(pair => pair.status !== 'identical') ? 'different' : 'identical'
const observedVersions = results.flatMap(pair => pair.reference_evidence ? [{
state: pair.name,
evidence_id: pair.reference_evidence.id,
version: pair.reference_evidence.version,
observed: pair.reference_evidence.version_observed,
}] : [])
let originalParity
if (parsed.source.kind !== 'rea-workbuddy') {
originalParity = { status: 'not_assessed', exactOriginalParity: false, versions: [], reasons: ['Source is local fixture data, not authenticated WorkBuddy Evidence.'] }
} else {
const reasons = []
if (missingRequiredStates.length) reasons.push('One or more required states are missing.')
if (observedVersions.length !== results.length || observedVersions.some(item => !item.observed)) reasons.push('A WorkBuddy version was not observed in browser.user_agent.')
if (reasons.length) originalParity = { status: 'incomplete', exactOriginalParity: false, versions: observedVersions, reasons }
else if (results.some(pair => pair.status !== 'identical')) originalParity = { status: 'different', exactOriginalParity: false, versions: observedVersions, reasons: ['At least one declared state differs beyond channel tolerance.'] }
else if (parsed.channelTolerance !== 0) originalParity = { status: 'not_exact', exactOriginalParity: false, versions: observedVersions, reasons: ['Non-zero channel tolerance does not establish exact pixel parity.'] }
else originalParity = { status: 'exact_for_declared_states', exactOriginalParity: true, versions: observedVersions, reasons: [] }
}
const reportStatus = parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'incomplete'
? 'incomplete'
: parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'not_exact'
? 'approximate'
: status
return {
status: reportStatus,
source: parsed.source,
originalParity,
scope: {
assessment: 'declared_state_set_only',
required_states: parsed.requiredStates,
full_1to1: 'not_assessed',
},
missing_required_states: missingRequiredStates,
channel_tolerance: parsed.channelTolerance,
metric_definition: 'Changed pixels have at least one of four decoded bitmap components above tolerance. Mean absolute channel delta is averaged across all four components in the compared region.',
limitations: [
'Only the manifest state set is assessed; this output is not a complete 1:1 product acceptance.',
'Images are decoded to nativeImage 8-bit bitmaps; their platform-specific byte order is compared consistently within this run. No registration, OCR, or semantic layout analysis is performed.',
'Diff PNG marks changed pixels white and unchanged pixels transparent.',
],
pairs: results,
}
}
async function runSelfTest() {
const fixture = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixels-self-test-'))
try {
assert.throws(() => parseManifest({ source: { kind: 'local-fixture' }, requiredStates: [], pairs: [] }), /non-empty array/)
const files = {
reference: path.join(fixture, 'reference.png'),
changed: path.join(fixture, 'changed.png'),
wide: path.join(fixture, 'wide.png'),
}
await runElectronHelper({ mode: 'fixtures', files })
const manifest = {
source: { kind: 'local-fixture' },
requiredStates: ['same', 'one-pixel', 'dimension-mismatch'],
pairs: [
{ name: 'same', reference: files.reference, candidate: files.reference },
{ name: 'one-pixel', reference: files.reference, candidate: files.changed, region: { x: 0, y: 0, width: 1, height: 1 } },
{ name: 'dimension-mismatch', reference: files.reference, candidate: files.wide },
],
}
const report = await compareManifest(manifest, fixture, path.join(fixture, 'diffs'))
assert.equal(report.status, 'different')
assert.equal(report.scope.full_1to1, 'not_assessed')
assert.equal(report.originalParity.status, 'not_assessed')
assert.equal(report.originalParity.exactOriginalParity, false)
const same = report.pairs.find(pair => pair.name === 'same')
assert.equal(same.status, 'identical')
assert.equal(same.compared_pixels, 4)
assert.equal(same.changed_pixels, 0)
assert.equal(same.changed_ratio, 0)
assert.equal(same.maximum_channel_delta, 0)
assert.equal(same.mean_absolute_channel_delta, 0)
const changed = report.pairs.find(pair => pair.name === 'one-pixel')
assert.equal(changed.status, 'different')
assert.equal(changed.changed_pixels, 1)
assert.equal(changed.changed_ratio, 1)
assert.equal(changed.maximum_channel_delta, 1)
assert.equal(changed.mean_absolute_channel_delta, 0.25)
assert.deepEqual(changed.diff_png.dimensions, { width: 1, height: 1 })
assert.match(changed.diff_png.sha256, /^[a-f0-9]{64}$/)
assert.ok((await fs.stat(changed.diff_png.path)).size > 0)
const mismatch = report.pairs.find(pair => pair.name === 'dimension-mismatch')
assert.equal(mismatch.status, 'dimension_mismatch')
assert.deepEqual(mismatch.reference.dimensions, { width: 2, height: 2 })
assert.deepEqual(mismatch.candidate.dimensions, { width: 3, height: 2 })
assert.equal(mismatch.changed_pixels, null)
assert.equal(mismatch.diff_png, null)
const incomplete = await compareManifest({
source: { kind: 'local-fixture' },
requiredStates: ['present', 'missing'],
pairs: [{ name: 'present', reference: files.reference, candidate: files.reference }],
}, fixture, path.join(fixture, 'incomplete-diffs'))
assert.equal(incomplete.status, 'incomplete')
assert.deepEqual(incomplete.missing_required_states, ['missing'])
const { createEvidence } = await import(pathToFileURL(path.join(runtime, 'domain/evidence.js')))
const referenceBytes = await fs.readFile(files.reference)
const changedBytes = await fs.readFile(files.changed)
const asArtifact = bytes => ({ sha256: hash(bytes), bytes: bytes.length, media_type: 'image/png', data_base64: bytes.toString('base64') })
const baselineArtifact = asArtifact(referenceBytes)
const probeArtifact = asArtifact(changedBytes)
const workbuddyTargetPath = 'C:\\Program Files\\WorkBuddy\\resources\\app.asar\\renderer\\index.html'
const syntheticEvidence = ({ visuals, targetFilePath = workbuddyTargetPath, userAgent = 'WorkBuddy/5.7.0 Electron/37.10.3', actions = [], authority = actions.length ? 'controlled-replay' : 'external-service' }) => createEvidence(undefined, {
id: 'rea-cdp-electron',
name: 'Synthetic WorkBuddy REA fixture provider',
version: 'self-test',
}, {
predicateType: 'rea.electron-page-inspection',
operation: 'inspect_electron_page',
parameters: actions.length ? { visual_probe: { actions } } : {},
result: { browser: { user_agent: userAgent }, target: { file_path: targetFilePath }, visuals },
confidence: 'observed',
authority,
limitations: ['Synthetic self-test fixture only; not captured from WorkBuddy.'],
})
const authenticatedEvidence = syntheticEvidence({
visuals: { artifact: baselineArtifact, probe: { frames: [{ step_id: 'probe-step', at_ms: 0, artifact: probeArtifact }] } },
actions: [{ kind: 'click', step_id: 'synthetic-probe-step' }],
})
const archivePath = path.join(fixture, 'synthetic-evidence.json')
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
const evidenced = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['baseline', 'probe'],
pairs: [
{ name: 'baseline', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
{ name: 'probe', reference: files.changed, candidate: files.changed, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
],
}, fixture, path.join(fixture, 'evidenced-diffs'))
assert.equal(evidenced.status, 'identical')
assert.equal(evidenced.originalParity.status, 'exact_for_declared_states')
assert.equal(evidenced.originalParity.exactOriginalParity, true)
assert.equal(evidenced.originalParity.versions[0].version, '5.7.0')
assert.equal(evidenced.pairs[0].reference_evidence.authority, 'controlled-replay')
assert.equal(evidenced.scope.assessment, 'declared_state_set_only')
assert.equal(evidenced.scope.full_1to1, 'not_assessed')
assert.deepEqual(evidenced.pairs[0].reference_evidence.artifact_locations, ['visuals.artifact'])
assert.deepEqual(evidenced.pairs[1].reference_evidence.artifact_locations, ['visuals.probe.frames[0].artifact'])
const localFixtureEvidence = syntheticEvidence({
visuals: { artifact: baselineArtifact },
targetFilePath: path.join(fixture, 'renderer', 'index.html'),
})
await fs.writeFile(archivePath, JSON.stringify(localFixtureEvidence), 'utf8')
const localMasqueradeManifest = {
source: { kind: 'rea-workbuddy' },
requiredStates: ['local-fixture'],
pairs: [{ name: 'local-fixture', reference: files.reference, candidate: files.reference, referenceEvidence: { id: localFixtureEvidence.evidence_id, archive: archivePath } }],
}
await assert.rejects(compareManifest(localMasqueradeManifest, fixture, path.join(fixture, 'local-masquerade-diffs')), /target is not the WorkBuddy renderer/)
const falseBindingEvidence = syntheticEvidence({ visuals: { artifact: probeArtifact } })
const workbuddyManifest = {
source: { kind: 'rea-workbuddy' },
requiredStates: ['forged'],
pairs: [{ name: 'forged', reference: files.reference, candidate: files.reference, referenceEvidence: { id: falseBindingEvidence.evidence_id, archive: archivePath } }],
}
const forgedEvidenceId = `ev_${'0'.repeat(64)}`
await fs.writeFile(archivePath, JSON.stringify({ ...falseBindingEvidence, evidence_id: forgedEvidenceId }), 'utf8')
const forgedIdManifest = {
...workbuddyManifest,
pairs: [{ ...workbuddyManifest.pairs[0], referenceEvidence: { id: forgedEvidenceId, archive: archivePath } }],
}
await assert.rejects(compareManifest(forgedIdManifest, fixture, path.join(fixture, 'forged-id-diffs')), /authentication|identifier|digest|Evidence/i)
await fs.writeFile(archivePath, JSON.stringify(falseBindingEvidence), 'utf8')
await assert.rejects(compareManifest(workbuddyManifest, fixture, path.join(fixture, 'unclaimed-reference-diffs')), /SHA is absent/)
const unversionedEvidence = syntheticEvidence({ visuals: { artifact: baselineArtifact }, userAgent: 'Chromium/123.0' })
await fs.writeFile(archivePath, JSON.stringify(unversionedEvidence), 'utf8')
const unversioned = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['unversioned'],
pairs: [{ name: 'unversioned', reference: files.reference, candidate: files.reference, referenceEvidence: { id: unversionedEvidence.evidence_id, archive: archivePath } }],
}, fixture, path.join(fixture, 'unversioned-diffs'))
assert.equal(unversioned.status, 'incomplete')
assert.equal(unversioned.originalParity.status, 'incomplete')
assert.equal(unversioned.originalParity.exactOriginalParity, false)
assert.equal(unversioned.originalParity.versions[0].version, 'unobserved')
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
const approximate = await compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['tolerant'],
channelTolerance: 1,
pairs: [{ name: 'tolerant', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: path.join(fixture, 'synthetic-evidence.json') } }],
}, fixture, path.join(fixture, 'tolerant-diffs'))
assert.equal(approximate.status, 'approximate')
assert.equal(approximate.originalParity.status, 'not_exact')
assert.equal(approximate.originalParity.exactOriginalParity, false)
const mismatchedAuthority = syntheticEvidence({
visuals: { artifact: baselineArtifact },
actions: [{ kind: 'click', step_id: 'synthetic-action' }],
authority: 'external-service',
})
await fs.writeFile(archivePath, JSON.stringify(mismatchedAuthority), 'utf8')
await assert.rejects(compareManifest({
source: { kind: 'rea-workbuddy' },
requiredStates: ['authority'],
pairs: [{ name: 'authority', reference: files.reference, candidate: files.reference, referenceEvidence: { id: mismatchedAuthority.evidence_id, archive: archivePath } }],
}, fixture, path.join(fixture, 'authority-diffs')), /authority does not match visual_probe.actions/)
console.log('Visual pixel self-test passed: pixel comparison, incomplete required states, authenticated synthetic baseline/probe binding, local-target masquerade rejection, observed-version and authority checks, nonzero-tolerance exactness rejection, and forged Evidence rejection')
} finally {
await fs.rm(fixture, { recursive: true, force: true })
}
}
async function main() {
const args = process.argv.slice(2)
if (args.length === 1 && args[0] === '--self-test') {
await runSelfTest()
return
}
const manifestPath = args[0]
if (!manifestPath || manifestPath.startsWith('--')) throw new Error('Usage: node scripts/rea/compare-visual-pixels.mjs <manifest.json> [--output <directory>] | --self-test')
let outputDirectory
for (let index = 1; index < args.length; index += 1) {
if (args[index] !== '--output' || !args[index + 1]) throw new Error('Expected --output followed by a directory')
outputDirectory = path.resolve(args[++index])
}
const absoluteManifest = path.resolve(manifestPath)
const raw = JSON.parse(await fs.readFile(absoluteManifest, 'utf8'))
const parsed = parseManifest(raw)
const report = await compareManifest(parsed, path.dirname(absoluteManifest), outputDirectory ?? path.join(path.dirname(absoluteManifest), 'visual-pixel-diffs'))
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
if (report.status !== 'identical') process.exitCode = 1
}
main().catch(error => {
process.stderr.write(`${error.stack || error.message || error}\n`)
process.exitCode = 1
})