mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 10:26:14 +00:00
Prevent developer tools from being exposed when the MCP server runs in multi-user mode. Keep them available for local devenv usage and document the mode restriction. Add regression coverage for the registration policy. Closes #11291 AI-assisted-by: gpt-5.6-luna Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
162 lines
6.2 KiB
TypeScript
162 lines
6.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { PenpotMcpServer, shouldRegisterDeveloperTools } 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);
|
|
});
|
|
|
|
// ── 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);
|
|
}
|
|
});
|
|
|
|
// ── 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;
|
|
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);
|
|
return { prevServer, prevWs, prevRepl };
|
|
}
|
|
|
|
function restoreEnv(
|
|
devEnv: string | undefined,
|
|
ports: { prevServer: string | undefined; prevWs: string | undefined; prevRepl: 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);
|
|
}
|
|
|
|
function restoreOrDelete(key: string, value: string | undefined) {
|
|
if (value !== undefined) {
|
|
process.env[key] = value;
|
|
} else {
|
|
delete process.env[key];
|
|
}
|
|
}
|