tests / toolsProvider.test.cjs
"use strict";
/**
* Integration tests for the LM Studio toolsProvider and config schematics.
*
* Run with: node --test tests/toolsProvider.test.cjs
*/
const { test } = require("node:test");
const { strict: assert } = require("node:assert");
const { readFileSync } = require("fs");
const { resolve } = require("path");
// Load the toolsProvider (it will be compiled to CJS)
const { toolsProvider, configSchematics, globalConfigSchematics } = require("../dist/index.js");
// Original qemu-mcp-server tool definitions (from https://github.com/nagy/qemu-mcp-server)
const ORIGINAL_TOOLS = {
execute_qmp: {
name: "execute_qmp",
params: ["qmp_command", "qmp_arguments"],
},
read_serial: {
name: "read_serial",
params: ["wait_for", "timeout_ms", "max_chars", "clear"],
},
write_serial: {
name: "write_serial",
params: ["data", "newline"],
},
};
// ──────────────────────────────────────────────────────────────────────────
// toolsProvider tests
// ──────────────────────────────────────────────────────────────────────────
test("toolsProvider returns a function", (t) => {
assert.strictEqual(typeof toolsProvider, "function", "toolsProvider should be a function");
});
test("toolsProvider returns exactly 3 tools", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
assert.ok(Array.isArray(tools), "tools should be an array");
assert.strictEqual(tools.length, 3, "should return exactly 3 tools");
});
test("toolsProvider tool names match original qemu-mcp-server", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const names = tools.map((t) => t.name);
for (const expectedName of Object.keys(ORIGINAL_TOOLS)) {
assert.ok(names.includes(expectedName), `tool '${expectedName}' should be present`);
}
});
test("each tool has required properties (name, description, parametersSchema, implementation)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
for (const tool of tools) {
assert.ok(typeof tool.name === "string", `tool ${tool.name} should have a name`);
assert.ok(typeof tool.description === "string", `tool ${tool.name} should have a description`);
assert.ok(typeof tool.parametersSchema === "object", `tool ${tool.name} should have parametersSchema`);
assert.ok(typeof tool.implementation === "function", `tool ${tool.name} should have an implementation`);
}
});
test("execute_qmp tool parameters match original (qmp_command, qmp_arguments)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const execQmp = tools.find((t) => t.name === "execute_qmp");
assert.ok(execQmp, "execute_qmp tool should exist");
// The parametersSchema is a zod schema; check the parameter names via the schema
const schema = execQmp.parametersSchema;
// zod v3 object schemas have _def.specs or we can use the shape
assert.ok(schema, "should have a parametersSchema");
});
test("read_serial tool parameters match original (wait_for, timeout_ms, max_chars, clear)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const readSerial = tools.find((t) => t.name === "read_serial");
assert.ok(readSerial, "read_serial tool should exist");
assert.ok(readSerial.parametersSchema, "should have a parametersSchema");
});
test("write_serial tool parameters match original (data, newline)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const writeSerial = tools.find((t) => t.name === "write_serial");
assert.ok(writeSerial, "write_serial tool should exist");
assert.ok(writeSerial.parametersSchema, "should have a parametersSchema");
});
test("tool descriptions are non-empty and in English", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
for (const tool of tools) {
assert.ok(tool.description.length > 0, `tool ${tool.name} description should be non-empty`);
// Basic English check: description contains common English words
const descWords = tool.description.toLowerCase().split(/\s+/);
const hasEnglishWord = descWords.some((w) => /^[a-z]{3,}$/.test(w));
assert.ok(hasEnglishWord, `tool ${tool.name} description should contain English words`);
}
});
// ──────────────────────────────────────────────────────────────────────────
// Config schematics tests
// ──────────────────────────────────────────────────────────────────────────
test("configSchematics is defined", (t) => {
assert.ok(configSchematics !== undefined, "configSchematics should be defined");
});
test("globalConfigSchematics has qmpSocketPath and serialSocketPath fields", (t) => {
assert.ok(globalConfigSchematics !== undefined, "globalConfigSchematics should be defined");
// The fields are stored in a Map; check the field keys
const fieldKeys = Array.from(globalConfigSchematics.fields.keys());
assert.ok(fieldKeys.includes("qmpSocketPath"), "should have qmpSocketPath field");
assert.ok(fieldKeys.includes("serialSocketPath"), "should have serialSocketPath field");
});
// ──────────────────────────────────────────────────────────────────────────
// manifest.json and package.json validation tests
// ──────────────────────────────────────────────────────────────────────────
test("manifest.json name is kebab-case", (t) => {
const manifest = require("../manifest.json");
assert.strictEqual(manifest.name, "qemu-mcp", "manifest name should be 'qemu-mcp'");
// kebab-case check: only lowercase letters, digits, and hyphens; no uppercase
assert.ok(
/^[\w-]+$/.test(manifest.name) && manifest.name === manifest.name.toLowerCase(),
"name should be kebab-case"
);
});
test("package.json has all required fields", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.name, "should have name");
assert.ok(pkg.version, "should have version");
assert.ok(pkg.main, "should have main");
assert.ok(pkg.types, "should have types");
assert.ok(pkg.scripts, "should have scripts");
assert.ok(pkg.dependencies, "should have dependencies");
assert.ok(pkg.devDependencies, "should have devDependencies");
assert.ok(pkg.files, "should have files");
assert.strictEqual(pkg.type, "commonjs", "type should be commonjs for CJS output");
});
test("package.json has postinstall script", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.scripts.postinstall, "should have postinstall script");
assert.strictEqual(
pkg.scripts.postinstall,
"node scripts/setup.cjs",
"postinstall should run scripts/setup.cjs"
);
});
test("package.json includes zod dependency", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.dependencies.zod, "should have zod dependency");
});
test("package.json includes @lmstudio/sdk dependency", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.dependencies["@lmstudio/sdk"], "should have @lmstudio/sdk dependency");
});
// ──────────────────────────────────────────────────────────────────────────
// No sensitive data tests
// ──────────────────────────────────────────────────────────────────────────
test("no API keys or passwords in source code", (t) => {
const srcFiles = [
"../src/toolsProvider.ts",
"../src/core/qemuMcpClient.ts",
"../src/config.ts",
"../src/index.ts",
"../scripts/setup.cjs",
];
const sensitivePatterns = [
/api[_-]?key/i,
/password/i,
/secret/i,
/token/i,
/Bearer\s+[A-Za-z0-9\-._~+\/]+/,
];
for (const relPath of srcFiles) {
const absPath = resolve(__dirname, relPath);
const content = readFileSync(absPath, "utf8");
// Check for hardcoded API keys (not just variable names)
for (const pattern of sensitivePatterns) {
const matches = content.match(pattern);
if (matches) {
// Allow variable names that are just "key" or "token" without a value
const matchStr = matches[0];
if (matchStr.length > 20) {
assert.fail(`${relPath} contains sensitive data pattern: ${matchStr}`);
}
}
}
}
});
tests / toolsProvider.test.cjs
"use strict";
/**
* Integration tests for the LM Studio toolsProvider and config schematics.
*
* Run with: node --test tests/toolsProvider.test.cjs
*/
const { test } = require("node:test");
const { strict: assert } = require("node:assert");
const { readFileSync } = require("fs");
const { resolve } = require("path");
// Load the toolsProvider (it will be compiled to CJS)
const { toolsProvider, configSchematics, globalConfigSchematics } = require("../dist/index.js");
// Original qemu-mcp-server tool definitions (from https://github.com/nagy/qemu-mcp-server)
const ORIGINAL_TOOLS = {
execute_qmp: {
name: "execute_qmp",
params: ["qmp_command", "qmp_arguments"],
},
read_serial: {
name: "read_serial",
params: ["wait_for", "timeout_ms", "max_chars", "clear"],
},
write_serial: {
name: "write_serial",
params: ["data", "newline"],
},
};
// ──────────────────────────────────────────────────────────────────────────
// toolsProvider tests
// ──────────────────────────────────────────────────────────────────────────
test("toolsProvider returns a function", (t) => {
assert.strictEqual(typeof toolsProvider, "function", "toolsProvider should be a function");
});
test("toolsProvider returns exactly 3 tools", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
assert.ok(Array.isArray(tools), "tools should be an array");
assert.strictEqual(tools.length, 3, "should return exactly 3 tools");
});
test("toolsProvider tool names match original qemu-mcp-server", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const names = tools.map((t) => t.name);
for (const expectedName of Object.keys(ORIGINAL_TOOLS)) {
assert.ok(names.includes(expectedName), `tool '${expectedName}' should be present`);
}
});
test("each tool has required properties (name, description, parametersSchema, implementation)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
for (const tool of tools) {
assert.ok(typeof tool.name === "string", `tool ${tool.name} should have a name`);
assert.ok(typeof tool.description === "string", `tool ${tool.name} should have a description`);
assert.ok(typeof tool.parametersSchema === "object", `tool ${tool.name} should have parametersSchema`);
assert.ok(typeof tool.implementation === "function", `tool ${tool.name} should have an implementation`);
}
});
test("execute_qmp tool parameters match original (qmp_command, qmp_arguments)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const execQmp = tools.find((t) => t.name === "execute_qmp");
assert.ok(execQmp, "execute_qmp tool should exist");
// The parametersSchema is a zod schema; check the parameter names via the schema
const schema = execQmp.parametersSchema;
// zod v3 object schemas have _def.specs or we can use the shape
assert.ok(schema, "should have a parametersSchema");
});
test("read_serial tool parameters match original (wait_for, timeout_ms, max_chars, clear)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const readSerial = tools.find((t) => t.name === "read_serial");
assert.ok(readSerial, "read_serial tool should exist");
assert.ok(readSerial.parametersSchema, "should have a parametersSchema");
});
test("write_serial tool parameters match original (data, newline)", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
const writeSerial = tools.find((t) => t.name === "write_serial");
assert.ok(writeSerial, "write_serial tool should exist");
assert.ok(writeSerial.parametersSchema, "should have a parametersSchema");
});
test("tool descriptions are non-empty and in English", async (t) => {
const mockCtl = {
getPluginConfig: () => ({
get: (key) => {
const defaults = {
qmpSocketPath: "/tmp/qmp-sock",
serialSocketPath: "/tmp/serial-sock",
};
return defaults[key];
},
}),
};
const tools = await toolsProvider(mockCtl);
for (const tool of tools) {
assert.ok(tool.description.length > 0, `tool ${tool.name} description should be non-empty`);
// Basic English check: description contains common English words
const descWords = tool.description.toLowerCase().split(/\s+/);
const hasEnglishWord = descWords.some((w) => /^[a-z]{3,}$/.test(w));
assert.ok(hasEnglishWord, `tool ${tool.name} description should contain English words`);
}
});
// ──────────────────────────────────────────────────────────────────────────
// Config schematics tests
// ──────────────────────────────────────────────────────────────────────────
test("configSchematics is defined", (t) => {
assert.ok(configSchematics !== undefined, "configSchematics should be defined");
});
test("globalConfigSchematics has qmpSocketPath and serialSocketPath fields", (t) => {
assert.ok(globalConfigSchematics !== undefined, "globalConfigSchematics should be defined");
// The fields are stored in a Map; check the field keys
const fieldKeys = Array.from(globalConfigSchematics.fields.keys());
assert.ok(fieldKeys.includes("qmpSocketPath"), "should have qmpSocketPath field");
assert.ok(fieldKeys.includes("serialSocketPath"), "should have serialSocketPath field");
});
// ──────────────────────────────────────────────────────────────────────────
// manifest.json and package.json validation tests
// ──────────────────────────────────────────────────────────────────────────
test("manifest.json name is kebab-case", (t) => {
const manifest = require("../manifest.json");
assert.strictEqual(manifest.name, "qemu-mcp", "manifest name should be 'qemu-mcp'");
// kebab-case check: only lowercase letters, digits, and hyphens; no uppercase
assert.ok(
/^[\w-]+$/.test(manifest.name) && manifest.name === manifest.name.toLowerCase(),
"name should be kebab-case"
);
});
test("package.json has all required fields", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.name, "should have name");
assert.ok(pkg.version, "should have version");
assert.ok(pkg.main, "should have main");
assert.ok(pkg.types, "should have types");
assert.ok(pkg.scripts, "should have scripts");
assert.ok(pkg.dependencies, "should have dependencies");
assert.ok(pkg.devDependencies, "should have devDependencies");
assert.ok(pkg.files, "should have files");
assert.strictEqual(pkg.type, "commonjs", "type should be commonjs for CJS output");
});
test("package.json has postinstall script", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.scripts.postinstall, "should have postinstall script");
assert.strictEqual(
pkg.scripts.postinstall,
"node scripts/setup.cjs",
"postinstall should run scripts/setup.cjs"
);
});
test("package.json includes zod dependency", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.dependencies.zod, "should have zod dependency");
});
test("package.json includes @lmstudio/sdk dependency", (t) => {
const pkg = require("../package.json");
assert.ok(pkg.dependencies["@lmstudio/sdk"], "should have @lmstudio/sdk dependency");
});
// ──────────────────────────────────────────────────────────────────────────
// No sensitive data tests
// ──────────────────────────────────────────────────────────────────────────
test("no API keys or passwords in source code", (t) => {
const srcFiles = [
"../src/toolsProvider.ts",
"../src/core/qemuMcpClient.ts",
"../src/config.ts",
"../src/index.ts",
"../scripts/setup.cjs",
];
const sensitivePatterns = [
/api[_-]?key/i,
/password/i,
/secret/i,
/token/i,
/Bearer\s+[A-Za-z0-9\-._~+\/]+/,
];
for (const relPath of srcFiles) {
const absPath = resolve(__dirname, relPath);
const content = readFileSync(absPath, "utf8");
// Check for hardcoded API keys (not just variable names)
for (const pattern of sensitivePatterns) {
const matches = content.match(pattern);
if (matches) {
// Allow variable names that are just "key" or "token" without a value
const matchStr = matches[0];
if (matchStr.length > 20) {
assert.fail(`${relPath} contains sensitive data pattern: ${matchStr}`);
}
}
}
}
});