tests / testHelper.cjs
"use strict";
/**
* Test helper that manages swapping the real qemu-mcp-server binary with a
* fake MCP server for unit testing.
*
* Usage:
* const { withFakeBinary, restoreBinary } = require("./testHelper");
* // or use the async generator pattern:
* for await (const _ of withFakeBinary(async () => { ... })) { ... }
*/
const { spawnSync, execSync } = require("child_process");
const { existsSync, renameSync, readFileSync, writeFileSync, chmodSync } = require("fs");
const { join, resolve } = require("path");
const BIN_DIR = resolve(__dirname, "..", "qemu-mcp-server", "target", "release");
const BIN_PATH = resolve(BIN_DIR, "qemu-mcp-server");
const BACKUP_PATH = resolve(BIN_DIR, "qemu-mcp-server.real.backup");
const FAKE_SERVER = resolve(__dirname, "fakeMcpServer.cjs");
const WRAPPER_PATH = resolve(BIN_DIR, "qemu-mcp-server.test.wrapper");
// ──────────────────────────────────────────────────────────────────────────
// Async mutex to prevent race conditions when multiple tests swap the binary
// simultaneously. This is needed because the binary path is shared across
// all tests, and swapping it is not thread-safe.
// ──────────────────────────────────────────────────────────────────────────
const mutex = {
locked: false,
queue: [],
async lock() {
return new Promise((resolve) => {
if (!this.locked) {
this.locked = true;
resolve();
} else {
this.queue.push(resolve);
}
});
},
unlock() {
this.locked = false;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
},
};
/**
* Create a wrapper script that invokes the fake server with Node.js.
* This mimics how the real binary would be invoked (as an executable).
*/
function createWrapper() {
const script = `#!/bin/sh
node "${FAKE_SERVER}"
`;
writeFileSync(WRAPPER_PATH, script, { mode: 0o755 });
}
/**
* Replace the real binary with the fake server wrapper.
* @returns {boolean} true if the swap was successful
*/
function swapToFake() {
if (!existsSync(BIN_PATH)) {
throw new Error(`Real binary not found at ${BIN_PATH}`);
}
// Back up the real binary
if (!existsSync(BACKUP_PATH)) {
renameSync(BIN_PATH, BACKUP_PATH);
}
// Create the wrapper
createWrapper();
// Replace the binary path with the wrapper
renameSync(WRAPPER_PATH, BIN_PATH);
return true;
}
/**
* Restore the real binary from backup.
*/
function restore() {
if (existsSync(BACKUP_PATH)) {
// If the binary path is currently our wrapper, remove it first
if (existsSync(BIN_PATH) && readFileSync(BIN_PATH, "utf8").startsWith("#!/bin/sh")) {
const wrapperContent = readFileSync(BIN_PATH, "utf8");
// Remove the wrapper file (it's a symlink or file)
try {
require("fs").unlinkSync(BIN_PATH);
} catch {
// Might be a directory or read-only
}
}
renameSync(BACKUP_PATH, BIN_PATH);
// Ensure executable
chmodSync(BIN_PATH, 0o755);
}
}
/**
* Check if the current binary at BIN_PATH is our fake wrapper.
*/
function isFakeActive() {
if (!existsSync(BIN_PATH)) return false;
try {
const content = readFileSync(BIN_PATH, "utf8");
return content.startsWith("#!/bin/sh");
} catch {
return false;
}
}
/**
* Run a test with the fake binary active.
* @param {Function} fn - Test function to run (can be async)
* @param {object} opts - Options
* @param {boolean} opts.restoreAfter - Whether to restore after the test (default: true)
* @returns {Promise<*>} The result of the test function
*/
async function withFakeBinary(fn, opts = {}) {
const restoreAfter = opts.restoreAfter !== false;
await mutex.lock();
try {
const hadBackup = existsSync(BACKUP_PATH);
swapToFake();
const result = await fn();
return result;
} finally {
// Always restore if a backup exists and the current BIN_PATH is our wrapper.
if (restoreAfter && existsSync(BACKUP_PATH) && existsSync(BIN_PATH)) {
restore();
}
mutex.unlock();
}
}
/**
* Run a test without the binary (test binary-not-found behavior).
* @param {Function} fn - Test function to run (can be async)
* @returns {Promise<*>} The result of the test function
*/
async function withoutBinary(fn) {
await mutex.lock();
const hadBackup = existsSync(BACKUP_PATH);
try {
if (existsSync(BIN_PATH)) {
renameSync(BIN_PATH, BACKUP_PATH);
}
const result = await fn();
return result;
} finally {
if (!hadBackup && existsSync(BACKUP_PATH)) {
restore();
}
mutex.unlock();
}
}
module.exports = {
BIN_PATH,
BACKUP_PATH,
FAKE_SERVER,
withFakeBinary,
restore,
isFakeActive,
withoutBinary,
swapToFake,
};
tests / testHelper.cjs
"use strict";
/**
* Test helper that manages swapping the real qemu-mcp-server binary with a
* fake MCP server for unit testing.
*
* Usage:
* const { withFakeBinary, restoreBinary } = require("./testHelper");
* // or use the async generator pattern:
* for await (const _ of withFakeBinary(async () => { ... })) { ... }
*/
const { spawnSync, execSync } = require("child_process");
const { existsSync, renameSync, readFileSync, writeFileSync, chmodSync } = require("fs");
const { join, resolve } = require("path");
const BIN_DIR = resolve(__dirname, "..", "qemu-mcp-server", "target", "release");
const BIN_PATH = resolve(BIN_DIR, "qemu-mcp-server");
const BACKUP_PATH = resolve(BIN_DIR, "qemu-mcp-server.real.backup");
const FAKE_SERVER = resolve(__dirname, "fakeMcpServer.cjs");
const WRAPPER_PATH = resolve(BIN_DIR, "qemu-mcp-server.test.wrapper");
// ──────────────────────────────────────────────────────────────────────────
// Async mutex to prevent race conditions when multiple tests swap the binary
// simultaneously. This is needed because the binary path is shared across
// all tests, and swapping it is not thread-safe.
// ──────────────────────────────────────────────────────────────────────────
const mutex = {
locked: false,
queue: [],
async lock() {
return new Promise((resolve) => {
if (!this.locked) {
this.locked = true;
resolve();
} else {
this.queue.push(resolve);
}
});
},
unlock() {
this.locked = false;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
},
};
/**
* Create a wrapper script that invokes the fake server with Node.js.
* This mimics how the real binary would be invoked (as an executable).
*/
function createWrapper() {
const script = `#!/bin/sh
node "${FAKE_SERVER}"
`;
writeFileSync(WRAPPER_PATH, script, { mode: 0o755 });
}
/**
* Replace the real binary with the fake server wrapper.
* @returns {boolean} true if the swap was successful
*/
function swapToFake() {
if (!existsSync(BIN_PATH)) {
throw new Error(`Real binary not found at ${BIN_PATH}`);
}
// Back up the real binary
if (!existsSync(BACKUP_PATH)) {
renameSync(BIN_PATH, BACKUP_PATH);
}
// Create the wrapper
createWrapper();
// Replace the binary path with the wrapper
renameSync(WRAPPER_PATH, BIN_PATH);
return true;
}
/**
* Restore the real binary from backup.
*/
function restore() {
if (existsSync(BACKUP_PATH)) {
// If the binary path is currently our wrapper, remove it first
if (existsSync(BIN_PATH) && readFileSync(BIN_PATH, "utf8").startsWith("#!/bin/sh")) {
const wrapperContent = readFileSync(BIN_PATH, "utf8");
// Remove the wrapper file (it's a symlink or file)
try {
require("fs").unlinkSync(BIN_PATH);
} catch {
// Might be a directory or read-only
}
}
renameSync(BACKUP_PATH, BIN_PATH);
// Ensure executable
chmodSync(BIN_PATH, 0o755);
}
}
/**
* Check if the current binary at BIN_PATH is our fake wrapper.
*/
function isFakeActive() {
if (!existsSync(BIN_PATH)) return false;
try {
const content = readFileSync(BIN_PATH, "utf8");
return content.startsWith("#!/bin/sh");
} catch {
return false;
}
}
/**
* Run a test with the fake binary active.
* @param {Function} fn - Test function to run (can be async)
* @param {object} opts - Options
* @param {boolean} opts.restoreAfter - Whether to restore after the test (default: true)
* @returns {Promise<*>} The result of the test function
*/
async function withFakeBinary(fn, opts = {}) {
const restoreAfter = opts.restoreAfter !== false;
await mutex.lock();
try {
const hadBackup = existsSync(BACKUP_PATH);
swapToFake();
const result = await fn();
return result;
} finally {
// Always restore if a backup exists and the current BIN_PATH is our wrapper.
if (restoreAfter && existsSync(BACKUP_PATH) && existsSync(BIN_PATH)) {
restore();
}
mutex.unlock();
}
}
/**
* Run a test without the binary (test binary-not-found behavior).
* @param {Function} fn - Test function to run (can be async)
* @returns {Promise<*>} The result of the test function
*/
async function withoutBinary(fn) {
await mutex.lock();
const hadBackup = existsSync(BACKUP_PATH);
try {
if (existsSync(BIN_PATH)) {
renameSync(BIN_PATH, BACKUP_PATH);
}
const result = await fn();
return result;
} finally {
if (!hadBackup && existsSync(BACKUP_PATH)) {
restore();
}
mutex.unlock();
}
}
module.exports = {
BIN_PATH,
BACKUP_PATH,
FAKE_SERVER,
withFakeBinary,
restore,
isFakeActive,
withoutBinary,
swapToFake,
};