mirror of
https://github.com/penpot/penpot.git
synced 2026-09-28 06:46:14 +00:00
The unauthenticated REPL server started in --multi-user mode whenever DEVENV or REPL_ENABLE was set, unlike developer tools which are already blocked there. Gate its creation behind shouldStartReplServer, mirroring the developer tools policy, so it never starts with --multi-user. Give it a dedicated PENPOT_MCP_REPL_HOST knob defaulting to localhost so it no longer inherits the main server 0.0.0.0 bind in Docker, keep the devenv script on 0.0.0.0, and document and test the new behavior. Closes #11631 AI-assisted-by: muse-spark-1.3-contributor
259 lines
9.8 KiB
TypeScript
259 lines
9.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { PenpotMcpServer, shouldRegisterDeveloperTools, shouldStartReplServer } from "./PenpotMcpServer";
|
|
|
|
test("registers developer tools in local devenv mode", () => {
|
|
assert.equal(shouldRegisterDeveloperTools(true, false), true);
|
|
});
|
|
|
|
test("does not register developer tools in multi-user devenv mode", () => {
|
|
assert.equal(shouldRegisterDeveloperTools(true, true), false);
|
|
});
|
|
|
|
test("does not register developer tools when devenv mode is disabled", () => {
|
|
assert.equal(shouldRegisterDeveloperTools(false, false), false);
|
|
});
|
|
|
|
test("starts REPL server in single-user mode when enabled", () => {
|
|
assert.equal(shouldStartReplServer(true, false), true);
|
|
});
|
|
|
|
test("does not start REPL server in multi-user mode even when enabled", () => {
|
|
assert.equal(shouldStartReplServer(true, true), false);
|
|
});
|
|
|
|
test("does not start REPL server when disabled in single-user mode", () => {
|
|
assert.equal(shouldStartReplServer(false, false), false);
|
|
});
|
|
|
|
test("does not start REPL server when disabled in multi-user mode", () => {
|
|
assert.equal(shouldStartReplServer(false, true), false);
|
|
});
|
|
|
|
// ── Pure function tests ────────────────────────────────────────
|
|
|
|
test("isDevEnvEnabled returns false when PENPOT_MCP_DEVENV is not set", () => {
|
|
assert.equal(PenpotMcpServer.isDevEnvEnabled({}), false);
|
|
});
|
|
|
|
test("isDevEnvEnabled returns false when PENPOT_MCP_DEVENV is 'false'", () => {
|
|
assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "false" }), false);
|
|
});
|
|
|
|
test("isDevEnvEnabled returns true when PENPOT_MCP_DEVENV is 'true'", () => {
|
|
assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
|
|
});
|
|
|
|
// ── Pure function tests: isReplEnabled ──────────────────────────
|
|
|
|
test("isReplEnabled returns false when neither env var is set", () => {
|
|
assert.equal(PenpotMcpServer.isReplEnabled({}), false);
|
|
});
|
|
|
|
test("isReplEnabled returns true when PENPOT_MCP_DEVENV is 'true' (fallback)", () => {
|
|
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
|
|
});
|
|
|
|
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true'", () => {
|
|
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
|
|
});
|
|
|
|
test("isReplEnabled returns false when PENPOT_MCP_REPL_ENABLE is 'false' even if DEVENV is true", () => {
|
|
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "false", PENPOT_MCP_DEVENV: "true" }), false);
|
|
});
|
|
|
|
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true' regardless of DEVENV", () => {
|
|
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
|
|
});
|
|
|
|
// ── Integration tests: constructor gating ──────────────────────
|
|
//
|
|
// Each test uses unique ports to avoid conflicts when tests run
|
|
// in the same process. The server is stopped in the finally block
|
|
// to release the WebSocket port.
|
|
|
|
let portCounter = 14_500;
|
|
function uniquePorts() {
|
|
const base = portCounter;
|
|
portCounter += 10;
|
|
return { server: base, ws: base + 1, repl: base + 2 };
|
|
}
|
|
|
|
test("constructor does not create ReplServer when PENPOT_MCP_DEVENV is unset", async () => {
|
|
const prev = process.env.PENPOT_MCP_DEVENV;
|
|
const prevPorts = setUniqueEnv();
|
|
delete process.env.PENPOT_MCP_DEVENV;
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), false);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prev, prevPorts);
|
|
}
|
|
});
|
|
|
|
test("constructor creates ReplServer when PENPOT_MCP_DEVENV is 'true'", async () => {
|
|
const prev = process.env.PENPOT_MCP_DEVENV;
|
|
const prevPorts = setUniqueEnv();
|
|
process.env.PENPOT_MCP_DEVENV = "true";
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), true);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prev, prevPorts);
|
|
}
|
|
});
|
|
|
|
test("constructor creates ReplServer when PENPOT_MCP_REPL_ENABLE is 'true' without DEVENV", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
|
const prevPorts = setUniqueEnv();
|
|
delete process.env.PENPOT_MCP_DEVENV;
|
|
process.env.PENPOT_MCP_REPL_ENABLE = "true";
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), true);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
|
}
|
|
});
|
|
|
|
test("constructor does not create ReplServer when PENPOT_MCP_REPL_ENABLE is 'false' even with DEVENV", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
|
const prevPorts = setUniqueEnv();
|
|
process.env.PENPOT_MCP_DEVENV = "true";
|
|
process.env.PENPOT_MCP_REPL_ENABLE = "false";
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), false);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
|
}
|
|
});
|
|
|
|
test("constructor does not create ReplServer in multi-user mode even with DEVENV", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
|
const prevPorts = setUniqueEnv();
|
|
process.env.PENPOT_MCP_DEVENV = "true";
|
|
delete process.env.PENPOT_MCP_REPL_ENABLE;
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(true);
|
|
assert.equal(server.hasReplServer(), false);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
|
}
|
|
});
|
|
|
|
test("constructor does not create ReplServer in multi-user mode even with explicit REPL_ENABLE", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
|
|
const prevPorts = setUniqueEnv();
|
|
delete process.env.PENPOT_MCP_DEVENV;
|
|
process.env.PENPOT_MCP_REPL_ENABLE = "true";
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(true);
|
|
assert.equal(server.hasReplServer(), false);
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
|
|
}
|
|
});
|
|
|
|
test("replHost defaults to localhost and ignores SERVER_HOST", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevServerHost = process.env.PENPOT_MCP_SERVER_HOST;
|
|
const prevReplHost = process.env.PENPOT_MCP_REPL_HOST;
|
|
const prevPorts = setUniqueEnv();
|
|
process.env.PENPOT_MCP_DEVENV = "true";
|
|
process.env.PENPOT_MCP_SERVER_HOST = "0.0.0.0";
|
|
delete process.env.PENPOT_MCP_REPL_HOST;
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), true);
|
|
assert.equal(server.replHost, "localhost");
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_SERVER_HOST", prevServerHost);
|
|
restoreOrDelete("PENPOT_MCP_REPL_HOST", prevReplHost);
|
|
}
|
|
});
|
|
|
|
test("replHost respects PENPOT_MCP_REPL_HOST", async () => {
|
|
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
|
|
const prevReplHost = process.env.PENPOT_MCP_REPL_HOST;
|
|
const prevPorts = setUniqueEnv();
|
|
process.env.PENPOT_MCP_DEVENV = "true";
|
|
process.env.PENPOT_MCP_REPL_HOST = "0.0.0.0";
|
|
let server: PenpotMcpServer | undefined;
|
|
try {
|
|
server = new PenpotMcpServer(false);
|
|
assert.equal(server.hasReplServer(), true);
|
|
assert.equal(server.replHost, "0.0.0.0");
|
|
} finally {
|
|
await server?.stop();
|
|
restoreEnv(prevDevEnv, prevPorts);
|
|
restoreOrDelete("PENPOT_MCP_REPL_HOST", prevReplHost);
|
|
}
|
|
});
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────
|
|
|
|
function setUniqueEnv() {
|
|
const ports = uniquePorts();
|
|
const prevServer = process.env.PENPOT_MCP_SERVER_PORT;
|
|
const prevWs = process.env.PENPOT_MCP_WEBSOCKET_PORT;
|
|
const prevRepl = process.env.PENPOT_MCP_REPL_PORT;
|
|
const prevReplHost = process.env.PENPOT_MCP_REPL_HOST;
|
|
process.env.PENPOT_MCP_SERVER_PORT = String(ports.server);
|
|
process.env.PENPOT_MCP_WEBSOCKET_PORT = String(ports.ws);
|
|
process.env.PENPOT_MCP_REPL_PORT = String(ports.repl);
|
|
delete process.env.PENPOT_MCP_REPL_HOST;
|
|
return { prevServer, prevWs, prevRepl, prevReplHost };
|
|
}
|
|
|
|
function restoreEnv(
|
|
devEnv: string | undefined,
|
|
ports: {
|
|
prevServer: string | undefined;
|
|
prevWs: string | undefined;
|
|
prevRepl: string | undefined;
|
|
prevReplHost: string | undefined;
|
|
}
|
|
) {
|
|
if (devEnv !== undefined) {
|
|
process.env.PENPOT_MCP_DEVENV = devEnv;
|
|
} else {
|
|
delete process.env.PENPOT_MCP_DEVENV;
|
|
}
|
|
restoreOrDelete("PENPOT_MCP_SERVER_PORT", ports.prevServer);
|
|
restoreOrDelete("PENPOT_MCP_WEBSOCKET_PORT", ports.prevWs);
|
|
restoreOrDelete("PENPOT_MCP_REPL_PORT", ports.prevRepl);
|
|
restoreOrDelete("PENPOT_MCP_REPL_HOST", ports.prevReplHost);
|
|
}
|
|
|
|
function restoreOrDelete(key: string, value: string | undefined) {
|
|
if (value !== undefined) {
|
|
process.env[key] = value;
|
|
} else {
|
|
delete process.env[key];
|
|
}
|
|
}
|