Skip to content

Commit 318176c

Browse files
committed
test(coverage): drive mcp to >=95% line + branch coverage
Adds tests for every src/* surface: the InstantClient HTTP client (error envelope branches, requestMultipart edges, empty-2xx sentinel), the index.ts tool handlers (every create_*/lifecycle handler's success + catch + optional-field branch), and the pure-helper exports (formatError's every cascade arm, formatLimits typed-limit branches, appendUpgradeBlock's 4 truth combinations). To unit-test the tool handlers and helpers directly, src/index.ts now: - exports `formatError`, `formatLimits`, `appendUpgradeBlock`, `textResult` (was module-private; integration tests still exercise them via the spawned subprocess, unchanged) - exports the `server` instance so unit tests can pluck registered tool callbacks out of its `_registeredTools` map and call them in-process - guards the top-level `await server.connect(transport)` behind INSTANODE_MCP_NO_LISTEN — the production binary path and the integration tests' `node dist/index.js` invocation never set this var, so prod behavior is unchanged - wraps the package.json version read in a try/catch with a "dev" fallback (mirrors client.ts's User-Agent resolver) so unit tests importing from a non-canonical build path don't crash before any test code runs Coverage on src/ (via dist-test/src/, where `npm test` measures --experimental-test-coverage): Before: client 93.71% / 76.77%, index 92.29% / 30.77% After: client 100.00% / 95.04%, index 99.70% / 95.00% (index uncovered lines 995-997 = the production `await server.connect()` path, unreachable in unit tests) All files: 99.81% lines / 95.03% branches Test count: 87 -> 248 passing (161 new unit-level tests across three new/extended files): - test/client-unit.test.ts (extended; was 25 tests, now 73) - test/index-unit.test.ts (new; 35 tests for the pure helpers) - test/tools-unit.test.ts (new; 79 tests for every tool handler, including error-injection paths via stubbed globalThis.fetch) Integration suite (87 tests) unchanged and still passing — the new unit-level coverage is additive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b3d0d5f commit 318176c

5 files changed

Lines changed: 2552 additions & 9 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"dev": "tsc --watch",
4747
"start": "node dist/index.js",
4848
"pretest": "tsc && tsc -p tsconfig.test.json",
49-
"test": "node --test dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/client-unit.test.js",
49+
"test": "node --test --experimental-test-coverage --test-coverage-exclude='dist-test/test/**' --test-coverage-exclude='dist/**' --test-coverage-exclude='node_modules/**' dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js",
50+
"test:nocov": "node --test dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js",
5051
"test:smoke": "bash test.sh",
5152
"prepublishOnly": "npm run build"
5253
},

src/index.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,26 @@ import { nameSchema } from "./name_schema.js";
5959

6060
const client = new InstantClient();
6161

62-
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
63-
const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version as string;
62+
/**
63+
* Resolve the package.json version once at module-init. Falls back to "dev"
64+
* if the file isn't where we expect — same defensive pattern as the
65+
* User-Agent resolver in client.ts, so unit tests that import this module
66+
* from a non-canonical build path (e.g. `dist-test/src/index.js`, two dirs
67+
* removed from the package.json instead of one) don't crash before any
68+
* test code runs. The production binary path (`dist/index.js`) is one
69+
* level under the repo root, so the resolve always finds the file.
70+
*/
71+
function resolvePkgVersion(): string {
72+
try {
73+
const here = dirname(fileURLToPath(import.meta.url));
74+
const pkgPath = resolve(here, "..", "package.json");
75+
return (JSON.parse(readFileSync(pkgPath, "utf8")).version as string) ?? "dev";
76+
} catch {
77+
return "dev";
78+
}
79+
}
80+
81+
const pkgVersion = resolvePkgVersion();
6482

6583
const server = new McpServer({
6684
name: "instanode.dev",
@@ -85,7 +103,7 @@ const server = new McpServer({
85103
* Upgrade: {upgrade_url, when present}
86104
* Claim: {claim_url, when present}
87105
*/
88-
function formatError(err: unknown): string {
106+
export function formatError(err: unknown): string {
89107
if (err instanceof AuthRequiredError) {
90108
return err.message;
91109
}
@@ -152,11 +170,11 @@ function formatError(err: unknown): string {
152170
return `instanode.dev error: ${msg}`;
153171
}
154172

155-
function textResult(text: string) {
173+
export function textResult(text: string) {
156174
return { content: [{ type: "text" as const, text }] };
157175
}
158176

159-
function formatLimits(limits: ProvisionLimits | undefined): string[] {
177+
export function formatLimits(limits: ProvisionLimits | undefined): string[] {
160178
const lines: string[] = [];
161179
if (!limits) return lines;
162180
if (typeof limits.storage_mb === "number") lines.push(`Storage: ${limits.storage_mb} MB`);
@@ -172,7 +190,7 @@ function formatLimits(limits: ProvisionLimits | undefined): string[] {
172190
* so the end user sees the exact CTA + claim URL. Structurally typed so
173191
* it accepts both ProvisionResultBase and DeployResult.
174192
*/
175-
function appendUpgradeBlock(
193+
export function appendUpgradeBlock(
176194
lines: string[],
177195
result: { note?: string; upgrade?: string }
178196
): void {
@@ -1136,5 +1154,17 @@ Requires INSTANODE_TOKEN.`,
11361154

11371155
// ── Start server ──────────────────────────────────────────────────────────────
11381156

1139-
const transport = new StdioServerTransport();
1140-
await server.connect(transport);
1157+
// Unit tests import this module purely to reach the exported helpers
1158+
// (formatError / formatLimits / appendUpgradeBlock) without binding to a real
1159+
// stdio transport — set INSTANODE_MCP_NO_LISTEN=1 in that case. The CLI binary
1160+
// path (and integration tests that spawn `node dist/index.js`) never set this
1161+
// var, so the production behavior is unchanged.
1162+
if (!process.env["INSTANODE_MCP_NO_LISTEN"]) {
1163+
const transport = new StdioServerTransport();
1164+
await server.connect(transport);
1165+
}
1166+
1167+
// Re-export the MCP server so unit tests can introspect the tool registry
1168+
// without spawning a subprocess. Production callers ignore this — the binary
1169+
// entrypoint only depends on the `await server.connect(...)` above.
1170+
export { server };

0 commit comments

Comments
 (0)