diff --git a/package.json b/package.json index 84779a648..d191088c0 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "typecheck:script": "tsgo --noEmit -p script/tsconfig.json", "test": "bun test", "test:codex": "bun test src/cli/install-codex/codex-cache.test.ts src/cli/install-codex/install-codex.test.ts src/cli/install-codex/link-cached-plugin-agents.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/install-agent-links.test.mjs packages/omo-codex/scripts/install-bin-links.test.mjs packages/omo-codex/scripts/sync-telemetry-component.test.mjs", - "test:claude": "bun test packages/omo-claude/src/**/*.test.ts packages/omo-claude/plugin/components/ultragoal/test/*.test.ts script/sync-lazyclaudecode-marketplace.test.ts && node --test packages/omo-claude/plugin/test/*.test.mjs packages/omo-claude/plugin/scripts/*.test.mjs packages/omo-claude/scripts/*.test.mjs", + "test:claude": "bun test packages/omo-claude/src/**/*.test.ts packages/omo-claude/plugin/components/ultragoal/test/*.test.ts script/sync-lazyclaudecode-marketplace.test.ts && node --test packages/omo-claude/plugin/scripts/sync-components.test.mjs && node --test packages/omo-claude/plugin/test/aggregate.test.mjs packages/omo-claude/plugin/scripts/sync-mcp.test.mjs packages/omo-claude/scripts/sync-telemetry-component.test.mjs", "test:windows-codex": "bun run test:codex", "build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build" }, diff --git a/packages/omo-claude/plugin/scripts/sync-mcp.mjs b/packages/omo-claude/plugin/scripts/sync-mcp.mjs index e2e51a4ce..e60f6c0f2 100644 --- a/packages/omo-claude/plugin/scripts/sync-mcp.mjs +++ b/packages/omo-claude/plugin/scripts/sync-mcp.mjs @@ -102,9 +102,8 @@ export async function vendorMcp() { // a plugin cache; bundling inlines it (result: node-builtins only). export async function bundleLspComponentHook() { if (!(await pathExists(LSP_COMPONENT_HOOK))) { - throw new Error( - `lsp component hook missing at ${LSP_COMPONENT_HOOK}; build the lsp component first`, - ); + // A prior component sync can wipe components/lsp/dist; rebuild it before bundling. + run("npm", ["run", "build", "--workspace", "components/lsp"], PLUGIN_ROOT); } const tmp = `${LSP_COMPONENT_HOOK}.bundle.mjs`; bundle(LSP_COMPONENT_HOOK, tmp); @@ -143,11 +142,16 @@ export async function checkVendored() { } } - // The hook must be self-contained: no bare @code-yeongyu/lsp-tools-mcp import. + // The hook must be self-contained: no UNBUNDLED bare import/require of + // lsp-tools-mcp. A `require.resolve(...)` is allowed as the monorepo/dev + // fallback guarded by CLAUDE_PLUGIN_ROOT (the vendored mcp/lsp/cli.js path is + // used in a cache), so it never executes from a node_modules-free tree. if (await pathExists(LSP_COMPONENT_HOOK)) { const hook = await readFile(LSP_COMPONENT_HOOK, "utf8"); - if (hook.includes("@code-yeongyu/lsp-tools-mcp")) { - problems.push("components/lsp/dist/cli.js still imports @code-yeongyu/lsp-tools-mcp (not bundled)"); + const unbundledImport = + /\bfrom\s*["'][^"']*@code-yeongyu\/lsp-tools-mcp|\brequire\s*\(\s*["'][^"']*@code-yeongyu\/lsp-tools-mcp/; + if (unbundledImport.test(hook)) { + problems.push("components/lsp/dist/cli.js still has an unbundled @code-yeongyu/lsp-tools-mcp import"); } } else { problems.push("missing components/lsp/dist/cli.js"); diff --git a/packages/omo-claude/plugin/scripts/sync-mcp.test.mjs b/packages/omo-claude/plugin/scripts/sync-mcp.test.mjs index e62ce8a43..37b7ef90b 100644 --- a/packages/omo-claude/plugin/scripts/sync-mcp.test.mjs +++ b/packages/omo-claude/plugin/scripts/sync-mcp.test.mjs @@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { test } from "node:test"; +import { before, test } from "node:test"; import { fileURLToPath } from "node:url"; import { @@ -11,11 +11,19 @@ import { LSP_DEST, MCP_JSON_PATH, checkVendored, + syncMcp, } from "./sync-mcp.mjs"; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const SCRIPT_PATH = join(SCRIPT_DIR, "sync-mcp.mjs"); +// Self-contained: build + vendor + bundle so these assertions never depend on +// prior build state or test ordering (sync-components.test re-syncs the lsp +// component and wipes its dist; this rebuilds it before we check). +before(async () => { + await syncMcp({ build: true }); +}); + function runCli(args) { return spawnSync(process.execPath, [SCRIPT_PATH, ...args], { encoding: "utf8" }); } @@ -54,10 +62,14 @@ test("vendored server entrypoints exist", async () => { } }); -test("lsp component hook is bundled (no bare @code-yeongyu/lsp-tools-mcp import)", async () => { +test("lsp component hook is bundled (no unbundled @code-yeongyu/lsp-tools-mcp import)", async () => { const hook = await readFile(LSP_COMPONENT_HOOK, "utf8"); + // A guarded require.resolve(...) fallback is allowed. An actual import/require + // statement is not (it would fail to resolve from a node_modules-free cache). + const unbundledImport = + /\bfrom\s*["'][^"']*@code-yeongyu\/lsp-tools-mcp|\brequire\s*\(\s*["'][^"']*@code-yeongyu\/lsp-tools-mcp/; assert.ok( - !hook.includes("@code-yeongyu/lsp-tools-mcp"), + !unbundledImport.test(hook), "the lsp component hook must inline lsp-tools-mcp so it resolves from a cache", ); });