Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

test/fixture/env.ts

Raw
// Sandboxed-environment helpers for tests, owned by the repository.
//
// Ported from the removed `@marcfargas/pi-test-harness` so tests that only need
// a deterministic HOME/XDG sandbox (not a full Pi session) can swap their import
// without behavior change.
import path from "node:path";

export interface EnvOptions {
	/** Explicit env additions. Undefined deletes the key. */
	env?: Record<string, string | undefined>;
	/** Ambient env keys to copy intentionally. */
	inheritEnv?: string[];
}

const BASE_ENV_KEYS = [
	"PATH",
	"PATHEXT",
	"SystemRoot",
	"WINDIR",
	"COMSPEC",
	"TMPDIR",
	"TMP",
	"TEMP",
	"SHELL",
	"TERM",
	"COLORTERM",
	"NO_COLOR",
	"FORCE_COLOR",
	"CI",
	"VITEST",
	"VITEST_POOL_ID",
	"VITEST_WORKER_ID",
	"NODE_OPTIONS",
];

export function sandboxEnv(
	root: string,
	options: EnvOptions = {},
): NodeJS.ProcessEnv {
	const env: NodeJS.ProcessEnv = {};
	for (const key of BASE_ENV_KEYS) copyIfSet(env, process.env, key);
	for (const key of options.inheritEnv ?? []) copyIfSet(env, process.env, key);

	env["HOME"] = path.join(root, ".test-home");
	env["USERPROFILE"] = env["HOME"];
	env["XDG_CONFIG_HOME"] = path.join(root, ".test-config");
	env["XDG_CACHE_HOME"] = path.join(root, ".test-cache");
	env["XDG_STATE_HOME"] = path.join(root, ".test-state");
	env["XDG_DATA_HOME"] = path.join(root, ".test-data");
	// Native Windows consumers do not consult HOME or XDG_*.
	env["APPDATA"] = env["XDG_CONFIG_HOME"];
	env["LOCALAPPDATA"] = env["XDG_STATE_HOME"];

	for (const [key, value] of Object.entries(options.env ?? {}))
		setEnvValue(env, key, value);
	return env;
}

export async function withProcessEnv<T>(
	env: NodeJS.ProcessEnv,
	fn: () => T | Promise<T>,
): Promise<T> {
	const original = { ...process.env };
	replaceProcessEnv({ ...env });
	try {
		return await fn();
	} finally {
		replaceProcessEnv(original);
	}
}

export function withProcessEnvSync<T>(env: NodeJS.ProcessEnv, fn: () => T): T {
	const original = { ...process.env };
	replaceProcessEnv({ ...env });
	try {
		return fn();
	} finally {
		replaceProcessEnv(original);
	}
}

function replaceProcessEnv(env: NodeJS.ProcessEnv): void {
	for (const key of Object.keys(process.env)) delete process.env[key];
	Object.assign(process.env, env);
}

function copyIfSet(
	to: NodeJS.ProcessEnv,
	from: NodeJS.ProcessEnv,
	key: string,
): void {
	const sourceKey =
		process.platform === "win32"
			? Object.keys(from).find(
					(candidate) => candidate.toLowerCase() === key.toLowerCase(),
				)
			: key;
	if (!sourceKey) return;
	setEnvValue(to, sourceKey, from[sourceKey]);
}

function setEnvValue(
	env: NodeJS.ProcessEnv,
	key: string,
	value: string | undefined,
): void {
	if (process.platform === "win32") {
		for (const candidate of Object.keys(env))
			if (candidate.toLowerCase() === key.toLowerCase()) delete env[candidate];
	} else {
		delete env[key];
	}
	if (value !== undefined) env[key] = value;
}