Files
paseo/packages/server/scripts/supervisor-entrypoint.env.test.ts
paseo-bot[bot]andhanhnt2 749fe3e5ae fix(server): keep the local speech library path out of agent environments (#5987)
The supervisor prepended the sherpa-onnx platform package directory to the
daemon worker's loader variable (PATH, LD_LIBRARY_PATH, DYLD_LIBRARY_PATH),
so every agent and terminal inherited it. In packaged desktop builds that
directory resolves inside app.asar, and on Windows Git Bash truncates the
PATH it hands to native executables at that entry.

The local speech worker already sets the loader variable on its own
environment when it is forked, so the daemon worker does not need it.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: hanhnt2 <56497031+hanh9898@users.noreply.github.com>
2026-10-03 22:04:28 +02:00

86 lines
3.1 KiB
TypeScript

import { spawn } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { expect, test } from "vitest";
import {
resolveSherpaLoaderEnv,
sherpaLoaderEnvKey,
} from "../src/server/speech/providers/local/sherpa/sherpa-runtime-env.js";
const loaderKey = sherpaLoaderEnvKey();
const loaderEnv = resolveSherpaLoaderEnv();
// Agents and terminals inherit the daemon worker's environment, so the local speech
// runtime's library directory must stay scoped to the speech worker.
test.runIf(loaderKey && loaderEnv)(
"daemon worker environment does not carry the local speech library directory",
async () => {
const root = await mkdtemp(path.join(tmpdir(), "paseo-supervisor-env-"));
const reportPath = path.join(root, "worker-env.json");
const preloadPath = path.join(root, "report-worker-env.mjs");
await writeFile(
preloadPath,
`
import { writeFileSync } from "node:fs";
if (/daemon-worker\\.(ts|js)$/.test(process.argv[1] ?? "")) {
writeFileSync(${JSON.stringify(reportPath)}, JSON.stringify({ value: process.env[${JSON.stringify(loaderKey)}] ?? "" }));
process.exit(0);
}
`,
);
const env = Object.fromEntries(
Object.entries(process.env).filter(([key]) => !key.startsWith("PASEO_")),
);
// Start from a parent environment that does not already list the speech library directory.
const envLoaderKey =
Object.keys(env).find((key) => key.toLowerCase() === loaderKey!.toLowerCase()) ?? loaderKey!;
const parentValue = (env[envLoaderKey] ?? "")
.split(path.delimiter)
.filter((entry) => entry && entry !== loaderEnv!.libDir)
.join(path.delimiter);
env[envLoaderKey] = parentValue;
const child = spawn(
process.execPath,
["--import", "tsx", fileURLToPath(new URL("./supervisor-entrypoint.ts", import.meta.url))],
{
cwd: fileURLToPath(new URL("../../../", import.meta.url)),
env: {
...env,
HOME: root,
USERPROFILE: root,
PASEO_HOME: path.join(root, "home"),
NODE_OPTIONS: `--import=${pathToFileURL(preloadPath).href}`,
},
stdio: ["ignore", "pipe", "pipe"],
},
);
let output = "";
child.stdout.on("data", (chunk) => {
output += chunk;
});
child.stderr.on("data", (chunk) => {
output += chunk;
});
try {
await expect
.poll(async () => readFile(reportPath, "utf8").catch(() => null), {
timeout: 20_000,
message: output,
})
.not.toBeNull();
const report = JSON.parse(await readFile(reportPath, "utf8")) as { value: string };
expect(report.value).toBe(parentValue);
} finally {
if (child.exitCode === null) {
const closed = new Promise((resolve) => child.once("close", resolve));
child.kill("SIGKILL");
await closed;
}
await rm(root, { recursive: true, force: true });
}
},
30_000,
);