177 lines
6.8 KiB
JavaScript
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)
|