Files
OpenHands/__tests__/scripts/dev-safe.test.ts
T
Graham Neubigandopenhands 3b754b5703 Improve missing agent-server setup guidance (#42)
Add actionable dev-safe output when agent-server is missing, including README and uv installation guidance, and update tests plus quickstart docs.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-27 10:40:06 -04:00

128 lines
4.0 KiB
TypeScript

import { spawn } from "node:child_process";
import { once } from "node:events";
import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
buildSafeDevConfig,
formatMissingAgentServerGuidance,
} from "../../scripts/dev-safe.mjs";
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../..",
);
describe("formatMissingAgentServerGuidance", () => {
it("includes install, PATH, README, and fallback workflow hints", () => {
const guidance = formatMissingAgentServerGuidance(
"/workspace/project/agent-server-gui",
);
expect(guidance).toContain(
"uv tool install -U --with openhands-tools --with openhands-workspace openhands-agent-server",
);
expect(guidance).toContain('export PATH="$HOME/.local/bin:$PATH"');
expect(guidance).toContain(
"/workspace/project/agent-server-gui/README.md",
);
expect(guidance).toContain(
"https://docs.astral.sh/uv/getting-started/installation/",
);
expect(guidance).toContain("npm run dev:frontend");
expect(guidance).toContain("npm run dev:mock");
});
});
describe("buildSafeDevConfig", () => {
it("builds isolated default paths and ports", () => {
const cwd = "/workspace/project/agent-server-gui";
const config = buildSafeDevConfig(cwd, {});
expect(config.backendPort).toBe(18000);
expect(config.vscodePort).toBe(18001);
expect(config.backendBaseUrl).toBe("http://127.0.0.1:18000");
expect(config.backendHost).toBe("127.0.0.1:18000");
expect(config.workingDir).toBe(cwd);
expect(config.stateDir).toBe(
path.join(cwd, ".openhands-dev", "safe-dev-18000"),
);
expect(config.tmuxTmpDir).toBe(path.join(config.stateDir, "tmux"));
expect(config.conversationsPath).toBe(
path.join(config.stateDir, "conversations"),
);
expect(config.bashEventsDir).toBe(
path.join(config.stateDir, "bash_events"),
);
});
it("honors environment overrides", () => {
const cwd = "/workspace/project/agent-server-gui";
const config = buildSafeDevConfig(cwd, {
OH_GUI_SAFE_BACKEND_PORT: "19000",
OH_GUI_SAFE_VSCODE_PORT: "19010",
OH_GUI_SAFE_STATE_DIR: ".tmp/dev-safe",
VITE_WORKING_DIR: "/workspace/custom-repo",
});
expect(config.backendPort).toBe(19000);
expect(config.vscodePort).toBe(19010);
expect(config.backendBaseUrl).toBe("http://127.0.0.1:19000");
expect(config.backendHost).toBe("127.0.0.1:19000");
expect(config.stateDir).toBe(path.join(cwd, ".tmp", "dev-safe"));
expect(config.workingDir).toBe("/workspace/custom-repo");
});
});
describe("dev-safe CLI startup", () => {
it("exits promptly when agent-server is missing", async () => {
const child = spawn(process.execPath, ["scripts/dev-safe.mjs"], {
cwd: repoRoot,
env: {
...process.env,
PATH: "/usr/bin:/bin",
},
stdio: ["ignore", "pipe", "pipe"],
});
let output = "";
child.stdout.on("data", (chunk) => {
output += chunk.toString();
});
child.stderr.on("data", (chunk) => {
output += chunk.toString();
});
const exitResult = await Promise.race([
once(child, "exit").then(([code, signal]) => ({
code,
signal,
timedOut: false,
})),
delay(4_000).then(() => ({ code: null, signal: null, timedOut: true })),
]);
if (exitResult.timedOut) {
child.kill("SIGKILL");
}
expect(exitResult.timedOut).toBe(false);
expect(exitResult.code).toBe(1);
expect(output).toContain("Failed to start agent-server");
expect(output).toContain(
"uv tool install -U --with openhands-tools --with openhands-workspace openhands-agent-server",
);
expect(output).toContain(
"https://docs.astral.sh/uv/getting-started/installation/",
);
expect(output).toContain("README.md");
expect(output).toContain("npm run dev:mock");
expect(output).toContain("spawn agent-server ENOENT");
});
});