app/scripts/smoke-node.mjs
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { once } from 'node:events'
import { mkdtempSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:net'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { setTimeout as delay } from 'node:timers/promises'
import Sqlite from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import { readMigrationFiles } from 'drizzle-orm/migrator'
import { smokeMcp } from './smoke-mcp.mjs'
import { smokeSettings } from './smoke-settings.mjs'
import { smokeAssets } from './smoke-assets.mjs'
import { smokePlugins } from './smoke-plugins.mjs'
import { smokePluginServices } from './smoke-plugin-services.mjs'
const development = process.env.DEVPUSH_SMOKE_DEVELOPMENT === '1'
const sourceDevelopment = development && !process.env.DEVPUSH_SMOKE_APP_DIR
const appDir = resolve(
process.env.DEVPUSH_SMOKE_APP_DIR ||
fileURLToPath(new URL('../', import.meta.url)),
)
const packageDir = process.env.DEVPUSH_SMOKE_APP_DIR
? realpathSync(resolve(appDir, 'node_modules/devpush'))
: appDir
mkdirSync(resolve(appDir, '.tmp'), { recursive: true })
const directory = mkdtempSync(resolve(appDir, '.tmp/node-smoke-'))
const mainPath = resolve(directory, 'main.sqlite')
const logsPath = resolve(directory, 'logs.sqlite')
const stores = [new Sqlite(mainPath), new Sqlite(logsPath)]
for (const [index, store] of stores.entries()) {
store.pragma('foreign_keys = ON')
store.pragma('journal_mode = WAL')
if (!sourceDevelopment)
migrate(drizzle(store), {
migrationsFolder: resolve(
packageDir,
'src/server/db',
index === 0 ? 'migrations-sqlite' : 'migrations-logs',
),
})
}
const listener = createServer()
await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve))
const { port } = listener.address()
await new Promise((resolve, reject) =>
listener.close((error) => (error ? reject(error) : resolve())),
)
const origin = `http://127.0.0.1:${port}`
const authSecret = randomBytes(32).toString('hex')
// Do not inherit .env or provider/storage credentials from the developer's
// app. This production process uses only disposable stores and generated keys.
const env = {
PATH: process.env.PATH,
TMPDIR: process.env.TMPDIR,
NODE_ENV: development ? 'development' : 'production',
HOST: '127.0.0.1',
PORT: String(port),
DEVPUSH_BASE_URL: origin,
BETTER_AUTH_URL: origin,
DEVPUSH_MCP_URL: `${origin}/mcp`,
BETTER_AUTH_SECRET: authSecret,
DEVPUSH_DATABASE_URL: '',
DEVPUSH_LOG_DATABASE_URL: '',
DEVPUSH_DATABASE_AUTH_TOKEN: '',
DEVPUSH_LOG_DATABASE_AUTH_TOKEN: '',
DEVPUSH_DATABASE_PATH: mainPath,
DEVPUSH_LOG_DATABASE_PATH: logsPath,
DEVPUSH_SECRET_ENCRYPTION_KEY: randomBytes(32).toString('hex'),
}
let output = ''
const server = spawn(
sourceDevelopment ? 'npm' : process.execPath,
sourceDevelopment
? ['run', 'dev', '--', '--host', '127.0.0.1', '--port', String(port)]
: development
? [
'./node_modules/vite/bin/vite.js',
'dev',
'--host',
'127.0.0.1',
'--port',
String(port),
'--strictPort',
]
: ['.output/server/index.mjs'],
{
cwd: appDir,
env,
detached: sourceDevelopment && process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe'],
},
)
server.stdout.on('data', (chunk) => {
output += chunk
})
server.stderr.on('data', (chunk) => {
output += chunk
})
try {
for (let attempt = 0; ; attempt++) {
try {
assert.equal(
(
await fetch(`${origin}/api/health`, {
signal: AbortSignal.timeout(2000),
})
).status,
200,
)
break
} catch (error) {
if (attempt === 79 || server.exitCode !== null) throw error
await delay(250)
}
}
if (sourceDevelopment) {
assert.ok(
stores[0]
.prepare('pragma table_info(user)')
.all()
.some((column) => column.name === 'profile_confirmed_at'),
)
for (const [index, store] of stores.entries()) {
const migrations = readMigrationFiles({
migrationsFolder: resolve(
packageDir,
'src/server/db',
index === 0 ? 'migrations-sqlite' : 'migrations-logs',
),
})
assert.deepEqual(
store
.prepare('select hash from __drizzle_migrations order by created_at')
.all()
.map((row) => row.hash),
migrations.map((migration) => migration.hash),
)
}
console.log(
'Development startup: all main/log migrations applied before serving requests.',
)
}
const login = await fetch(`${origin}/auth/login`)
assert.equal(login.status, 200)
await smokeAssets(origin, await login.text(), { production: !development })
await smokePlugins(origin)
const settings = await fetch(`${origin}/settings`)
assert.equal(settings.status, 200)
assert.match(await settings.text(), /data-slot="skeleton"/)
await smokeSettings({
origin,
authSecret,
appDir,
development,
modulePrefix: process.env.DEVPUSH_SMOKE_APP_DIR
? `@fs/${packageDir}/src`
: 'src',
query: async (index, sql) => {
const statement = stores[index].prepare(sql)
if (statement.reader) return statement.all()
statement.run()
return []
},
})
await smokePluginServices({
origin,
authSecret,
appDir,
development,
modulePrefix: process.env.DEVPUSH_SMOKE_APP_DIR
? `@fs/${packageDir}/src`
: 'src',
query: async (index, sql) => {
const statement = stores[index].prepare(sql)
if (statement.reader) return statement.all()
statement.run()
return []
},
})
if (!development)
await smokeMcp({
origin,
authSecret,
supportsCimd: true,
appDir,
query: async (index, sql) => {
const statement = stores[index].prepare(sql)
if (statement.reader) return statement.all()
statement.run()
return []
},
})
console.log(
development
? 'Vite development: login, settings pending UI and concurrent settings saves passed.'
: 'Node production: login, settings pending UI, REST, SDK2026 MCP, OAuth consent, grant revocation and refresh replay passed.',
)
} catch (error) {
console.error(output)
throw error
} finally {
writeFileSync(resolve(directory, 'server.log'), output)
if (server.exitCode === null) {
const closed = once(server, 'exit')
if (sourceDevelopment && process.platform !== 'win32')
process.kill(-server.pid, 'SIGTERM')
else server.kill('SIGTERM')
await closed
}
stores.forEach((store) => store.close())
}