test(omo-claude): make sync-mcp self-healing and test:claude order-independent

sync-mcp rebuilds the lsp component if a prior sync wiped its dist; sync-mcp.test
builds in a before hook; checkVendored flags only real unbundled imports (a guarded
require.resolve fallback is allowed); test:claude runs the destructive sync-components
test in its own node --test pass so it never races the build-checking tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
YeonGyu-Kim
2026-05-29 14:11:57 +09:00
parent 027c543340
commit 9ddb2b9819
3 changed files with 26 additions and 10 deletions
+1 -1
View File
@@ -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"
},
@@ -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");
@@ -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",
);
});