scripts/package-smoke.mjs
Rama main · réplica de lectura del repositorio original.
import { spawn, spawnSync } from "node:child_process";
import {
chmod,
cp,
mkdtemp,
readFile,
rm,
writeFile
} from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
function option(args, name) {
const index = args.indexOf(name);
if (index < 0) return undefined;
const value = args[index + 1];
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${name}`);
return value;
}
async function freePort() {
const server = net.createServer();
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (!address || typeof address === "string") {
server.close();
throw new Error("Unable to allocate test port");
}
const port = address.port;
await new Promise((resolve) => server.close(resolve));
return port;
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env,
encoding: "utf8",
windowsHide: true,
...(options.timeout ? { timeout: options.timeout } : {})
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(
`${command} failed with exit code ${result.status}: ${(result.stderr || result.stdout).trim()}`
);
}
return result;
}
async function waitForHealth(url, child) {
const deadline = Date.now() + 15_000;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`Packaged Agent exited early with code ${child.exitCode}`);
}
try {
const response = await fetch(`${url}/api/v1/health`);
if (response.ok) return response.json();
} catch {
// Startup race.
}
await new Promise((resolve) => setTimeout(resolve, 150));
}
throw new Error("Timed out waiting for packaged Agent health endpoint");
}
async function main() {
const rootArg = option(process.argv.slice(2), "--root");
if (!rootArg) {
throw new Error("Usage: node scripts/package-smoke.mjs --root <runtime-root>");
}
const sourceRoot = path.resolve(rootArg);
const temp = await mkdtemp(path.join(os.tmpdir(), "palmtty-installed-smoke-"));
const root = path.join(temp, "PalmTTY");
let child;
let childClosed;
let stderr = "";
try {
// Execute from a detached temporary directory so no source-repository
// relative path can accidentally satisfy runtime lookups.
await cp(sourceRoot, root, { recursive: true });
const node = path.join(
root,
"runtime",
process.platform === "win32" ? "node.exe" : "node"
);
const cli = path.join(root, "tools", "installed-cli.mjs");
const agent = path.join(root, "app", "dist", "index.js");
const qwenSidecar = path.join(root, "tools", "qwen3-tts-server.py");
if (!(await readFile(qwenSidecar, "utf8")).includes("Qwen3-TTS")) {
throw new Error("Packaged runtime is missing the optional Qwen3-TTS sidecar script");
}
if (process.platform !== "win32") {
await chmod(node, 0o755);
await chmod(path.join(root, "bin", "palmtty"), 0o755);
}
const manifest = JSON.parse(
await readFile(path.join(root, "release-manifest.json"), "utf8")
);
const env = { ...process.env };
delete env.PALMTTY_INSTALL_ROOT;
const bundledRuntimeVersion = run(node, ["--version"], { cwd: root, env }).stdout.trim();
const expectedNode = (await readFile(path.resolve(".node-version"), "utf8")).trim();
const [expectedMajor, expectedMinor, expectedPatch] = expectedNode.split(".").map(Number);
const [actualMajor, actualMinor, actualPatch] =
bundledRuntimeVersion.slice(1).split(".").map(Number);
if (bundledRuntimeVersion !== manifest.nodeVersion ||
actualMajor !== expectedMajor || actualMinor < expectedMinor ||
(actualMinor === expectedMinor && actualPatch < expectedPatch)) {
throw new Error(`Bundled Node version mismatch: binary ${bundledRuntimeVersion}, manifest ${manifest.nodeVersion}, baseline ${expectedNode}`);
}
const version = run(node, [cli, "version"], { cwd: root, env }).stdout.trim();
if (version !== manifest.version) {
throw new Error(
`Installed CLI reported version ${version}, expected ${manifest.version}`
);
}
const port = await freePort();
const configPath = path.join(temp, "config.yaml");
const envPath = path.join(temp, "credentials.env");
await writeFile(
configPath,
`server:
port: ${port}
exposure:
mode: local
auth:
enabled: true
tokenEnv: PALMTTY_ACCESS_TOKEN
maxLoginSessions: 4
sessionTtlMinutes: 60
sessions:
maxSessions: 2
exitedRetentionMinutes: 1
scrollbackLines: 1000
replayBytes: 262144
maxSocketBufferedBytes: 262144
`,
"utf8"
);
await writeFile(
envPath,
"PALMTTY_ACCESS_TOKEN=package-smoke-token-1234567890\n",
{ encoding: "utf8", mode: 0o600 }
);
if (process.platform !== "win32") await chmod(envPath, 0o600);
run(
node,
[agent, "--config", configPath, "--env-file", envPath, "--preflight"],
{ cwd: root, env }
);
child = spawn(
node,
[agent, "--config", configPath, "--env-file", envPath],
{
cwd: root,
env,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true
}
);
childClosed = new Promise((resolve) => child.once("close", resolve));
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
if (stderr.length < 8192) stderr += chunk;
});
const url = `http://127.0.0.1:${port}`;
const health = await waitForHealth(url, child);
if (
health?.status !== "ok" ||
health?.version !== manifest.version ||
health?.platform !== process.platform
) {
throw new Error(
`Packaged Agent health mismatch: ${JSON.stringify(health)}`
);
}
const web = await fetch(`${url}/`);
if (!web.ok || !(await web.text()).includes("<!doctype html>")) {
throw new Error("Packaged Agent did not serve the compiled Web UI");
}
console.log(
`[PalmTTY] installed-runtime smoke passed: ${manifest.version} ${manifest.platform}-${manifest.arch}`
);
} catch (error) {
if (child && child.exitCode !== null) {
throw new Error(
`${error instanceof Error ? error.message : error}; packaged Agent stderr: ${stderr ?? ""}`
);
}
throw error;
} finally {
if (child) {
if (child.exitCode === null && child.signalCode === null) child.kill();
// "exit" precedes closure of Node/Win32 stdio file handles; wait for
// "close" before attempting to remove a packaged runtime directory.
await Promise.race([
childClosed,
new Promise((resolve) => setTimeout(resolve, 5000))
]);
}
try {
await rm(temp, {
recursive: true, force: true, maxRetries: 60, retryDelay: 250
});
} catch (error) {
if (process.platform !== "win32" ||
!["EBUSY", "EPERM", "ENOTEMPTY"].includes(error?.code)) throw error;
console.warn(`[PalmTTY] Windows deferred smoke temp cleanup: ${error.code}`);
}
}
}
main().catch((error) => {
console.error(
`[PalmTTY] package smoke failed: ${error instanceof Error ? error.message : error}`
);
process.exitCode = 1;
});