124 lines
7.4 KiB
JavaScript
124 lines
7.4 KiB
JavaScript
/** Offline VM contracts only: no Electron, browser, layout or native input is executed. */
|
|
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import vm from 'node:vm'
|
|
import test from 'node:test'
|
|
|
|
const source = fs.readFileSync(new URL('./verify-b04-image-native.cjs', import.meta.url), 'utf8')
|
|
const flush = async () => { for (let i = 0; i < 12; i++) await Promise.resolve() }
|
|
const deferred = () => { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b }); return { promise, resolve, reject } }
|
|
const validReport = () => ({ samples: [{ height: 232, textLength: 35, imageHeight: 180 }], errors: [] })
|
|
function harness({ report = validReport(), holdReady = false, holdLoad = false, holdFrames = false, frameError } = {}) {
|
|
const ready = deferred(), load = deferred(), frames = deferred()
|
|
const state = { writes: [], exits: [], events: {}, timers: new Map(), windows: [], logs: [] }
|
|
let id = 0
|
|
const app = { setPath: (key, value) => { state.profile = { key, value } }, whenReady: () => ready.promise, exit: code => state.exits.push(code) }
|
|
class BrowserWindow {
|
|
constructor(options) {
|
|
state.windows.push(options); state.window = this; this.destroyed = false
|
|
this.webContents = { on: (name, callback) => { state.events[name] = callback }, executeJavaScript: async code => {
|
|
state.rendererCode = code
|
|
if (frameError) throw Error(frameError)
|
|
if (holdFrames) return frames.promise
|
|
return report
|
|
} }
|
|
}
|
|
loadFile(file) { state.loaded = file; return holdLoad ? load.promise : Promise.resolve() }
|
|
isDestroyed() { return this.destroyed }
|
|
destroy() { this.destroyed = true }
|
|
}
|
|
const modules = {
|
|
electron: { app, BrowserWindow, session: { defaultSession: { webRequest: { onBeforeRequest: callback => { state.network = callback } } } } },
|
|
'node:path': path, 'node:os': { tmpdir: () => '/mock-temp' },
|
|
'node:fs': { existsSync: () => true, mkdtempSync: prefix => prefix + 'isolated', writeFileSync: (file, body) => state.writes.push({ file, report: JSON.parse(body) }) }
|
|
}
|
|
vm.runInNewContext(source, { require: name => { assert.ok(name in modules); return modules[name] }, process: { argv: ['electron', 'probe.cjs', '/mock-fixture'] },
|
|
console: { log: value => state.logs.push(value) }, setTimeout: (callback, delay) => { state.timers.set(++id, { callback, delay }); return id }, clearTimeout: key => state.timers.delete(key) })
|
|
if (!holdReady) ready.resolve()
|
|
return { state, ready, load, frames, expire: () => { const timer = [...state.timers.values()][0]; assert.equal(timer.delay, 20000); timer.callback() } }
|
|
}
|
|
function result(h, status, pattern) {
|
|
assert.equal(h.state.writes.length, 1)
|
|
const report = h.state.writes[0].report
|
|
assert.equal(report.result, status)
|
|
assert.deepEqual(h.state.exits, [status === 'PASS' ? 0 : 1])
|
|
if (pattern) assert.match(report.error, pattern)
|
|
assert.match(report.limits, /not the installed application/)
|
|
assert.equal(h.state.timers.size, 0)
|
|
return report
|
|
}
|
|
|
|
test('B04 mock host preserves security, frame scheduling, deadline and report', async () => {
|
|
const h = harness(); await flush()
|
|
const report = result(h, 'PASS')
|
|
assert.equal(report.phase, 'checking-native-report')
|
|
const options = h.state.windows[0]
|
|
assert.equal(options.show, true)
|
|
assert.equal(options.webPreferences.backgroundThrottling, false)
|
|
assert.equal(options.webPreferences.sandbox, true)
|
|
assert.equal(options.webPreferences.contextIsolation, true)
|
|
assert.equal(options.webPreferences.nodeIntegration, false)
|
|
assert.equal(h.state.profile.key, 'userData')
|
|
assert.match(h.state.profile.value, /ycode-b04-native-/)
|
|
assert.equal(h.state.window.destroyed, true)
|
|
for (const [url, cancel] of [['https://example.invalid/image', true], ['file:///fixture.js', false], ['data:image/png;base64,AA', false], ['blob:local', false]]) {
|
|
let actual; h.state.network({ url }, value => { actual = value.cancel }); assert.equal(actual, cancel)
|
|
}
|
|
assert.match(h.state.rendererCode, /window\.__b04Error/)
|
|
assert.match(h.state.rendererCode, /window\.__b04Report/)
|
|
})
|
|
for (const [label, report, message] of [
|
|
['zero layout', { samples: [{ height: 0, textLength: 0 }] }, /No nonzero/],
|
|
['large text', { samples: [{ height: 232, textLength: 1001 }] }, /text\/height spike/],
|
|
['large layout', { samples: [{ height: 2001, textLength: 35 }] }, /text\/height spike/],
|
|
['missing samples', {}, /some|samples/]
|
|
]) test(`B04 mock rejects ${label}`, async () => { const h = harness({ report }); await flush(); result(h, 'FAIL', message) })
|
|
test('B04 mock propagates fixture rejection', async () => { const h = harness({ frameError: 'synthetic no URI text assertion' }); await flush(); result(h, 'FAIL', /no URI text assertion/) })
|
|
for (const [option, phase] of [['holdReady', 'waiting-for-app'], ['holdLoad', 'loading-fixture'], ['holdFrames', 'waiting-for-native-frames']]) {
|
|
test(`B04 mock deadline covers ${phase}`, async () => {
|
|
const h = harness({ [option]: true }); await flush(); h.expire()
|
|
const report = result(h, 'FAIL', /timed out after 20s/); assert.equal(report.phase, phase)
|
|
if (option === 'holdReady') h.ready.resolve()
|
|
if (option === 'holdLoad') h.load.resolve()
|
|
if (option === 'holdFrames') h.frames.resolve(validReport())
|
|
await flush(); assert.equal(h.state.writes.length, 1)
|
|
if (option === 'holdReady') assert.equal(h.state.windows.length, 0)
|
|
})
|
|
}
|
|
for (const [event, args, message] of [
|
|
['render-process-gone', [{}, { reason: 'crashed' }], /Renderer gone: crashed/],
|
|
['did-fail-load', [{}, -6, 'FILE_NOT_FOUND'], /Page load failed: -6 FILE_NOT_FOUND/]
|
|
]) test(`B04 mock records ${event} once`, async () => {
|
|
const h = harness({ holdLoad: true }); await flush(); h.state.events[event](...args); h.load.reject(Error('later rejection')); await flush(); result(h, 'FAIL', message)
|
|
})
|
|
test('B04 mock fails renderer console errors', async () => {
|
|
const h = harness({ holdFrames: true }); await flush()
|
|
h.state.events['console-message']({ level: 'error', message: 'synthetic renderer error' })
|
|
h.frames.resolve(validReport()); await flush(); result(h, 'FAIL', /synthetic renderer error/)
|
|
})
|
|
test('B04 fixture retains real rAF sampling and decode assertions (source contract only)', () => {
|
|
const fixture = fs.readFileSync(new URL('./b04-image-lifecycle-fixture.tsx', import.meta.url), 'utf8')
|
|
assert.match(fixture, /requestAnimationFrame\(\(\) => \{sample\(/)
|
|
assert.match(fixture, /await folds\(true\)/)
|
|
assert.match(fixture, /naturalWidth === 320/)
|
|
assert.match(fixture, /imageHeight > 0/)
|
|
assert.match(fixture, /no URI text/)
|
|
})
|
|
|
|
for (const scenario of ['success', 'fixture-error', 'pending']) test(`B04 renderer polling contract: ${scenario} (mock window)`, async () => {
|
|
const h = harness(); await flush()
|
|
const report = validReport(), window = {}, timers = []
|
|
if (scenario === 'success') window.__b04Report = report
|
|
if (scenario === 'fixture-error') { window.__b04Error = 'synthetic fixture assertion'; window.__b04Report = report }
|
|
const promise = vm.runInNewContext(h.state.rendererCode, { window, setTimeout: (callback, delay) => timers.push({ callback, delay }) })
|
|
if (scenario === 'fixture-error') await assert.rejects(promise, /synthetic fixture assertion/)
|
|
else {
|
|
if (scenario === 'pending') {
|
|
assert.equal(timers.length, 1); assert.equal(timers[0].delay, 25)
|
|
window.__b04Report = report; timers[0].callback()
|
|
}
|
|
assert.equal(await promise, report)
|
|
}
|
|
})
|