dev-mcps/dev-server-mcp/test/dev-server-service.test.js

73 lines
3.9 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { loadDevServerConfig } from "../src/config.js";
import { DevServerService } from "../src/dev-server-service.js";
function fixture(config) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "dev-server-mcp-"));
const configPath = path.join(root, "services.json");
fs.writeFileSync(configPath, JSON.stringify(config));
return { root, configPath, cleanup: () => fs.rmSync(root, { recursive: true, force: true }) };
}
test("configuration rejects commands outside the operator allowlist", () => {
const item = fixture({ services: { web: { command: "sh", args: [], cwd: "." } } });
try {
assert.throws(() => loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: ["node"] }), /not allowlisted/);
} finally { item.cleanup(); }
});
test("configuration rejects workspace traversal", () => {
// Refused on the shape of the path, before anything is resolved, so it holds for a
// per-execution service too - whose directory does not exist yet and so cannot be resolved.
const item = fixture({ services: { web: { command: "node", args: [], cwd: ".." } } });
try {
assert.throws(() => loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: ["node"] }), /Invalid cwd/);
} finally { item.cleanup(); }
});
test("configuration cannot replace the safe process environment", () => {
const item = fixture({ services: { web: { command: "node", args: [], cwd: ".", env: { PATH: "/workspace/bin" } } } });
try {
assert.throws(() => loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: ["node"] }), /Invalid environment/);
} finally { item.cleanup(); }
});
test("list exposes an operator-written setup contract without exposing caller-controlled commands", () => {
const item = fixture({ services: { web: {
command: "node", args: ["server.js", "${port}"], cwd: ".", workspaceMode: "execution",
agentInstructions: "Create server.js, bind 0.0.0.0 and use process.env.PORT."
} } });
try {
const config = loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: ["node"] });
const devServer = new DevServerService({ services: config.services });
const listed = devServer.list({ key: "execution-1" }).services[0];
assert.equal(listed.agentInstructions, "Create server.js, bind 0.0.0.0 and use process.env.PORT.");
assert.equal(Object.hasOwn(listed, "command"), false);
} finally { item.cleanup(); }
});
test("dev server starts only configured arguments, captures logs, and stops the process group", async () => {
const item = fixture({ services: { web: { command: process.execPath, args: ["-e", "console.log('ready'); setInterval(() => {}, 1000)"], cwd: ".", shutdownTimeoutMs: 500 } } });
try {
const config = loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: [process.execPath] });
const devServer = new DevServerService({ services: config.services });
const started = await devServer.start({ service: "web" });
assert.equal(started.status, "running");
await new Promise((resolve) => setTimeout(resolve, 50));
assert.match(devServer.logs({ service: "web" }).logs, /ready/);
const stopped = await devServer.stop({ service: "web" });
assert.equal(stopped.status, "stopped");
await devServer.close();
} finally { item.cleanup(); }
});
test("unknown services and oversized log requests fail closed", () => {
const devServer = new DevServerService({ services: new Map([["web", { id: "web", clientCwd: ".", publicUrl: null }]]) });
assert.throws(() => devServer.status({ service: "missing" }), /Unknown service/);
assert.throws(() => devServer.logs({ service: "web", tailBytes: 999999 }), /tailBytes/);
});