feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
// Smoke test for DiffEditor streaming rendering.
|
||||
// Starts the Vite demo, opens /diff in headless Chromium, and asserts Monaco renders text.
|
||||
|
||||
import net from 'node:net'
|
||||
import process from 'node:process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
const demoDir = path.join(repoRoot, 'examples', 'streaming-demo')
|
||||
|
||||
function isPortOpen(port, host = '127.0.0.1') {
|
||||
return new Promise((resolve) => {
|
||||
const sock = net.createConnection({ host, port })
|
||||
sock.on('connect', () => {
|
||||
sock.end()
|
||||
resolve(true)
|
||||
})
|
||||
sock.on('error', () => {
|
||||
sock.destroy()
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function findFreePort(start = 5173, end = 5190) {
|
||||
for (let port = start; port <= end; port++) {
|
||||
// If nothing is listening, consider it free.
|
||||
// (We only bind after Vite starts, so this is best-effort.)
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const open = await isPortOpen(port)
|
||||
if (!open)
|
||||
return port
|
||||
}
|
||||
throw new Error(`No free port found in ${start}-${end}`)
|
||||
}
|
||||
|
||||
async function waitForPort(port, ms = 20000) {
|
||||
const start = Date.now()
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const open = await isPortOpen(port)
|
||||
if (open)
|
||||
return
|
||||
if (Date.now() - start > ms)
|
||||
throw new Error(`Timed out waiting for port ${port}`)
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
}
|
||||
}
|
||||
|
||||
function killProcessTree(child) {
|
||||
if (!child || child.killed)
|
||||
return
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
}
|
||||
catch { }
|
||||
// Fallback hard kill after a short grace period.
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (!child.killed)
|
||||
child.kill('SIGKILL')
|
||||
}
|
||||
catch { }
|
||||
}, 3000).unref?.()
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort()
|
||||
if (!Number.isFinite(port))
|
||||
throw new Error(`Invalid PORT: ${process.env.PORT}`)
|
||||
|
||||
const vite = spawn(
|
||||
'pnpm',
|
||||
['-C', demoDir, 'dev', '--host', '127.0.0.1', '--port', String(port), '--strictPort'],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
|
||||
)
|
||||
|
||||
const logs = []
|
||||
vite.stdout.on('data', d => logs.push(String(d)))
|
||||
vite.stderr.on('data', d => logs.push(String(d)))
|
||||
|
||||
const onExit = () => killProcessTree(vite)
|
||||
process.on('SIGINT', onExit)
|
||||
process.on('SIGTERM', onExit)
|
||||
process.on('exit', onExit)
|
||||
|
||||
try {
|
||||
await waitForPort(port)
|
||||
|
||||
const url = `http://127.0.0.1:${port}/diff`
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage()
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle' })
|
||||
await page.waitForSelector('.monaco-diff-editor', { timeout: 20000 })
|
||||
await page.waitForTimeout(1200)
|
||||
|
||||
const before = await page.evaluate(() => {
|
||||
const viewLines = Array.from(document.querySelectorAll('.monaco-editor .view-lines'))
|
||||
const text = viewLines.map(v => (v.textContent ?? '')).join('\n').trim()
|
||||
return { viewLinesCount: viewLines.length, textLen: text.length, sample: text.slice(0, 120) }
|
||||
})
|
||||
|
||||
// Wait for at least one streaming tick from the demo.
|
||||
await page.waitForTimeout(700)
|
||||
const after = await page.evaluate(() => {
|
||||
const viewLines = Array.from(document.querySelectorAll('.monaco-editor .view-lines'))
|
||||
const text = viewLines.map(v => (v.textContent ?? '')).join('\n').trim()
|
||||
return { viewLinesCount: viewLines.length, textLen: text.length }
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
// We only assert that Monaco renders *some* text and stays rendered after
|
||||
// streaming ticks. Diff line wrapping/reflow can change `textLen`, so do
|
||||
// not assert monotonic growth.
|
||||
const ok = before.textLen > 0 && after.textLen > 0
|
||||
const result = { ok, url, before, after }
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
if (!ok) {
|
||||
console.error('Diff smoke failed; recent Vite logs:\n', logs.slice(-40).join(''))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
killProcessTree(vite)
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user