dev-mcps/coding-agent-mcp/test/command-service.test.js

114 lines
5.4 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { CommandService } from "../src/command-service.js";
import { buildTaskCommand } from "../src/execution-policy.js";
async function makeWorkspace() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "command-mcp-"));
return { root, cleanup: () => fs.rm(root, { recursive: true, force: true }) };
}
async function makeNpmProject(root, scripts = {}) {
await fs.writeFile(path.join(root, "package.json"), `${JSON.stringify({ name: "fixture", version: "1.0.0", private: true, scripts: {
test: "node -e \"process.stdout.write('ok')\"",
build: "node -e \"process.stdout.write('build-ok')\"",
lint: "node -e \"process.stderr.write('lint-output')\"",
slow: "node -e \"setTimeout(() => process.stdout.write('late'), 2000)\"",
...scripts
} }, null, 2)}\n`);
}
test("run_task executes a typed npm build", async () => {
const workspace = await makeWorkspace();
try {
await makeNpmProject(workspace.root);
const result = await new CommandService({ roots: [workspace.root] }).runTask({ runner: "npm", task: "build" });
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /build-ok/);
} finally { await workspace.cleanup(); }
});
test("an empty run_task workdir is normalized to the workspace root", async () => {
const workspace = await makeWorkspace();
try {
await makeNpmProject(workspace.root);
const result = await new CommandService({ roots: [workspace.root] }).runTask({ workdir: "", runner: "npm", task: "test" });
assert.equal(result.exitCode, 0);
assert.equal(result.workdir, ".");
} finally { await workspace.cleanup(); }
});
test("run_task scopes execution to the workspace subpath without adding a session directory", async () => {
const workspace = await makeWorkspace();
try {
const project = path.join(workspace.root, "project-a");
await fs.mkdir(project, { recursive: true });
await makeNpmProject(project);
const result = await new CommandService({ roots: [workspace.root] }).runTask({ runner: "npm", task: "test", sessionId: "sess_1", workspaceSubpath: "project-a" });
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /ok/);
} finally { await workspace.cleanup(); }
});
test("typed runners reject unsupported tasks and paths", () => {
assert.throws(() => buildTaskCommand({ runner: "pytest", task: "build" }), /only the test task/);
assert.throws(() => buildTaskCommand({ runner: "npm", task: "test", paths: ["tests"] }), /does not accept paths/);
assert.throws(() => buildTaskCommand({ runner: "shell", task: "test" }), /runner is not supported/);
assert.deepEqual(buildTaskCommand({ runner: "cargo", task: "lint" }), ["cargo", "clippy"]);
});
test("run_task rejects traversal, absolute workdirs, options, and escaped task paths", async () => {
const workspace = await makeWorkspace();
try {
await makeNpmProject(workspace.root);
const service = new CommandService({ roots: [workspace.root] });
await assert.rejects(service.runTask({ workdir: "../outside", runner: "npm", task: "test" }), /relative path|escapes/);
await assert.rejects(service.runTask({ workdir: workspace.root, runner: "npm", task: "test" }), /relative path/);
await assert.rejects(service.runTask({ runner: "pytest", task: "test", paths: ["--rootdir=/tmp"] }), /not options/);
await assert.rejects(service.runTask({ runner: "pytest", task: "test", paths: ["../outside"] }), /outside/);
} finally { await workspace.cleanup(); }
});
test("run_task uses a minimal environment", async () => {
const workspace = await makeWorkspace();
const original = process.env.MCP_TEST_SECRET;
process.env.MCP_TEST_SECRET = "must-not-reach-child";
try {
await makeNpmProject(workspace.root, { test: "node -e \"process.stdout.write(process.env.MCP_TEST_SECRET || 'clean')\"" });
const result = await new CommandService({ roots: [workspace.root] }).runTask({ runner: "npm", task: "test" });
assert.equal(result.stdout.includes("must-not-reach-child"), false);
assert.match(result.stdout, /clean/);
} finally {
if (original === undefined) delete process.env.MCP_TEST_SECRET;
else process.env.MCP_TEST_SECRET = original;
await workspace.cleanup();
}
});
test("run_task enforces timeouts and output limits", async () => {
const workspace = await makeWorkspace();
try {
await makeNpmProject(workspace.root, {
test: "node -e \"process.stdout.write('x'.repeat(5000))\"",
build: "node -e \"setTimeout(() => process.stdout.write('late'), 2000)\""
});
const service = new CommandService({ roots: [workspace.root] });
const large = await service.runTask({ runner: "npm", task: "test", outputByteLimit: 1024 });
assert.equal(large.stdoutTruncated, true);
const slow = await service.runTask({ runner: "npm", task: "build", timeoutSeconds: 1 });
assert.equal(slow.timedOut, true);
} finally { await workspace.cleanup(); }
});
test("disabled execution backend fails closed", async () => {
const workspace = await makeWorkspace();
try {
await makeNpmProject(workspace.root);
const service = new CommandService({ roots: [workspace.root], executionBackend: "disabled" });
await assert.rejects(service.runTask({ runner: "npm", task: "test" }), /disabled/);
} finally { await workspace.cleanup(); }
});