diff --git a/packages/server/scripts/supervisor-entrypoint.env.test.ts b/packages/server/scripts/supervisor-entrypoint.env.test.ts new file mode 100644 index 0000000000..d87bc678e8 --- /dev/null +++ b/packages/server/scripts/supervisor-entrypoint.env.test.ts @@ -0,0 +1,85 @@ +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, +); diff --git a/packages/server/scripts/supervisor-entrypoint.ts b/packages/server/scripts/supervisor-entrypoint.ts index 6a27b5d2f6..ee3746accf 100644 --- a/packages/server/scripts/supervisor-entrypoint.ts +++ b/packages/server/scripts/supervisor-entrypoint.ts @@ -14,7 +14,6 @@ import { PRIVATE_FILE_MODE } from "../src/server/private-files.js"; import { loadPersistedConfig } from "../src/server/persisted-config.js"; import { runSupervisor } from "./supervisor.js"; import { resolveSupervisorLogFile } from "./supervisor-log-config.js"; -import { applySherpaLoaderEnv } from "../src/server/speech/providers/local/sherpa/sherpa-runtime-env.js"; process.title = "Paseo Supervisor"; @@ -108,8 +107,6 @@ async function main(): Promise { ? resolvePackagedNodeEntrypointRunnerPath(fileURLToPath(import.meta.url)) : null; - applySherpaLoaderEnv(workerEnv); - const paseoHome = resolvePaseoHome(workerEnv); const persistedConfig = loadPersistedConfig(paseoHome); const supervisorLogFile = resolveSupervisorLogFile(paseoHome, persistedConfig, workerEnv);