// Drives the running app's renderer over the Chrome DevTools Protocol, calling // the same `window.nexusterm` API the UI calls. This exercises the real path — // preload bridge, ipcRenderer.invoke, main-process handler, zod validation, // repository, SQLite, and the reply — rather than a stand-in for it. // // Usage: // npm run build // ./node_modules/electron/dist/electron --no-sandbox --remote-debugging-port=9222 . // npm run smoke const PORT = 9222 async function findPage() { for (let attempt = 0; attempt < 40; attempt++) { try { const res = await fetch(`http://127.0.0.1:${PORT}/json`) const targets = await res.json() const page = targets.find((t) => t.type === 'page' && t.webSocketDebuggerUrl) if (page) return page } catch { // DevTools endpoint not up yet. } await new Promise((r) => setTimeout(r, 500)) } throw new Error('no debuggable page found on port ' + PORT) } let nextId = 1 function evaluate(ws, expression) { const id = nextId++ return new Promise((resolve) => { const onMessage = (event) => { const msg = JSON.parse(event.data) if (msg.id !== id) return ws.removeEventListener('message', onMessage) if (msg.error) return resolve({ ok: false, error: JSON.stringify(msg.error) }) const result = msg.result if (result.exceptionDetails) { const d = result.exceptionDetails return resolve({ ok: false, error: d.exception?.description ?? d.text ?? 'exception' }) } return resolve({ ok: true, value: result.result.value }) } ws.addEventListener('message', onMessage) ws.send( JSON.stringify({ id, method: 'Runtime.evaluate', params: { expression, awaitPromise: true, returnByValue: true }, }), ) }) } const page = await findPage() const ws = new WebSocket(page.webSocketDebuggerUrl) await new Promise((resolve, reject) => { ws.addEventListener('open', resolve, { once: true }) ws.addEventListener('error', reject, { once: true }) }) const script = `(async () => { const api = window.nexusterm.tree const report = {} const findByLabel = (t, label) => { for (const n of t) { if (n.kind === 'host' && n.label === label) return n if (n.kind === 'folder') { const hit = findByLabel(n.children, label); if (hit) return hit } } return null } const findFolder = (t, name) => { for (const n of t) { if (n.kind === 'folder' && n.name === name) return n if (n.kind === 'folder') { const hit = findFolder(n.children, name); if (hit) return hit } } return null } // Names of a layer in tree order, for asserting interleaving. const layerNames = (nodes) => nodes.map(n => n.kind === 'folder' ? n.name : n.label) const before = await api.list() report.initialRootCount = before.length // --- create and nest ------------------------------------------------- let tree = await api.createFolder({ parentId: null, name: 'SMOKE-alpha' }) const alpha = findFolder(tree, 'SMOKE-alpha') tree = await api.createFolder({ parentId: alpha.id, name: 'SMOKE-beta' }) const beta = findFolder(tree, 'SMOKE-beta') report.nestedFolder = beta && beta.parentId === alpha.id tree = await api.createHost({ parentId: beta.id, label: 'SMOKE-host', hostname: '10.9.9.9', port: 2222, username: 'tester', authType: 'password', keyPath: null, color: null, }) report.createdHost = !!findByLabel(tree, 'SMOKE-host') tree = await api.renameFolder({ id: alpha.id, name: 'SMOKE-renamed' }) report.renamed = !!findFolder(tree, 'SMOKE-renamed') // --- interleaving ---------------------------------------------------- // Two folders with a host dropped between them. Not expressible before the // tree was interleaved: the two sort_order sequences had no common scale. tree = await api.createFolder({ parentId: null, name: 'SMOKE-first' }) const first = findFolder(tree, 'SMOKE-first') tree = await api.createFolder({ parentId: null, name: 'SMOKE-third' }) const third = findFolder(tree, 'SMOKE-third') tree = await api.createHost({ parentId: null, label: 'SMOKE-middle', hostname: '10.9.9.10', port: 22, username: 'tester', authType: 'password', keyPath: null, color: null, }) const middle = findByLabel(tree, 'SMOKE-middle') tree = await api.moveNode({ id: middle.id, parentId: null, prevSiblingId: first.id, nextSiblingId: third.id }) const order = layerNames(tree) report.interleaved = order.indexOf('SMOKE-first') < order.indexOf('SMOKE-middle') && order.indexOf('SMOKE-middle') < order.indexOf('SMOKE-third') report.interleavedOrder = order.filter(n => n.startsWith('SMOKE-')) // --- move back out to the root layer --------------------------------- const hostInBeta = findByLabel(tree, 'SMOKE-host') tree = await api.moveNode({ id: hostInBeta.id, parentId: null, prevSiblingId: null, nextSiblingId: null }) report.movedToRoot = tree.some(n => n.kind === 'host' && n.label === 'SMOKE-host') // --- a move into its own descendant must be refused ------------------ const renamed = findFolder(tree, 'SMOKE-renamed') const betaAgain = findFolder(tree, 'SMOKE-beta') try { await api.moveNode({ id: renamed.id, parentId: betaAgain.id, prevSiblingId: null, nextSiblingId: null }) report.cycleRefused = false } catch (e) { report.cycleRefused = /descendant/.test(String(e && e.message)) report.cycleMessage = String(e && e.message).slice(0, 120) } // --- validation ------------------------------------------------------ try { await api.createHost({ parentId: null, label: '', hostname: '', port: 99999, username: '', authType: 'nope', keyPath: null, color: null }) report.validationRejected = false } catch { report.validationRejected = true } // --- local PTY ------------------------------------------------------- // This catches platform-specific shell selection and packaged node-pty // failures, neither of which the repository tests can exercise. const local = await window.nexusterm.session.openLocal() if (!local.ok) throw new Error(local.message) report.localShell = true await window.nexusterm.session.close(local.sessionId) // --- clean up -------------------------------------------------------- tree = await api.list() for (const node of tree) { if (node.kind === 'folder' && node.name.startsWith('SMOKE-')) await api.deleteNode(node.id) if (node.kind === 'host' && node.label.startsWith('SMOKE-')) await api.deleteNode(node.id) } const final = await api.list() report.cleanedUp = !final.some(n => (n.kind === 'folder' && n.name.startsWith('SMOKE-')) || (n.kind === 'host' && n.label.startsWith('SMOKE-'))) report.finalRootCount = final.length return report })()` const result = await evaluate(ws, script) console.log(JSON.stringify(result, null, 2)) ws.close() if (!result.ok) process.exit(1)