Files
gpt-load/web/scripts/check-visual-runner-lock.mjs
T

161 lines
5.1 KiB
JavaScript

import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const approvedRenderFingerprint = {
viewports: [
{ width: 375, height: 812 },
{ width: 768, height: 900 },
{ width: 1024, height: 900 },
{ width: 1440, height: 900 },
],
device_scale_factor: 1,
locales: ['en-US', 'zh-CN'],
timezone: 'UTC',
color_schemes: ['light', 'dark'],
reduced_motion: 'reduce',
animations_disabled: true,
}
function fail(message) {
throw new Error(`visual runner lock: ${message}`)
}
function fullDigest(value) {
return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value)
}
export function validateVisualRunnerLock(lock) {
if (!lock || typeof lock !== 'object') fail('manifest must be an object')
if (lock.schema_version !== 2) fail('unsupported schema version')
if (
typeof lock.container?.image !== 'string' ||
!/@sha256:[0-9a-f]{64}$/.test(lock.container.image)
) {
fail('container image must use a full sha256 manifest digest')
}
if (/:(latest|main|master)(?:@|$)/.test(lock.container.image)) {
fail('mutable container tag is forbidden')
}
for (const platform of ['linux/amd64', 'linux/arm64']) {
if (!fullDigest(lock.container.platform_digests?.[platform])) {
fail(`missing full ${platform} image digest`)
}
}
for (const field of [
'container_node',
'package_manager',
'playwright',
'vite',
'zlib',
'brotli',
]) {
if (typeof lock.runtime?.[field] !== 'string' || lock.runtime[field].length === 0) {
fail(`missing runtime ${field}`)
}
}
if (!/^pnpm@\d+\.\d+\.\d+$/.test(lock.runtime.package_manager)) {
fail('package manager must pin an exact pnpm version')
}
for (const browser of ['chromium', 'webkit']) {
if (
typeof lock.browsers?.[browser]?.revision !== 'string' ||
typeof lock.browsers?.[browser]?.version !== 'string' ||
lock.browsers[browser].revision.length === 0 ||
lock.browsers[browser].version.length === 0
) {
fail(`incomplete ${browser} fingerprint`)
}
}
if (
!Number.isSafeInteger(lock.fonts?.count) ||
lock.fonts.count <= 0 ||
!/^[0-9a-f]{64}$/.test(lock.fonts?.sha256 ?? '')
) {
fail('incomplete font inventory')
}
if (JSON.stringify(lock.render_fingerprint) !== JSON.stringify(approvedRenderFingerprint)) {
fail('render fingerprint does not match the approved matrix')
}
if (lock.pixel_baseline?.enabled !== false || !lock.pixel_baseline.external_gate) {
fail('pixel baseline must remain disabled until human approval is recorded')
}
return lock
}
export async function loadVisualRunnerContract() {
const lockPath = join(webRoot, 'visual-runner.lock.json')
const packagePath = join(webRoot, 'package.json')
const lockSource = await readFile(lockPath, 'utf8')
const lock = validateVisualRunnerLock(JSON.parse(lockSource))
const packageJSON = JSON.parse(await readFile(packagePath, 'utf8'))
const browserPath = join(
webRoot,
'node_modules',
'.pnpm',
`playwright-core@${packageJSON.devDependencies['@playwright/test']}`,
'node_modules',
'playwright-core',
'browsers.json',
)
const browserManifest = JSON.parse(await readFile(browserPath, 'utf8'))
const installedBrowsers = Object.fromEntries(
['chromium', 'webkit'].map((name) => {
const browser = browserManifest.browsers.find((candidate) => candidate.name === name)
if (!browser) fail(`Playwright package is missing ${name}`)
return [
name,
{
revision: browser.revision,
version: browser.browserVersion,
},
]
}),
)
const packageRuntime = {
package_manager: packageJSON.packageManager,
playwright: packageJSON.devDependencies['@playwright/test'],
vite: packageJSON.devDependencies.vite,
}
for (const [field, value] of Object.entries(packageRuntime)) {
if (lock.runtime[field] !== value) {
fail(`${field} mismatch: expected ${lock.runtime[field]}, received ${value}`)
}
}
if (JSON.stringify(installedBrowsers) !== JSON.stringify(lock.browsers)) {
fail(`browser package fingerprint mismatch: ${JSON.stringify(installedBrowsers)}`)
}
return {
lock,
lockSource,
lockSha256: createHash('sha256').update(lockSource).digest('hex'),
}
}
async function runManifestCheck() {
if (process.argv[2] !== '--manifest-only') {
fail('use --manifest-only for the host-side contract check')
}
const contract = await loadVisualRunnerContract()
process.stdout.write(
`${JSON.stringify({
check: 'manifest-only',
runner_execution_verified: false,
lock_sha256: contract.lockSha256,
container: contract.lock.container,
runtime: contract.lock.runtime,
browsers: contract.lock.browsers,
fonts: contract.lock.fonts,
render_fingerprint: contract.lock.render_fingerprint,
pixel_baseline: contract.lock.pixel_baseline,
})}\n`,
)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await runManifestCheck()
}