← Commit history

0.21.3: renderer survives package updates; the gate says what will render (#58)

Colby Knox ·ab153b6d2f ·28d ago ·parent 636bea3
2 files changed +83
install.sh

  
test/renderer.mjsadded+83
@@ -0,0 +1,83 @@+#!/usr/bin/env node+// Renderer-status tests (hero-studio#58): node test/renderer.mjs+//+// A package update replaced the package directory, node_modules included, so+// puppeteer-core vanished under a running studio and the human's Generate+// walked through two failing legs with nothing at the gate saying why. Now:+//   - puppeteer-core is looked for under ~/.cache/hero-studio first (where+//     install.sh puts it, outside the package dir),+//   - the pin follows this Node (24.x below 22.12),+//   - the server carries state.renderer, refreshed before every Generate,+//   - Generate refuses with RENDERER_NOT_READY and the fix when nothing can+//     render, and stops refusing the moment the pieces are back, no restart.+import { spawn } from 'child_process';+import net from 'net';+import fs from 'fs';+import os from 'os';+import path from 'path';+import { fileURLToPath } from 'url';+import { createRequire } from 'module';++const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');+const RP = createRequire(import.meta.url)(path.join(ROOT, 'renderer-paths.cjs'));+let pass = 0; const fails = [];+const ok = (name, cond, detail) => { if (cond) { pass++; console.log('  ok   ' + name); } else { fails.push(name); console.log('  FAIL ' + name + (detail ? ': ' + detail : '')); } };++// the pin follows Node+ok('pin: Node 18 gets puppeteer-core 24', RP.puppeteerPin('18.19.1') === '24');+ok('pin: Node 22.11 gets 24', RP.puppeteerPin('22.11.9') === '24');+ok('pin: Node 22.12 gets latest', RP.puppeteerPin('22.12.0') === 'latest');+ok('pin: Node 24 gets latest', RP.puppeteerPin('24.1.0') === 'latest');+ok('install command targets the cache dir, not the package dir', RP.puppeteerInstallCommand().includes('--prefix ' + RP.RENDERER_HOME) && !RP.puppeteerInstallCommand().includes(ROOT));++// an isolated box: nothing resolves, then the cache install appears+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'hs-renderer-'));+const RH = path.join(tmp, 'rh');+const chromeStub = path.join(tmp, 'chrome');+const env = { ...process.env, HOME: tmp, HERO_STUDIO_RENDERER_HOME: RH, HERO_STUDIO_RENDERER_ISOLATED: '1', PUPPETEER_CORE: '', CHROME: chromeStub, VSCODE_PROXY_URI: '', HERO_RENDERER: '' };+delete env.PUPPETEER_CORE; delete env.VSCODE_PROXY_URI; delete env.HERO_RENDERER;+const probe = (code) => new Promise((res) => {+  const c = spawn(process.execPath, ['-e', code], { env, cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });+  let out = ''; c.stdout.on('data', (d) => { out += d; }); c.on('close', () => res(out.trim()));+});+const st0 = JSON.parse(await probe("console.log(JSON.stringify(require('./renderer-paths.cjs').quickStatus()))"));+ok('isolated: local renderer not ok, names puppeteer-core', !st0.local.ok && /puppeteer-core/.test(st0.local.why), JSON.stringify(st0.local));+ok('isolated: pup not ok without a proxy URL', !st0.pup.ok);+const entry = path.join(RH, 'node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js');+fs.mkdirSync(path.dirname(entry), { recursive: true });+fs.writeFileSync(entry, 'export default {};\n');+fs.writeFileSync(path.join(RH, 'node_modules/puppeteer-core/package.json'), JSON.stringify({ name: 'puppeteer-core', version: '24.0.0', type: 'module', main: 'lib/puppeteer/puppeteer-core.js' }));+fs.writeFileSync(chromeStub, '#!/bin/sh\n');+const st1 = JSON.parse(await probe("console.log(JSON.stringify(require('./renderer-paths.cjs').quickStatus()))"));+ok('the cache install is found first, local ok', st1.local.ok && st1.local.puppeteer === entry, JSON.stringify(st1.local));++// the server: state.renderer, refusal, recovery without restart+fs.rmSync(entry); fs.rmSync(chromeStub);+const PORT = await new Promise((res) => { const s = net.createServer().listen(0, () => { const p = s.address().port; s.close(() => res(p)); }); });+const server = spawn(process.execPath, [path.join(ROOT, 'server.js'), '--port', String(PORT), '--ai-thread', 'renderer test', '--ttl', '1'],+  { env: { ...env, HERO_STUDIO_PORT: String(PORT) }, cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });+const base = 'http://localhost:' + PORT;+let up = false;+for (let i = 0; i < 50 && !up; i++) { try { const r = await fetch(base + '/state'); up = r.ok; } catch {} if (!up) await new Promise(r => setTimeout(r, 200)); }+ok('a hero-studio server answers on :' + PORT, up);+try {+  const st = await (await fetch(base + '/state')).json();+  ok('state.renderer is carried and reads not ready', st.renderer && !st.renderer.local.ok && !st.renderer.pup.ok, JSON.stringify(st.renderer));+  ok('state.renderer names the running version', st.renderer && st.renderer.running === JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version);+  const gen = async () => (await fetch(base + '/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aiThread: 'renderer test', fromGate: true }) })).json();+  const g0 = await gen();+  ok('Generate refuses with RENDERER_NOT_READY before any render leg', g0.ok === false && g0.errorCode === 'RENDERER_NOT_READY', JSON.stringify(g0).slice(0, 200));+  ok('the refusal carries the install command', /npm install --prefix/.test(g0.hint || ''), g0.hint);+  // the pieces come back (what install.sh or the human's npm install does); no restart+  fs.writeFileSync(entry, 'export default {};\n');+  fs.writeFileSync(chromeStub, '#!/bin/sh\n');+  const g1 = await gen();+  ok('Generate stops refusing once the pieces are back, without a restart', g1.errorCode !== 'RENDERER_NOT_READY', JSON.stringify(g1).slice(0, 200));+  const st2 = await (await fetch(base + '/state')).json();+  ok('state.renderer flipped to local ok', st2.renderer && st2.renderer.local.ok, JSON.stringify(st2.renderer));+} finally {+  server.kill();+  fs.rmSync(tmp, { recursive: true, force: true });+}+console.log('\n' + pass + ' passed, ' + fails.length + ' failed');+process.exit(fails.length ? 1 : 0);