Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5ee7ab5
fix(mcp): address remaining MCP_harden inline review follow-ups
tonythethompson Aug 8, 2026
ffd9d35
fix: Delay malformed lock reclamation
qodo-code-review[bot] Aug 8, 2026
6c6a4d3
fix(mcp): publish smoke lock PID atomically and grace empty reclaim
tonythethompson Aug 8, 2026
a5fdac8
fix(mcp): publish smoke locks via temp file and linkSync
tonythethompson Aug 8, 2026
11aea72
Merge branch 'main' into fix/mcp-harden-inline-followups
tonythethompson Aug 8, 2026
89e08ab
[CodeFactor] Apply fixes
code-factor Aug 8, 2026
3d370b5
fix(mcp): treat partial lock PIDs as incomplete and harden stub tests
tonythethompson Aug 8, 2026
8080cd9
test(mcp): cover smoke lock EPERM reclaim falling through to sleep
tonythethompson Aug 8, 2026
4ae7cf9
test(mcp): reuse makeFs for ENOTSUP smoke lock fallback coverage
tonythethompson Aug 8, 2026
002e739
fix(mcp): compare lock body before reclaim unlink
tonythethompson Aug 8, 2026
70f8a8f
fix(mcp): return stub timeout failures and serialize lock reclaim
tonythethompson Aug 8, 2026
90f515c
fix(mcp): recover orphaned smoke-lock reclaim mutex
tonythethompson Aug 8, 2026
f4a7a8a
fix(mcp): honor stub deadline remaining sleep and drain test teardown
tonythethompson Aug 8, 2026
a80371e
fix(mcp): never age-clear live reclaim mutexes; re-verify gate ownership
tonythethompson Aug 8, 2026
031d14c
fix(mcp): age-clear incomplete orphaned reclaim mutexes after grace
tonythethompson Aug 8, 2026
09a14ac
fix(mcp): publish reclaim gate via temp+link like the main lock
tonythethompson Aug 8, 2026
6a8834c
fix(mcp): publish lock via COPYFILE_EXCL when hardlinks fail
tonythethompson Aug 8, 2026
db80284
fix(mcp): type smoke-lock copyFileSync mock as PathLike
tonythethompson Aug 8, 2026
e0d2d45
fix(mcp): invoke mcporter via node argv on Windows smoke
tonythethompson Aug 8, 2026
1f90b97
Update vite.config.ts
tonythethompson Aug 8, 2026
ada73eb
fix(mcp): publish smoke locks via rename when hardlinks fail
tonythethompson Aug 8, 2026
de08c25
fix(mcp): never POSIX-rename over a live smoke lock
tonythethompson Aug 8, 2026
c9ee116
fix(mcp): harden smoke lock publish and agent-smoke helpers
tonythethompson Aug 8, 2026
a14699d
fix(mcp): never age-reclaim incomplete smoke lock bodies
tonythethompson Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 103 additions & 20 deletions scripts/mcp-agent-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* Studio is started with OLIVE_JOB_SETUP_STUB=1 so submits never download models
* or run Olive (AGENTS.md: no real Olive execute in CI/VM).
*
* Mcporter is installed once into a unique temp prefix (mkdtemp), then invoked
* via `node <cli> …` with `shell: false` so Windows does not strip quotes from
* `--args` JSON (npx+cmd was corrupting submit payloads).
*
* Usage (repo root):
* node scripts/mcp-agent-smoke.mjs
* pnpm mcp:agent-smoke
Expand Down Expand Up @@ -52,6 +56,100 @@ const STUDIO_CONFIG_SMOKE_LOCK = path.join(
);
const STRICT = process.env.MCP_SMOKE_STRICT === "1";

/** Resolved once: `node <cli> …` keeps argv intact on Windows (no cmd re-parse). */
let mcporterCliPath = /** @type {string | null} */ (null);

/**
* Install pinned mcporter into a unique per-run temp prefix and return its CLI.
* Invocations then use `process.execPath` with `shell: false` so `--args` JSON
* is not stripped by cmd.exe (which is what breaks submit on Windows).
* Uses mkdtempSync so concurrent smokes never share a half-installed tree.
* @returns {string}
*/
function ensureMcporterCli() {
if (mcporterCliPath) return mcporterCliPath;
const pinDir = mkdtempSync(path.join(tmpdir(), "olive-studio-mcporter-"));
const cli = path.join(pinDir, "node_modules", "mcporter", "dist", "cli.js");
// Fresh dir: install may need shell on Windows for npm.cmd only.
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
const install = spawnSync(
npmCmd,
[
"install",
"--prefix",
pinDir,
MCPORTER,
"--no-save",
"--no-package-lock",
"--no-fund",
"--no-audit",
"--loglevel=error",
],
{
encoding: "utf8",
timeout: 180_000,
shell: process.platform === "win32",
env: { ...process.env },
},
);
if (install.status !== 0) {
try {
rmSync(pinDir, { recursive: true, force: true });
} catch {
/* best-effort */
}
throw new Error(
`failed to install ${MCPORTER}: ${(install.stderr || install.stdout || "").slice(0, 800)}`,
);
}
if (!existsSync(cli)) {
try {
rmSync(pinDir, { recursive: true, force: true });
} catch {
/* best-effort */
}
throw new Error(`mcporter CLI missing after install: ${cli}`);
}
mcporterCliPath = cli;
return cli;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Resolve the pinned CLI, append `--config`, and log the invocation banner.
* @param {string[]} args mcporter argv (without node/cli/`--config`)
* @returns {{ cli: string, full: string[] }}
*/
function prepareMcporterInvocation(args) {
const cli = ensureMcporterCli();
const full = [...args, "--config", smokeConfigPath];
console.log(
`$ node ${path.basename(path.dirname(path.dirname(cli)))}/dist/cli.js ${full.join(" ")}`,
);
return { cli, full };
}

/**
* @param {string[]} args mcporter argv (without node/cli)
* @param {{ timeoutMs?: number, env?: NodeJS.ProcessEnv, expectOk?: boolean }} [opts]
*/
function runMcporterSync(args, opts = {}) {
const { timeoutMs = 90_000, env = process.env, expectOk = true } = opts;
const { cli, full } = prepareMcporterInvocation(args);
const r = spawnSync(process.execPath, [cli, ...full], {
cwd: root,
encoding: "utf8",
timeout: timeoutMs,
shell: false,
env: { ...env },
});
if (r.stdout) process.stdout.write(r.stdout);
if (r.stderr) process.stderr.write(r.stderr);
if (expectOk && r.status !== 0) {
throw new Error(`mcporter failed status=${r.status} signal=${r.signal}`);
}
return r;
}

/** Minimal CPU recipe accepted by Studio preflight (no real Olive execute required). */
const SMOKE_RECIPE = {
input_model: { type: "PyTorchModel", config: {} },
Expand Down Expand Up @@ -139,22 +237,7 @@ function patchAgentAccessDisk(patch) {
* @param {{ timeoutMs?: number, env?: NodeJS.ProcessEnv, expectOk?: boolean }} [opts]
*/
function run(args, opts = {}) {
const { timeoutMs = 90_000, env = process.env, expectOk = true } = opts;
const full = ["--yes", MCPORTER, ...args, "--config", smokeConfigPath];
console.log(`$ npx ${full.join(" ")}`);
const r = spawnSync("npx", full, {
cwd: root,
encoding: "utf8",
timeout: timeoutMs,
shell: process.platform === "win32",
env: { ...env },
});
if (r.stdout) process.stdout.write(r.stdout);
if (r.stderr) process.stderr.write(r.stderr);
if (expectOk && r.status !== 0) {
throw new Error(`mcporter failed status=${r.status} signal=${r.signal}`);
}
return r;
return runMcporterSync(args, opts);
}

/** @param {string} stdout */
Expand Down Expand Up @@ -185,14 +268,13 @@ function callToolAsync(selector, toolArgs, opts = {}) {
args.push("--args", JSON.stringify(toolArgs));
}
args.push("--timeout", String(timeoutMs), "--output", "json");
const full = ["--yes", MCPORTER, ...args, "--config", smokeConfigPath];
console.log(`$ npx ${full.join(" ")}`);
const { cli, full } = prepareMcporterInvocation(args);

return new Promise((resolve, reject) => {
const child = spawn("npx", full, {
const child = spawn(process.execPath, [cli, ...full], {
cwd: root,
env: { ...(opts.env ?? process.env) },
shell: process.platform === "win32",
shell: false,
});
let stdout = "";
let stderr = "";
Expand Down Expand Up @@ -609,6 +691,7 @@ async function cleanup() {
function failExitCode(signal) {
if (signal === "SIGINT") return 130;
if (signal === "SIGTERM") return 143;
if (signal === "SIGHUP") return 129;
return STRICT ? 1 : 0;
}

Expand Down
Loading
Loading