From aa8e2e72e5c44c6eccfa68a241ed06c52504c9c1 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Mon, 2 Mar 2026 15:15:35 -0700 Subject: [PATCH] fix: correct MCP server entry in init + add config tests - Fix .mcp.json generation: was using broken path (npx tsx node_modules/...), now correctly uses 'npx --package preflight-dev@latest preflight-serve' - Add bin/serve.js as dedicated MCP server entry point - Add 'preflight-serve' bin to package.json - Add 6 tests for config system (defaults, env vars, yaml loading, error handling) --- bin/serve.js | 10 ++++ package.json | 3 +- src/cli/init.ts | 11 +--- tests/lib/config.test.ts | 116 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 bin/serve.js create mode 100644 tests/lib/config.test.ts diff --git a/bin/serve.js b/bin/serve.js new file mode 100644 index 0000000..53ab153 --- /dev/null +++ b/bin/serve.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +// MCP server entry point — loads the compiled index +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const serverPath = join(__dirname, '../dist/index.js'); +await import(serverPath); diff --git a/package.json b/package.json index 141cc1b..6953dd5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "main": "dist/index.js", "bin": { - "preflight-dev": "./bin/cli.js" + "preflight-dev": "./bin/cli.js", + "preflight-serve": "./bin/serve.js" }, "engines": { "node": ">=18" diff --git a/src/cli/init.ts b/src/cli/init.ts index 996906d..4a31b99 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -106,16 +106,7 @@ async function main(): Promise { config.mcpServers["preflight"] = { command: "npx", - args: ["-y", "preflight-dev@latest"], - env, - }; - - // For the actual server entry point, we need to point to index.ts via tsx - // But npx will resolve the bin entry which is the init script - // So use a different approach: command runs the server - config.mcpServers["preflight"] = { - command: "npx", - args: ["-y", "tsx", "node_modules/preflight/src/index.ts"], + args: ["-y", "--package", "preflight-dev@latest", "preflight-serve"], env, }; diff --git a/tests/lib/config.test.ts b/tests/lib/config.test.ts new file mode 100644 index 0000000..265da57 --- /dev/null +++ b/tests/lib/config.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock fs and path before importing config +vi.mock("fs", () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), +})); + +vi.mock("js-yaml", () => ({ + load: vi.fn(), +})); + +vi.mock("../../src/lib/files.js", () => ({ + PROJECT_DIR: "/mock/project", +})); + +import { existsSync, readFileSync } from "fs"; +import { load as yamlLoad } from "js-yaml"; + +// Reset config singleton between tests +async function freshImport() { + vi.resetModules(); + return await import("../../src/lib/config.js"); +} + +describe("config", () => { + beforeEach(() => { + vi.resetAllMocks(); + delete process.env.PROMPT_DISCIPLINE_PROFILE; + delete process.env.PREFLIGHT_RELATED; + delete process.env.EMBEDDING_PROVIDER; + delete process.env.OPENAI_API_KEY; + }); + + it("returns default config when no .preflight/ dir exists", async () => { + vi.mocked(existsSync).mockReturnValue(false); + const { getConfig } = await freshImport(); + const config = getConfig(); + expect(config.profile).toBe("standard"); + expect(config.triage.strictness).toBe("standard"); + expect(config.embeddings.provider).toBe("local"); + }); + + it("reads env vars when no .preflight/ dir exists", async () => { + vi.mocked(existsSync).mockReturnValue(false); + process.env.PROMPT_DISCIPLINE_PROFILE = "minimal"; + process.env.PREFLIGHT_RELATED = "/path/a, /path/b"; + process.env.EMBEDDING_PROVIDER = "openai"; + process.env.OPENAI_API_KEY = "sk-test"; + + const { getConfig } = await freshImport(); + const config = getConfig(); + expect(config.profile).toBe("minimal"); + expect(config.related_projects).toHaveLength(2); + expect(config.related_projects[0].path).toBe("/path/a"); + expect(config.related_projects[1].alias).toBe("b"); + expect(config.embeddings.provider).toBe("openai"); + expect(config.embeddings.openai_api_key).toBe("sk-test"); + }); + + it("loads .preflight/config.yml when present", async () => { + vi.mocked(existsSync).mockImplementation((p: any) => { + const s = String(p); + return s.includes(".preflight") && !s.includes("triage"); + }); + vi.mocked(readFileSync).mockReturnValue(""); + vi.mocked(yamlLoad).mockReturnValue({ + profile: "full", + thresholds: { session_stale_minutes: 60 }, + }); + + const { getConfig } = await freshImport(); + const config = getConfig(); + expect(config.profile).toBe("full"); + expect(config.thresholds.session_stale_minutes).toBe(60); + // Other thresholds keep defaults + expect(config.thresholds.max_tool_calls_before_checkpoint).toBe(100); + }); + + it("ignores env vars when .preflight/ dir exists", async () => { + vi.mocked(existsSync).mockImplementation((p: any) => { + return String(p).includes(".preflight"); + }); + vi.mocked(readFileSync).mockReturnValue(""); + vi.mocked(yamlLoad).mockReturnValue(null); + process.env.PROMPT_DISCIPLINE_PROFILE = "full"; + + const { getConfig } = await freshImport(); + const config = getConfig(); + // Should use default, not env var, because .preflight/ exists + expect(config.profile).toBe("standard"); + }); + + it("hasPreflightConfig returns true when dir exists", async () => { + vi.mocked(existsSync).mockImplementation((p: any) => { + return String(p).endsWith(".preflight"); + }); + const { hasPreflightConfig } = await freshImport(); + expect(hasPreflightConfig()).toBe(true); + }); + + it("handles malformed yaml gracefully", async () => { + vi.mocked(existsSync).mockImplementation((p: any) => { + return String(p).includes(".preflight") && !String(p).includes("triage"); + }); + vi.mocked(readFileSync).mockReturnValue(""); + vi.mocked(yamlLoad).mockImplementation(() => { + throw new Error("bad yaml"); + }); + + const { getConfig } = await freshImport(); + // Should fall back to defaults without throwing + const config = getConfig(); + expect(config.profile).toBe("standard"); + }); +});