73 lines
3.9 KiB
JavaScript
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/);
|
|
});
|