openvibers/openvibe.bench Commits

Showcase: a generated answer renders from its entry type's field (#23)

OP OpenVibers committed 14 days ago · 96e205828b · 4 files, +39 −4

The generator stored every showcase answer as { text } (only html_page got
{ html }), and the renderer reads html, svg, glsl or gltf_base64, so all
seeded entries showed 'Nothing was generated'. payloadFor maps each entry
type to its field and takes the code out of a markdown fence; entries stored
as { text } render the same way.

Changes

ddiff --git a/server/bench/ai.js b/server/bench/ai.js
iindex 86ce138..974812d 100644
---- a/server/bench/ai.js
++++ b/server/bench/ai.js
@@@ -245,7 +245,9 @@ function createBenchAi({ config, fetchImpl = globalThis.fetch, log = console })
const provider = String(prov.provider || run.provider || (slot && slot.provider) || 'ai').slice(0, 80);
const ui_check = run.ui_check || (run.output && run.output.ui_check) || null;
const html = /html_page/.test(kind || '') ? sanitizeHtml(text, { maxBytes: ai.maxHtmlBytes }) : text;
-- const payload = /html_page/.test(kind || '') ? { html } : { text };
++ // A showcase entry renders from its type's field (render.js payloadFor): { html } for a page that runs in the
++ // isolated frame (not sanitized: its scripts are the point), { svg } or { glsl }. html_page stays the inert sandbox.
++ const payload = /html_page/.test(kind || '') ? { html } : require('./render').payloadFor(kind, text);
return {
payload,
html,
ddiff --git a/server/bench/render.js b/server/bench/render.js
iindex c2f1a0f..6c36e57 100644
---- a/server/bench/render.js
++++ b/server/bench/render.js
@@@ -48,6 +48,29 @@ const MAX_ENTRY_BYTES = MAX_BYTES;
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
++/** The payload field each showcase entry type renders from. A model answers in text; this is where that text goes. */
++const PAYLOAD_FIELD = Object.freeze({
++ html_game: 'html', three_scene: 'html', motion_html: 'html', ui_page: 'html', dataviz_page: 'html', component_set: 'html',
++ svg_art: 'svg', shader_glsl: 'glsl',
++});
++/** The code inside a model's answer: the first fenced block when it wrote one (```html … ```), else the whole answer. */
++function codeOf(text) {
++ const t = String(text == null ? '' : text);
++ const m = /```[\w-]*[^\S\n]*\n([\s\S]*?)```/.exec(t);
++ return (m ? m[1] : t).trim();
++}
++/** A generated answer as the payload its entry type renders: { html } | { svg } | { glsl }, or { text } for the rest. */
++function payloadFor(entryType, text) {
++ const field = PAYLOAD_FIELD[entryType];
++ return field ? { [field]: codeOf(text) } : { text: String(text == null ? '' : text) };
++}
++/** An entry stored before payloadFor existed held only { text }: read it as its entry type's field. */
++function renderablePayload(kind, payload) {
++ const p = payload || {};
++ if (p.html || p.svg || p.glsl || p.gltf_base64 || typeof p.text !== 'string') return p;
++ return { ...p, ...payloadFor(kind && kind.entry, p.text) };
++}
++
/** The CSP a model document runs under. The nonce is ours; model scripts get it, nothing else does. */
function cspFor(nonce) {
return [
@@@ -116,7 +139,7 @@ function importMap() {
/** The model document for a script kind: the model's own HTML, under our nonce and our CSP. */
function modelDocument(kind, payload, opts) {
const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');
-- const p = payload || {};
++ const p = renderablePayload(kind, payload);
let body = '';
if (typeof p.html === 'string' && p.html.trim()) body = p.html;
else if (typeof p.glsl === 'string' && p.glsl.trim()) body = glslPage(p.glsl);
@@@ -215,5 +238,5 @@ function wrapperDocument(opts) {
module.exports = {
RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,
-- FRAME_ANCESTORS, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,
++ FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, limitMsOf, modeOf, modelDocument, wrapperDocument,
};
ddiff --git a/server/http/render.js b/server/http/render.js
iindex 4444625..cee3dfd 100644
---- a/server/http/render.js
++++ b/server/http/render.js
@@@ -58,7 +58,8 @@ function createRenderRoutes(ctx) {
const kind = entry.kind;
const nonce = crypto.randomBytes(16).toString('base64');
if (kind && renderLib.modeOf(kind) === 'static') {
-- const svg = renderLib.sanitizeSvg(entry.payload && entry.payload.svg ? entry.payload.svg : (entry.payload && entry.payload.html) || '');
++ const p = renderLib.renderablePayload(kind, entry.payload);
++ const svg = renderLib.sanitizeSvg(p.svg || p.html || '');
return bare(res)
.set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox")
.type('image/svg+xml')
ddiff --git a/test/showcase.test.js b/test/showcase.test.js
iindex c79ef8c..47b5342 100644
---- a/test/showcase.test.js
++++ b/test/showcase.test.js
@@@ -113,6 +113,15 @@ const HOSTILE = [
assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');
});
++ await check('a generated answer renders from its entry type\'s field (10-30: every seeded entry was { text } and showed nothing)', async () => {
++ assert.deepStrictEqual(renderLib.payloadFor('ui_page', 'Here you go:\n```html\n<p>hi</p>\n```\nEnjoy.'), { html: '<p>hi</p>' });
++ assert.deepStrictEqual(renderLib.payloadFor('svg_art', '<svg viewBox="0 0 1 1"></svg>'), { svg: '<svg viewBox="0 0 1 1"></svg>' });
++ assert.deepStrictEqual(renderLib.payloadFor('shader_glsl', '```glsl\nvoid main(){}\n```'), { glsl: 'void main(){}' });
++ assert.deepStrictEqual(renderLib.payloadFor('model_answer', 'plain'), { text: 'plain' });
++ const doc = renderLib.modelDocument({ id: 'design.ui', entry: 'ui_page' }, { text: '```html\n<!doctype html><html><head></head><body><b>seeded</b></body></html>\n```' }, { nonce: 'N3' });
++ assert.ok(doc.includes('<b>seeded</b>') && !doc.includes('Nothing was generated'), 'an old { text } entry renders');
++ });
++
await check('the render origin vendors three 0.170.0 for opaque-origin frames', async () => {
const v = await t.get('/vendor/three.module.js');
assert.strictEqual(v.status, 200);