From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/workbench/qa-refresh.js | 141 +++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 prototypes/workbench/qa-refresh.js (limited to 'prototypes/workbench/qa-refresh.js') diff --git a/prototypes/workbench/qa-refresh.js b/prototypes/workbench/qa-refresh.js new file mode 100644 index 0000000..c74ef4c --- /dev/null +++ b/prototypes/workbench/qa-refresh.js @@ -0,0 +1,141 @@ +// Run through qa-refresh.py. Only the archived app module is replaced for the +// baseline; runtime, compiler, output adapter and Wasm stay identical. +async (page) => { + const baselineApp = /* BASELINE_APP */ null; + const checks = [], samples = [], errors = []; + const check = (ok, name) => { if (!ok) throw new Error(name); checks.push(name); }; + const onDialog = dialog => dialog.accept(); + const onError = error => errors.push(error.message); + page.on('dialog', onDialog); page.on('pageerror', onError); + let variant = 'after'; + const routeApp = async route => { + const response = await route.fetch(); + await route.fulfill({response, body:variant === 'before' ? baselineApp : await response.text()}); + }; + const client = await page.context().newCDPSession(page); + const metrics = async () => Object.fromEntries((await client.send('Performance.getMetrics')).metrics.map(m => [m.name, m.value])); + const source = ['state x = 40', 'input move = KeyM', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x)', + ...Array.from({length:22}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 30}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }'].join('\n'); + const apply = async value => { await page.locator('#source').fill(value); await page.locator('#apply').click(); }; + const snapshot = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const reload = async () => { + await page.reload(); await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); await page.locator('#scene').focus(); + }; + try { + if (!baselineApp) throw new Error('Archived app source is required.'); + await page.route('**/app.js', routeApp); + for (const mode of ['before', 'after', 'after', 'before', 'before', 'after']) { + variant = mode; await reload(); + await page.evaluate(() => { + window.__tap = () => { + const scene = document.getElementById('scene'); + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) window.__tap(); + }); + await client.send('Performance.enable'); + const before = await metrics(); + const sample = await page.evaluate(() => { + const observer = new MutationObserver(() => {}); + for (const id of ['line-numbers','source-size','cursor-position','draft-status']) + observer.observe(document.getElementById(id), {subtree:true, childList:true, characterData:true, attributes:true}); + const originalEncode = TextEncoder.prototype.encode; + let encodingCalls = 0, encodedBytes = 0; + TextEncoder.prototype.encode = function(...args) { + const value = originalEncode.apply(this, args); encodingCalls++; encodedBytes += value.byteLength; return value; + }; + let elapsedMs; + try { + const start = performance.now(); + for (let i = 0; i < 100; i++) window.__tap(); + elapsedMs = performance.now() - start; + } finally { TextEncoder.prototype.encode = originalEncode; } + const editorMutations = observer.takeRecords().length; + observer.disconnect(); + return {elapsedMs, encodingCalls, encodedBytes, editorMutations}; + }); + const after = await metrics(); + const s = await snapshot(); + samples.push({variant:mode, taps:100, events:200, ...sample, + metrics:Object.fromEntries(['LayoutCount','LayoutDuration','RecalcStyleCount','RecalcStyleDuration','ScriptDuration'] + .map(key => [key, after[key]-before[key]])), + final:{states:s.states, views:s.views, successfulEvents:s.successfulEvents, tick:s.tick, lastError:s.lastError}, + renderedX:await page.locator('[data-view-id="moving"]').getAttribute('x')}); + } + check(samples.every(s => JSON.stringify(s.final) === JSON.stringify(samples[0].final) && s.renderedX === '70'), + 'six runs preserve exact authored states, outputs, event counts and rendered position'); + check(samples.filter(s => s.variant === 'before').every(s => s.encodingCalls === 400 && s.editorMutations > 0), + 'archived path reproduces unrelated editor work on every event'); + check(samples.filter(s => s.variant === 'after').every(s => s.encodingCalls === 0 && s.editorMutations === 0), + 'runtime input performs no source encoding or editor metadata mutations'); + + variant = 'after'; await reload(); + const draft = source + '\n# 변경🙂\n'; + await page.locator('#source').fill(draft); + const editor = () => page.evaluate(() => ({ + source:document.getElementById('source').value, + size:document.getElementById('source-size').textContent, + lines:document.getElementById('line-numbers').textContent, + status:document.getElementById('draft-status').textContent, + cursor:document.getElementById('cursor-position').textContent, + expectedBytes:new TextEncoder().encode(document.getElementById('source').value).byteLength, + })); + let e = await editor(); + check(e.status === '미적용 변경' && e.size.startsWith(e.expectedBytes.toLocaleString('en-US')) && + e.lines.split('\n').length === draft.split('\n').length, + 'Unicode source editing updates byte count, line numbers and dirty status'); + await page.locator('#scene').focus(); + const prior = await editor(); + await page.keyboard.press('KeyM'); + await page.locator('#step').click(); + check(JSON.stringify(await editor()) === JSON.stringify(prior) && (await snapshot()).states.x === 40.25 && (await snapshot()).tick === 1, + 'keyboard input and frame stepping preserve unapplied editor metadata'); + await page.locator('#reset').click(); + check(JSON.stringify(await editor()) === JSON.stringify(prior) && (await snapshot()).states.x === 40, + 'reset preserves draft and its metadata while restoring active state'); + await page.locator('#apply').click(); + check((await editor()).status === '적용됨' && !(await snapshot()).dirty, + 'successful apply updates draft status without requiring a source edit'); + await apply(draft + 'state broken ='); + check((await editor()).status === '미적용 변경' && (await snapshot()).appliedSource === draft && + await page.locator('#error-location').isVisible(), + 'failed apply preserves dirty status and active work with a source diagnostic'); + await page.locator('#error-location').click(); + check((await editor()).cursor.startsWith(`${draft.split('\n').length}행`), + 'diagnostic navigation still updates the cursor position'); + await page.locator('#source').fill('state count = 1\n'); + await page.locator('#source').press('End'); + await page.locator('#source').press('Tab'); + e = await editor(); + check(e.source.endsWith(' ') && e.size.startsWith(e.expectedBytes.toLocaleString('en-US')), + 'programmatic Tab insertion updates source metadata'); + await apply(''); + check((await editor()).status === '적용됨' && (await snapshot()).views.length === 0, + 'applying an empty program has a correct applied status'); + await page.locator('[data-snippet="state"]').click(); + e = await editor(); + check(e.status === '미적용 변경' && e.source.includes('state count') && + e.size.startsWith(e.expectedBytes.toLocaleString('en-US')), + 'snippet insertion updates source metadata'); + check(errors.length === 0, 'no uncaught browser errors'); + await page.unroute('**/app.js', routeApp); + await page.reload(); await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + const evidence = {utc:new Date().toISOString(), checks, errors, samples, source, + browser:await page.evaluate(() => navigator.userAgent), + scope:'100 synchronous synthetic keyboard taps after 20 warm-up taps. Timings include encoding/mutation instrumentation, exclude actual input arrival and presentation latency. CDP metrics may include browser work around the batch.', + setup:'Only app.js is substituted from the verified archive for before runs. Other production modules are identical. No phase-timing wrappers in this comparison.'}; + await page.evaluate(value => {window.__pureRefreshEvidence = value;}, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack || error.message, checks, errors, samples}; + await page.evaluate(value => {window.__pureRefreshEvidence = value;}, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); + page.off('dialog', onDialog); page.off('pageerror', onError); await client.detach(); + } +} -- cgit v1.2.3