test / verify-shell.js
// Integration test for the shell wrapper.
//
// Drives src/shell.ts (via the compiled dist/) under a stripped PATH that
// mimics LM Studio's plugin runtime. Validates shell discovery, cwd handling,
// truncation, and timeout behavior.
//
// Run: npm run test:verify
"use strict";
// Strip the inherited shell PATH so we exercise the wrapper's discovery logic
// the same way LM Studio's plugin runtime does.
process.env.PATH = "/usr/bin:/bin";
const path = require("path");
const os = require("os");
const { runShell, describeEnvironment, formatRunResult } = require(
path.join(__dirname, "..", "dist", "shell"),
);
const SETTINGS = {
shell: "",
loginShell: true,
defaultCwd: "",
timeoutMs: 10000,
maxOutputBytes: 256,
};
function bar(label) {
console.log("\n" + "=".repeat(8) + " " + label + " " + "=".repeat(8));
}
function assert(cond, msg) {
if (!cond) {
console.log("FAIL:", msg);
return false;
}
return true;
}
async function main() {
let failed = false;
const t0 = Date.now();
bar("environment");
const info = describeEnvironment(SETTINGS);
console.log(info);
if (!assert(typeof info.shell === "string" && info.shell.length > 0, "shell resolved")) failed = true;
if (!assert(info.user.length > 0, "user populated")) failed = true;
bar("echo hello");
const echo = await runShell(SETTINGS, "echo hello world");
console.log(formatRunResult(echo, SETTINGS.maxOutputBytes));
if (!assert(echo.exitCode === 0, "echo exit 0")) failed = true;
if (!assert(echo.stdout.trim() === "hello world", "echo stdout matches")) failed = true;
bar("pwd defaults to home");
const pwd = await runShell(SETTINGS, "pwd");
console.log(formatRunResult(pwd, SETTINGS.maxOutputBytes));
if (!assert(pwd.exitCode === 0, "pwd exit 0")) failed = true;
if (!assert(pwd.stdout.trim() === os.homedir(), `pwd is home (${os.homedir()})`)) failed = true;
bar("explicit cwd override");
const tmpPwd = await runShell(SETTINGS, "pwd", { cwd: "/tmp" });
console.log(formatRunResult(tmpPwd, SETTINGS.maxOutputBytes));
// /tmp is symlinked to /private/tmp on macOS; accept either.
const tmpOk = tmpPwd.stdout.trim() === "/tmp" || tmpPwd.stdout.trim() === "/private/tmp";
if (!assert(tmpOk, "pwd reflects cwd override")) failed = true;
bar("nonzero exit");
const fail = await runShell(SETTINGS, "false");
console.log(formatRunResult(fail, SETTINGS.maxOutputBytes));
if (!assert(fail.exitCode === 1, "false returns exit 1")) failed = true;
bar("env override");
const envOut = await runShell(SETTINGS, "echo $LMS_TEST_VAR", {
env: { LMS_TEST_VAR: "verified" },
});
console.log(formatRunResult(envOut, SETTINGS.maxOutputBytes));
if (!assert(envOut.stdout.trim() === "verified", "env var is set in child")) failed = true;
bar("stderr capture");
const stderr = await runShell(SETTINGS, "echo to-stderr 1>&2; exit 3");
console.log(formatRunResult(stderr, SETTINGS.maxOutputBytes));
if (!assert(stderr.exitCode === 3, "exit code propagated")) failed = true;
if (!assert(stderr.stderr.includes("to-stderr"), "stderr captured")) failed = true;
bar("output truncation");
// Produce ~2KB of output, but maxOutputBytes is 256 — should truncate.
const big = await runShell(SETTINGS, "printf 'x%.0s' {1..2000}");
if (!assert(big.truncated.stdout, "stdout marked truncated")) failed = true;
if (!assert(big.stdout.length <= SETTINGS.maxOutputBytes, "stdout capped at maxOutputBytes")) failed = true;
console.log("captured length:", big.stdout.length, "truncated:", big.truncated.stdout);
bar("pipes and command chaining");
const piped = await runShell(SETTINGS, "printf 'a\\nb\\nc\\n' | wc -l | tr -d ' '");
console.log(formatRunResult(piped, SETTINGS.maxOutputBytes));
if (!assert(piped.exitCode === 0, "pipeline exit 0")) failed = true;
if (!assert(piped.stdout.trim() === "3", "pipeline output correct")) failed = true;
bar("timeout kills runaway");
const timed = await runShell(
{ ...SETTINGS, timeoutMs: 500 },
"sleep 5 && echo should-not-print",
);
console.log(formatRunResult(timed, SETTINGS.maxOutputBytes));
if (!assert(timed.timedOut === true, "timedOut flag set")) failed = true;
if (!assert(!timed.stdout.includes("should-not-print"), "command did not finish")) failed = true;
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
bar(failed ? `FAIL (${elapsed}s)` : `PASS (${elapsed}s)`);
process.exit(failed ? 1 : 0);
}
main().catch((err) => {
console.error("THREW:", err);
process.exit(1);
});