Files
nexusterm/scripts/ipc-smoke.mjs
T
2026-09-17 16:16:44 +08:00

177 lines
6.8 KiB
JavaScript

// 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)