test: verify published stdio entry behavior

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jax 2026-03-13 18:37:42 +08:00
parent 94a041d502
commit 37d94c669a
3 changed files with 159 additions and 3 deletions

View File

@ -0,0 +1,123 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const projectRoot = resolve(import.meta.dirname, "..");
async function readPublishedBinName() {
const packageJsonRaw = await readFile(join(projectRoot, "package.json"), "utf8");
const packageJson = JSON.parse(packageJsonRaw);
const binNames = Object.keys(packageJson.bin ?? {});
if (binNames.length !== 1) {
throw new Error(`Expected exactly one published bin entry, found ${binNames.length}`);
}
return binNames[0];
}
function run(command, args, options = {}) {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(command, args, {
cwd: projectRoot,
stdio: ["ignore", "pipe", "pipe"],
...options,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", rejectPromise);
child.on("exit", (code) => {
if (code === 0) {
resolvePromise({ stdout, stderr });
return;
}
rejectPromise(
new Error(
`${command} ${args.join(" ")} failed with code ${code}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`,
),
);
});
});
}
async function verifyPackagedBin() {
const packDir = await mkdtemp(join(tmpdir(), "mcp-template-pack-"));
const installDir = await mkdtemp(join(tmpdir(), "mcp-template-install-"));
const binName = await readPublishedBinName();
try {
await run("pnpm", ["pack", "--pack-destination", packDir]);
const tarballs = (await readdir(packDir)).filter((name) => name.endsWith(".tgz"));
if (tarballs.length !== 1) {
throw new Error(`Expected exactly one tarball, found ${tarballs.length}`);
}
const tarballPath = join(packDir, tarballs[0]);
await writeFile(join(installDir, "package.json"), '{"name":"publish-bin-check","private":true}\n');
await run("npm", ["install", tarballPath], { cwd: installDir });
const startup = spawn(join(installDir, "node_modules", ".bin", binName), [], {
cwd: installDir,
stdio: ["ignore", "pipe", "pipe"],
});
const startupOutput = await new Promise((resolvePromise, rejectPromise) => {
let stderr = "";
const timeout = setTimeout(() => {
startup.kill("SIGTERM");
rejectPromise(new Error(`Timed out waiting for packaged bin to start\nSTDERR:\n${stderr}`));
}, 10000);
startup.stderr.on("data", (chunk) => {
stderr += chunk.toString();
if (stderr.includes("MCP stdio server started")) {
clearTimeout(timeout);
startup.kill("SIGTERM");
resolvePromise(stderr);
}
});
startup.on("error", (error) => {
clearTimeout(timeout);
rejectPromise(error);
});
startup.on("exit", (code, signal) => {
if (signal === "SIGTERM") {
return;
}
clearTimeout(timeout);
rejectPromise(
new Error(
`Packaged bin exited before startup completed (code=${code}, signal=${signal})\nSTDERR:\n${stderr}`,
),
);
});
});
if (!String(startupOutput).includes("MCP stdio server started")) {
throw new Error("Packaged bin did not emit the expected startup log");
}
} finally {
await rm(packDir, { recursive: true, force: true });
await rm(installDir, { recursive: true, force: true });
}
}
await verifyPackagedBin();

View File

@ -1,5 +1,8 @@
#!/usr/bin/env node
import { realpathSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpCore } from "./core/index.js";
import { asError, toErrorPayload } from "./lib/errors.js";
@ -30,7 +33,10 @@ function isDirectExecution(): boolean {
return false;
}
return import.meta.url === pathToFileURL(resolve(entryPath)).href;
const runtimeEntryPath = realpathSync(resolve(entryPath));
const modulePath = realpathSync(fileURLToPath(import.meta.url));
return pathToFileURL(modulePath).href === pathToFileURL(runtimeEntryPath).href;
}
if (isDirectExecution()) {

View File

@ -12,8 +12,11 @@ const stdioEntryPath = resolve(projectRoot, "src/stdio.ts");
const packageJsonPath = resolve(projectRoot, "package.json");
type StdioCommandConfig = {
name?: string;
bin?: Record<string, string>;
mcp?: {
stdio?: {
developmentOnly?: boolean;
command?: string;
args?: string[];
};
@ -101,10 +104,11 @@ describe("stdio entrypoint", () => {
await transport.close();
});
it("publishes a non-npm stdio launch command for MCP clients", async () => {
it("publishes a development-only non-npm stdio launch command for workspace MCP clients", async () => {
const packageJsonRaw = await readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonRaw) as StdioCommandConfig;
expect(packageJson.mcp?.stdio?.developmentOnly).toBe(true);
expect(packageJson.mcp?.stdio?.command).not.toMatch(/^npm|^npx/);
expect(packageJson.scripts?.["start:stdio"]).toBeUndefined();
expect(packageJson.mcp?.stdio?.command).toBe("node");
@ -114,10 +118,33 @@ describe("stdio entrypoint", () => {
]);
});
it("publishes a bin entry for npx-based stdio startup", async () => {
const packageJsonRaw = await readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonRaw) as StdioCommandConfig;
expect(packageJson.name).toBeDefined();
expect(packageJson.bin).toEqual({
[packageJson.name as string]: "./dist/stdio.js",
});
});
it("does not write non-protocol logs to stdout from source entry", async () => {
const source = await readFile(stdioEntryPath, "utf8");
expect(source).not.toMatch(/console\.log\(/);
expect(source).not.toMatch(/process\.stdout\.write\(/);
});
it("keeps the published stdio entry executable", async () => {
const source = await readFile(stdioEntryPath, "utf8");
expect(source).toMatch(/^#!\/usr\/bin\/env node/m);
});
it("publishes a dedicated package-level verification script for the bin entry", async () => {
const packageJsonRaw = await readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonRaw) as StdioCommandConfig;
expect(packageJson.scripts?.["test:publish"]).toBe("node ./scripts/verify-publish-bin.mjs");
});
});